Motion
Restrained, functional motion. Every animation has to justify what it improves.
Motion only gets in if it supports what is being said. The tone is precise, not elastic: no animated text, no parallax, spring or skeletons.
Layers
| Layer | What | How |
| 1. Smooth scroll | Smoothed global scroll | Lenis on every page |
| 2. Staggered entrance | Fade-in on load | .fade-entry, random 50-200ms delay, 0.4s ease-out |
| 3. Scroll reveal | Elements entering the viewport | Intersection Observer + CSS |
| 4. Interactions | Hover, states, modal | CSS transitions per component |
Duration and easing
| Interaction | Duration | Easing |
| Link/button hover | 0.2s | ease |
| NavPill show/hide | 0.3s | ease-out |
| BentoGrid body reveal | 0.3s | ease-out |
| Modal open | 0.3s | ease-out |
| Modal close | 0.2s | ease-in |
What doesn't animate
- Prose word by word or line by line. Whole blocks only.
- Parallax. Never. On no element.
- Loading animations (skeletons, shimmers). Static site.
- Spring physics. The tone is precise, not elastic.
Allowed stack
| Library | Use | Loading |
| Lenis | Smooth scroll | Every page |
| CSS transitions | Hover, states, reveals | Native |
| Intersection Observer | Scroll reveal trigger | Native API |
| GSAP | Timeline coordination only | Per component, never global |