diff --git a/.agents/skills/emil-design-eng/SKILL.md b/.agents/skills/emil-design-eng/SKILL.md
index 491123532..d79f6d64b 100644
--- a/.agents/skills/emil-design-eng/SKILL.md
+++ b/.agents/skills/emil-design-eng/SKILL.md
@@ -39,12 +39,12 @@ People select tools based on the overall experience, not just functionality. Goo
When reviewing UI code, you MUST use a markdown table with Before/After columns. Do NOT use a list with "Before:" and "After:" on separate lines. Always output an actual markdown table like this:
-| Before | After | Why |
-| --- | --- | --- |
-| `transition: all 300ms` | `transition: transform 200ms ease-out` | Specify exact properties; avoid `all` |
-| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing in the real world appears from nothing |
-| `ease-in` on dropdown | `ease-out` with custom curve | `ease-in` feels sluggish; `ease-out` gives instant feedback |
-| No `:active` state on button | `transform: scale(0.97)` on `:active` | Buttons must feel responsive to press |
+| Before | After | Why |
+| ------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------- |
+| `transition: all 300ms` | `transition: transform 200ms ease-out` | Specify exact properties; avoid `all` |
+| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing in the real world appears from nothing |
+| `ease-in` on dropdown | `ease-out` with custom curve | `ease-in` feels sluggish; `ease-out` gives instant feedback |
+| No `:active` state on button | `transform: scale(0.97)` on `:active` | Buttons must feel responsive to press |
| `transform-origin: center` on popover | `transform-origin: var(--radix-popover-content-transform-origin)` | Popovers should scale from their trigger (not modals — modals stay centered) |
Wrong format (never do this):
@@ -95,15 +95,15 @@ If the purpose is just "it looks cool" and the user will see it often, don't ani
### 3. What easing should it use?
Is the element entering or exiting?
- Yes → ease-out (starts fast, feels responsive)
- No →
- Is it moving/morphing on screen?
- Yes → ease-in-out (natural acceleration/deceleration)
- Is it a hover/color change?
- Yes → ease
- Is it constant motion (marquee, progress bar)?
- Yes → linear
- Default → ease-out
+Yes → ease-out (starts fast, feels responsive)
+No →
+Is it moving/morphing on screen?
+Yes → ease-in-out (natural acceleration/deceleration)
+Is it a hover/color change?
+Yes → ease
+Is it constant motion (marquee, progress bar)?
+Yes → linear
+Default → ease-out
**Critical: use custom easing curves.** The built-in CSS easings are too weak. They lack the punch that makes animations feel intentional.
@@ -160,15 +160,15 @@ Springs feel more natural than duration-based animations because they simulate r
Tying visual changes directly to mouse position feels artificial because it lacks motion. Use `useSpring` from Motion (formerly Framer Motion) to interpolate value changes with spring-like behavior instead of updating immediately.
```jsx
-import { useSpring } from 'framer-motion';
+import { useSpring } from "framer-motion";
// Without spring: feels artificial, instant
const rotation = mouseX * 0.1;
// With spring: feels natural, has momentum
const springRotation = useSpring(mouseX * 0.1, {
- stiffness: 100,
- damping: 10,
+ stiffness: 100,
+ damping: 10,
});
```
@@ -202,11 +202,11 @@ Add `transform: scale(0.97)` on `:active`. This gives instant feedback, making t
```css
.button {
- transition: transform 160ms ease-out;
+ transition: transform 160ms ease-out;
}
.button:active {
- transform: scale(0.97);
+ transform: scale(0.97);
}
```
@@ -221,13 +221,13 @@ Start from `scale(0.9)` or higher, combined with opacity. Even a barely-visible
```css
/* Bad */
.entering {
- transform: scale(0);
+ transform: scale(0);
}
/* Good */
.entering {
- transform: scale(0.95);
- opacity: 0;
+ transform: scale(0.95);
+ opacity: 0;
}
```
@@ -238,12 +238,12 @@ Popovers should scale in from their trigger, not from center. The default `trans
```css
/* Radix UI */
.popover {
- transform-origin: var(--radix-popover-content-transform-origin);
+ transform-origin: var(--radix-popover-content-transform-origin);
}
/* Base UI */
.popover {
- transform-origin: var(--transform-origin);
+ transform-origin: var(--transform-origin);
}
```
@@ -255,19 +255,21 @@ Tooltips should delay before appearing to prevent accidental activation. But onc
```css
.tooltip {
- transition: transform 125ms ease-out, opacity 125ms ease-out;
- transform-origin: var(--transform-origin);
+ transition:
+ transform 125ms ease-out,
+ opacity 125ms ease-out;
+ transform-origin: var(--transform-origin);
}
.tooltip[data-starting-style],
.tooltip[data-ending-style] {
- opacity: 0;
- transform: scale(0.97);
+ opacity: 0;
+ transform: scale(0.97);
}
/* Skip animation on subsequent tooltips */
.tooltip[data-instant] {
- transition-duration: 0ms;
+ transition-duration: 0ms;
}
```
@@ -278,17 +280,17 @@ CSS transitions can be interrupted and retargeted mid-animation. Keyframes resta
```css
/* Interruptible - good for UI */
.toast {
- transition: transform 400ms ease;
+ transition: transform 400ms ease;
}
/* Not interruptible - avoid for dynamic UI */
@keyframes slideIn {
- from {
- transform: translateY(100%);
- }
- to {
- transform: translateY(0);
- }
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
}
```
@@ -302,20 +304,22 @@ Combine blur with scale-on-press (`scale(0.97)`) for a polished button state tra
```css
.button {
- transition: transform 160ms ease-out;
+ transition: transform 160ms ease-out;
}
.button:active {
- transform: scale(0.97);
+ transform: scale(0.97);
}
.button-content {
- transition: filter 200ms ease, opacity 200ms ease;
+ transition:
+ filter 200ms ease,
+ opacity 200ms ease;
}
.button-content.transitioning {
- filter: blur(2px);
- opacity: 0.7;
+ filter: blur(2px);
+ opacity: 0.7;
}
```
@@ -327,14 +331,16 @@ The modern CSS way to animate element entry without JavaScript:
```css
.toast {
- opacity: 1;
- transform: translateY(0);
- transition: opacity 400ms ease, transform 400ms ease;
+ opacity: 1;
+ transform: translateY(0);
+ transition:
+ opacity 400ms ease,
+ transform 400ms ease;
- @starting-style {
- opacity: 0;
- transform: translateY(100%);
- }
+ @starting-style {
+ opacity: 0;
+ transform: translateY(100%);
+ }
}
```
@@ -343,7 +349,7 @@ This replaces the common React pattern of using `useEffect` to set `mounted: tru
```jsx
// Legacy pattern (still works everywhere)
useEffect(() => {
- setMounted(true);
+ setMounted(true);
}, []);
//
```
@@ -357,12 +363,12 @@ Percentage values in `translate()` are relative to the element's own size. Use `
```css
/* Works regardless of drawer height */
.drawer-hidden {
- transform: translateY(100%);
+ transform: translateY(100%);
}
/* Works regardless of toast height */
.toast-enter {
- transform: translateY(-100%);
+ transform: translateY(-100%);
}
```
@@ -378,16 +384,16 @@ Unlike `width`/`height`, `scale()` also scales an element's children. When scali
```css
.wrapper {
- transform-style: preserve-3d;
+ transform-style: preserve-3d;
}
@keyframes orbit {
- from {
- transform: translate(-50%, -50%) rotateY(0deg) translateZ(72px) rotateY(360deg);
- }
- to {
- transform: translate(-50%, -50%) rotateY(360deg) translateZ(72px) rotateY(0deg);
- }
+ from {
+ transform: translate(-50%, -50%) rotateY(0deg) translateZ(72px) rotateY(360deg);
+ }
+ to {
+ transform: translate(-50%, -50%) rotateY(360deg) translateZ(72px) rotateY(0deg);
+ }
}
```
@@ -406,22 +412,22 @@ Every element has an anchor point from which transforms execute. The default is
```css
/* Fully hidden from right */
.hidden {
- clip-path: inset(0 100% 0 0);
+ clip-path: inset(0 100% 0 0);
}
/* Fully visible */
.visible {
- clip-path: inset(0 0 0 0);
+ clip-path: inset(0 0 0 0);
}
/* Reveal from left to right */
.overlay {
- clip-path: inset(0 100% 0 0);
- transition: clip-path 200ms ease-out;
+ clip-path: inset(0 100% 0 0);
+ transition: clip-path 200ms ease-out;
}
.button:active .overlay {
- clip-path: inset(0 0 0 0);
- transition: clip-path 2s linear;
+ clip-path: inset(0 0 0 0);
+ transition: clip-path 2s linear;
}
```
@@ -452,7 +458,7 @@ const timeTaken = new Date().getTime() - dragStartTime.current.getTime();
const velocity = Math.abs(swipeAmount) / timeTaken;
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
- dismiss();
+ dismiss();
}
```
@@ -470,8 +476,8 @@ Ignore additional touch points after the initial drag begins. Without this, swit
```js
function onPress() {
- if (isDragging) return;
- // Start drag...
+ if (isDragging) return;
+ // Start drag...
}
```
@@ -491,7 +497,7 @@ Changing a CSS variable on a parent recalculates styles for all children. In a d
```js
// Bad: triggers recalc on all children
-element.style.setProperty('--swipe-amount', `${distance}px`);
+element.style.setProperty("--swipe-amount", `${distance}px`);
// Good: only affects this element
element.style.transform = `translateY(${distance}px)`;
@@ -520,10 +526,10 @@ CSS animations run off the main thread. When the browser is busy loading a new p
The Web Animations API gives you JavaScript control with CSS performance. Hardware-accelerated, interruptible, and no library needed.
```js
-element.animate([{ clipPath: 'inset(0 0 100% 0)' }, { clipPath: 'inset(0 0 0 0)' }], {
- duration: 1000,
- fill: 'forwards',
- easing: 'cubic-bezier(0.77, 0, 0.175, 1)',
+element.animate([{ clipPath: "inset(0 0 100% 0)" }, { clipPath: "inset(0 0 0 0)" }], {
+ duration: 1000,
+ fill: "forwards",
+ easing: "cubic-bezier(0.77, 0, 0.175, 1)",
});
```
@@ -535,25 +541,25 @@ Animations can cause motion sickness. Reduced motion means fewer and gentler ani
```css
@media (prefers-reduced-motion: reduce) {
- .element {
- animation: fade 0.2s ease;
- /* No transform-based motion */
- }
+ .element {
+ animation: fade 0.2s ease;
+ /* No transform-based motion */
+ }
}
```
```jsx
const shouldReduceMotion = useReducedMotion();
-const closedX = shouldReduceMotion ? 0 : '-100%';
+const closedX = shouldReduceMotion ? 0 : "-100%";
```
### Touch device hover states
```css
@media (hover: hover) and (pointer: fine) {
- .element:hover {
- transform: scale(1.05);
- }
+ .element:hover {
+ transform: scale(1.05);
+ }
}
```
@@ -596,12 +602,12 @@ Pressing should be slow when it needs to be deliberate (hold-to-delete: 2s linea
```css
/* Release: fast */
.overlay {
- transition: clip-path 200ms ease-out;
+ transition: clip-path 200ms ease-out;
}
/* Press: slow and deliberate */
.button:active .overlay {
- transition: clip-path 2s linear;
+ transition: clip-path 2s linear;
}
```
@@ -611,29 +617,29 @@ When multiple elements enter together, stagger their appearance. Each element an
```css
.item {
- opacity: 0;
- transform: translateY(8px);
- animation: fadeIn 300ms ease-out forwards;
+ opacity: 0;
+ transform: translateY(8px);
+ animation: fadeIn 300ms ease-out forwards;
}
.item:nth-child(1) {
- animation-delay: 0ms;
+ animation-delay: 0ms;
}
.item:nth-child(2) {
- animation-delay: 50ms;
+ animation-delay: 50ms;
}
.item:nth-child(3) {
- animation-delay: 100ms;
+ animation-delay: 100ms;
}
.item:nth-child(4) {
- animation-delay: 150ms;
+ animation-delay: 150ms;
}
@keyframes fadeIn {
- to {
- opacity: 1;
- transform: translateY(0);
- }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
```
@@ -664,16 +670,16 @@ For touch interactions (drawers, swipe gestures), test on physical devices. Conn
When reviewing UI code, check for:
-| Issue | Fix |
-| ------------------------------------------ | ---------------------------------------------------------------- |
-| `transition: all` | Specify exact properties: `transition: transform 200ms ease-out` |
-| `scale(0)` entry animation | Start from `scale(0.95)` with `opacity: 0` |
-| `ease-in` on UI element | Switch to `ease-out` or custom curve |
-| `transform-origin: center` on popover | Set to trigger location or use Radix/Base UI CSS variable (modals are exempt — keep centered) |
-| Animation on keyboard action | Remove animation entirely |
-| Duration > 300ms on UI element | Reduce to 150-250ms |
-| Hover animation without media query | Add `@media (hover: hover) and (pointer: fine)` |
-| Keyframes on rapidly-triggered element | Use CSS transitions for interruptibility |
-| Framer Motion `x`/`y` props under load | Use `transform: "translateX()"` for hardware acceleration |
-| Same enter/exit transition speed | Make exit faster than enter (e.g., enter 2s, exit 200ms) |
-| Elements all appear at once | Add stagger delay (30-80ms between items) |
+| Issue | Fix |
+| -------------------------------------- | --------------------------------------------------------------------------------------------- |
+| `transition: all` | Specify exact properties: `transition: transform 200ms ease-out` |
+| `scale(0)` entry animation | Start from `scale(0.95)` with `opacity: 0` |
+| `ease-in` on UI element | Switch to `ease-out` or custom curve |
+| `transform-origin: center` on popover | Set to trigger location or use Radix/Base UI CSS variable (modals are exempt — keep centered) |
+| Animation on keyboard action | Remove animation entirely |
+| Duration > 300ms on UI element | Reduce to 150-250ms |
+| Hover animation without media query | Add `@media (hover: hover) and (pointer: fine)` |
+| Keyframes on rapidly-triggered element | Use CSS transitions for interruptibility |
+| Framer Motion `x`/`y` props under load | Use `transform: "translateX()"` for hardware acceleration |
+| Same enter/exit transition speed | Make exit faster than enter (e.g., enter 2s, exit 200ms) |
+| Elements all appear at once | Add stagger delay (30-80ms between items) |
diff --git a/.agents/skills/review-animations/SKILL.md b/.agents/skills/review-animations/SKILL.md
index 6b46fd332..6b1c1074c 100644
--- a/.agents/skills/review-animations/SKILL.md
+++ b/.agents/skills/review-animations/SKILL.md
@@ -12,7 +12,7 @@ A specialized review skill. It does ONE thing: review animation and motion code
You are a senior motion-design reviewer with a brutal eye for craft. Your bias is toward **motion that feels right**, not motion that merely runs. A transition that "works" but feels sluggish, lands from the wrong origin, fires too often, or drops frames is a regression, not a pass. Default to flagging. Approval is earned, not assumed.
-The substantive bar comes from Emil Kowalski's animation philosophy (animations.dev). The review *method* — non-negotiable standards, escalation triggers, a remedial hierarchy, tiered output, and explicit approval criteria — is adapted from aggressive code-quality review.
+The substantive bar comes from Emil Kowalski's animation philosophy (animations.dev). The review _method_ — non-negotiable standards, escalation triggers, a remedial hierarchy, tiered output, and explicit approval criteria — is adapted from aggressive code-quality review.
For the full rule catalog (easing curves, duration tables, spring config, gestures, clip-path, performance, a11y), see [STANDARDS.md](STANDARDS.md). Load it whenever a finding needs a precise value or citation.
@@ -81,12 +81,12 @@ Two parts, in this order.
A single markdown table. One row per issue. Never a "Before:/After:" list.
-| Before | After | Why |
-| --- | --- | --- |
-| `transition: all 300ms` | `transition: transform 200ms ease-out` | Specify exact properties; `all` animates unintended properties off-GPU |
-| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing appears from nothing — `scale(0)` looks like it came from nowhere |
-| `ease-in` on dropdown | `ease-out` + custom curve | `ease-in` delays the moment the user watches most; feels sluggish |
-| `transform-origin: center` on popover | `var(--radix-popover-content-transform-origin)` | Popovers scale from their trigger, not center (modals are exempt) |
+| Before | After | Why |
+| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
+| `transition: all 300ms` | `transition: transform 200ms ease-out` | Specify exact properties; `all` animates unintended properties off-GPU |
+| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing appears from nothing — `scale(0)` looks like it came from nowhere |
+| `ease-in` on dropdown | `ease-out` + custom curve | `ease-in` delays the moment the user watches most; feels sluggish |
+| `transform-origin: center` on popover | `var(--radix-popover-content-transform-origin)` | Popovers scale from their trigger, not center (modals are exempt) |
### Part 2 — Verdict (REQUIRED)
diff --git a/.agents/skills/review-animations/STANDARDS.md b/.agents/skills/review-animations/STANDARDS.md
index b6dc9b19a..ee2327796 100644
--- a/.agents/skills/review-animations/STANDARDS.md
+++ b/.agents/skills/review-animations/STANDARDS.md
@@ -4,12 +4,12 @@ The precise values, curves, and rules behind the review. Cite these in findings
## Should it animate? (frequency table)
-| Frequency | Decision |
-| --- | --- |
-| 100+ times/day (keyboard shortcuts, command palette toggle) | No animation. Ever. |
-| Tens of times/day (hover effects, list navigation) | Remove or drastically reduce |
-| Occasional (modals, drawers, toasts) | Standard animation |
-| Rare / first-time (onboarding, feedback, celebrations) | Can add delight |
+| Frequency | Decision |
+| ----------------------------------------------------------- | ---------------------------- |
+| 100+ times/day (keyboard shortcuts, command palette toggle) | No animation. Ever. |
+| Tens of times/day (hover effects, list navigation) | Remove or drastically reduce |
+| Occasional (modals, drawers, toasts) | Standard animation |
+| Rare / first-time (onboarding, feedback, celebrations) | Can add delight |
**Never animate keyboard-initiated actions** — they repeat hundreds of times daily; animation makes them feel slow and disconnected. (Raycast has no open/close animation — correct for something used hundreds of times a day.)
@@ -18,33 +18,34 @@ Valid purposes for motion: spatial consistency, state indication, explanation, f
## Easing
Decision order:
+
- Entering or exiting → **`ease-out`** (starts fast, feels responsive)
- Moving / morphing on screen → **`ease-in-out`**
- Hover / color change → **`ease`**
- Constant motion (marquee, progress) → **`linear`**
- Default → **`ease-out`**
-**Never `ease-in` on UI.** It starts slow, delaying the exact moment the user is watching. `ease-out` at 200ms *feels* faster than `ease-in` at 200ms.
+**Never `ease-in` on UI.** It starts slow, delaying the exact moment the user is watching. `ease-out` at 200ms _feels_ faster than `ease-in` at 200ms.
Built-in CSS easings are too weak. Use strong custom curves:
```css
---ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* strong ease-out for UI */
---ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* strong ease-in-out for on-screen movement */
---ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer curve (Ionic) */
+--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* strong ease-out for UI */
+--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* strong ease-in-out for on-screen movement */
+--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer curve (Ionic) */
```
Find curves at [easing.dev](https://easing.dev/) or [easings.co](https://easings.co/) — don't hand-roll from scratch.
## Duration
-| Element | Duration |
-| --- | --- |
-| Button press feedback | 100–160ms |
-| Tooltips, small popovers | 125–200ms |
-| Dropdowns, selects | 150–250ms |
-| Modals, drawers | 200–500ms |
-| Marketing / explanatory | Can be longer |
+| Element | Duration |
+| ------------------------ | ------------- |
+| Button press feedback | 100–160ms |
+| Tooltips, small popovers | 125–200ms |
+| Dropdowns, selects | 150–250ms |
+| Modals, drawers | 200–500ms |
+| Marketing / explanatory | Can be longer |
**Rule: UI animations stay under 300ms.** A 180ms dropdown feels more responsive than a 400ms one. Faster spinners make load feel faster (same actual time). Instant tooltips after the first (skip delay + animation) make a toolbar feel faster.
@@ -53,8 +54,12 @@ Find curves at [easing.dev](https://easing.dev/) or [easings.co](https://easings
- **Never `scale(0)`.** Start from `scale(0.9–0.97)` + `opacity: 0`. Nothing in the real world appears from nothing.
- **Origin-aware popovers.** Scale from the trigger, not center:
```css
- .popover { transform-origin: var(--radix-popover-content-transform-origin); } /* Radix */
- .popover { transform-origin: var(--transform-origin); } /* Base UI */
+ .popover {
+ transform-origin: var(--radix-popover-content-transform-origin);
+ } /* Radix */
+ .popover {
+ transform-origin: var(--transform-origin);
+ } /* Base UI */
```
**Modals are exempt** — they appear centered in the viewport, keep `transform-origin: center`.
- **Button press feedback.** `transform: scale(0.97)` on `:active`, `transition: transform 160ms ease-out`. Subtle (0.95–0.98). Applies to any pressable element.
@@ -81,19 +86,34 @@ CSS **transitions** can be interrupted and retargeted mid-animation; **keyframes
```css
/* Interruptible — good for dynamic UI */
-.toast { transition: transform 400ms ease; }
+.toast {
+ transition: transform 400ms ease;
+}
/* Not interruptible — avoid for dynamic UI */
-@keyframes slideIn { from { transform: translateY(100%); } to { transform: translateY(0); } }
+@keyframes slideIn {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
```
Use `@starting-style` for entry without JS:
```css
.toast {
- opacity: 1; transform: translateY(0);
- transition: opacity 400ms ease, transform 400ms ease;
- @starting-style { opacity: 0; transform: translateY(100%); }
+ opacity: 1;
+ transform: translateY(0);
+ transition:
+ opacity 400ms ease,
+ transform 400ms ease;
+ @starting-style {
+ opacity: 0;
+ transform: translateY(100%);
+ }
}
```
@@ -104,8 +124,12 @@ Legacy fallback: `useEffect(() => setMounted(true), [])` + `data-mounted` attrib
Slow where the user is deciding, fast where the system responds.
```css
-.overlay { transition: clip-path 200ms ease-out; } /* release: fast */
-.button:active .overlay { transition: clip-path 2s linear; } /* press: slow, deliberate */
+.overlay {
+ transition: clip-path 200ms ease-out;
+} /* release: fast */
+.button:active .overlay {
+ transition: clip-path 2s linear;
+} /* press: slow, deliberate */
```
## Performance
@@ -113,8 +137,8 @@ Slow where the user is deciding, fast where the system responds.
- **Only animate `transform` and `opacity`** — they skip layout/paint and run on the GPU. `padding`/`margin`/`height`/`width`/`top`/`left` trigger all three rendering steps.
- **Don't drive child transforms via a CSS variable on the parent** — it recalcs styles for all children. Set `transform` directly on the element.
```js
- element.style.setProperty('--swipe-amount', `${d}px`); // bad: recalc on all children
- element.style.transform = `translateY(${d}px)`; // good: only this element
+ element.style.setProperty("--swipe-amount", `${d}px`); // bad: recalc on all children
+ element.style.transform = `translateY(${d}px)`; // good: only this element
```
- **Framer Motion shorthands are NOT hardware-accelerated.** `x`/`y`/`scale` run on the main thread via rAF and drop frames under load. Use the full transform string:
```jsx
@@ -124,8 +148,11 @@ Slow where the user is deciding, fast where the system responds.
- **CSS animations beat JS under load** — they run off the main thread; rAF-based animations stutter while the browser loads/scripts/paints. Use CSS for predetermined motion, JS for dynamic/interruptible.
- **WAAPI** gives JS control with CSS performance (hardware-accelerated, interruptible, no library):
```js
- element.animate([{ clipPath: 'inset(0 0 100% 0)' }, { clipPath: 'inset(0 0 0 0)' }],
- { duration: 1000, fill: 'forwards', easing: 'cubic-bezier(0.77, 0, 0.175, 1)' });
+ element.animate([{ clipPath: "inset(0 0 100% 0)" }, { clipPath: "inset(0 0 0 0)" }], {
+ duration: 1000,
+ fill: "forwards",
+ easing: "cubic-bezier(0.77, 0, 0.175, 1)",
+ });
```
## Transforms & clip-path
@@ -152,26 +179,43 @@ When a crossfade shows two overlapping states despite tuning easing/duration, ad
Stagger group entrances; 30–80ms between items. Longer delays feel slow. Stagger is decorative — never block interaction while it plays.
```css
-.item { opacity: 0; transform: translateY(8px); animation: fadeIn 300ms ease-out forwards; }
-.item:nth-child(2) { animation-delay: 50ms; }
-.item:nth-child(3) { animation-delay: 100ms; }
-@keyframes fadeIn { to { opacity: 1; transform: translateY(0); } }
+.item {
+ opacity: 0;
+ transform: translateY(8px);
+ animation: fadeIn 300ms ease-out forwards;
+}
+.item:nth-child(2) {
+ animation-delay: 50ms;
+}
+.item:nth-child(3) {
+ animation-delay: 100ms;
+}
+@keyframes fadeIn {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
```
## Accessibility
```css
@media (prefers-reduced-motion: reduce) {
- .element { animation: fade 0.2s ease; } /* keep opacity/color, drop transform-based motion */
+ .element {
+ animation: fade 0.2s ease;
+ } /* keep opacity/color, drop transform-based motion */
}
@media (hover: hover) and (pointer: fine) {
- .element:hover { transform: scale(1.05); } /* gate hover motion — touch fires false hovers on tap */
+ .element:hover {
+ transform: scale(1.05);
+ } /* gate hover motion — touch fires false hovers on tap */
}
```
```jsx
const reduce = useReducedMotion();
-const closedX = reduce ? 0 : '-100%';
+const closedX = reduce ? 0 : "-100%";
```
Reduced motion means fewer and gentler animations, not zero — keep transitions that aid comprehension, remove movement/position changes.
diff --git a/frontend/src/landing/components/LandingAgentsBar.tsx b/frontend/src/landing/components/LandingAgentsBar.tsx
index cbf88ed77..33c796991 100644
--- a/frontend/src/landing/components/LandingAgentsBar.tsx
+++ b/frontend/src/landing/components/LandingAgentsBar.tsx
@@ -58,12 +58,7 @@ export function LandingAgentsBar() {
className="group flex h-[82px] w-[112px] shrink-0 flex-col items-center justify-end gap-2 px-2 py-2"
>
-

+
{agent.name}
diff --git a/frontend/src/landing/components/LandingFeatures.tsx b/frontend/src/landing/components/LandingFeatures.tsx
index 946539d8f..47eb0d54c 100644
--- a/frontend/src/landing/components/LandingFeatures.tsx
+++ b/frontend/src/landing/components/LandingFeatures.tsx
@@ -198,9 +198,7 @@ export function LandingFeatures() {
What's inside
Run the agent you already use.
-
- AO wraps the workflow around it.
-
+ AO wraps the workflow around it.
@@ -384,7 +382,6 @@ function AgentHarnessDemo({
-
@@ -423,9 +420,7 @@ function AgentSelectLabel({
function AgentLogo({ agent, className }: { agent: AgentHarness; className: string }) {
if (!agent.logo) {
return (
-