MOTION — Good motion is invisible
Applied app-wide on 2 Oct 2026 in ONE commit so it can be rolled back cleanly.
Rollback point: branch backup-before-motion = commit 3c45a62 (site exactly as it was before motion).
The six rules
- Under 250ms. UI transitions use 150–250ms. Tokens in
app/globals.css:--dur-fast150ms,--dur-base200ms,--dur-slow250ms. - Ease out.
--ease-out: cubic-bezier(0.25, 1, 0.5, 1)for things appearing on screen. - No overshoot on standard UI (menus, cards, modals, buttons). Springy bounce is only for delight: mascots and success moments.
- Transform origin = the trigger. The menu card grows from top-right (the 3-line button). Mascots are anchored to the floor (
center bottom). - Reduced motion.
prefers-reduced-motion: reducemeans fade, don't fly: loops stop, bots stand still, cards fade only. - Transform and opacity only. Never animate width, height, top, left or margin. (The old nav animated height and the burger animated
top; both were rewritten.)
Deliberate exceptions (delight)
- Mascot idle loops, Oracle pulse, logo bob, and the tapped-bot springy bounce. They loop or overshoot on purpose. All stop under reduced motion.
Card motion — Ravneet, 6 Oct 2026 (applies to the timetable card AND every other card)
First used in components/timetable/timetable.css. Reuse these patterns for every new card:
- Cards on page load: fade + slide up 8px, 250ms ease-out (
.tt-card). List rows stagger 40ms (.tt-item). - Faculty / status dots on hover: bounce spring, transform only (
.tt-dot, 320mscubic-bezier(0.34,1.56,0.64,1)). - Buttons and tabs on click: scale spring to 0.94 (
.tt-press) — instant feedback. Approved exception to rule 3. - Images: lazy-load and fade in once loaded (
.tt-img+.is-loaded). - Reduced motion: cards and rows fade only; no bounce, no press scale.
- TODO: lift these into a shared
components/ui/motion.csswhen the next card type is built, so all cards share one file.
Why we did NOT use a blanket transform: none !important
It would break the 3-line to X icon and decorative glows under reduced motion. We switch off the specific moving things instead.
Checklist for any new UI
- Uses
var(--dur-*)andvar(--ease-out), nothing over 250ms (springs: dots 320ms, press 180ms). - Animates only transform and opacity.
- Has a reduced-motion version that fades.