# 08 · Motion

> Motion in this brand confirms that something happened. It never performs.

---

## 8.1 The principle

Since Labs is calm software for people who are busy. **Animation is feedback,
not personality.** If a movement is there to be admired rather than to explain a
change of state, cut it.

The test: *would a person notice if this were instant?* If the answer is no,
make it instant. If the answer is "they would be confused about what just
happened", that is what the motion is for.

---

## 8.2 The scale

| Token | Duration | Easing | Use |
|---|---|---|---|
| Instant | 0ms | — | State that must read as a fact, not an animation |
| **Fast** | **120ms** | `cubic-bezier(0.2, 0, 0, 1)` | **The default.** Colour, opacity, border on hover and focus |
| Base | 200ms | same | Drawers, disclosure, position changes |
| Slow | 320ms | same | Full-surface transitions. The ceiling |

**Nothing in this brand animates longer than 320ms.** One easing curve
throughout — an ease-out that starts immediately and settles. No bounce, no
elastic, no spring.

---

## 8.3 What animates

| Property | Verdict |
|---|---|
| `color`, `background-color`, `border-color` | ✅ The overwhelming majority of motion in the product |
| `opacity` | ✅ Fades, disclosure |
| `transform: translate` | ✅ Drawers and sheets only |
| `box-shadow` | ✅ Card hover, sparingly |
| `width`, `height`, `top`, `left` | ❌ Layout-thrashing. Use `transform` |
| `rotate`, `scale` | ❌ Except a loading spinner |

Prefer `transition-colors` over `transition-all`. `transition-all` animates
properties you did not intend and produces the jelly effect that makes an
interface feel cheap.

---

## 8.4 Patterns

**Hover** — 120ms colour change. Never a size change; a control that grows under
the cursor moves the thing you were about to click.

**Focus** — the ring appears **instantly**. Never fade a focus ring: a keyboard
user tabbing quickly must see where they are without waiting.

**Drawer / mobile sidebar** — 200ms `translateX`, backdrop fades over the same
duration.

**Modal** — appears instantly. It is a mode change, and a fade makes the whole
app feel slow.

**Loading** — a 1s linear spin for a spinner. For content, prefer a skeleton in
`surface-mist` with a 2s pulse over a spinner, and prefer *nothing* over a
skeleton if the wait is under ~300ms.

**Route change** — nothing. The page changes. Page transitions are a personality
we have not earned.

---

## 8.5 Reduced motion

Non-negotiable. Ships in `tokens/since-brand.css`:

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
```

Anything driven by JavaScript must check the same signal:

```ts
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
```

Reduced motion means the **end state**, immediately — never a missing state.

---

## 8.6 Do and don't

**Do**
- ✅ 120ms colour transitions as the default
- ✅ Instant focus rings and instant modals
- ✅ One easing curve everywhere
- ✅ Honour `prefers-reduced-motion`

**Don't**
- ❌ `transition-all`
- ❌ Animate anything past 320ms
- ❌ Bounce, spring, or elastic easing
- ❌ Scale a control on hover
- ❌ Add a page transition
