chore: format with prettier [skip ci]

This commit is contained in:
github-actions[bot] 2026-06-30 12:44:18 +00:00
parent 4c8f9db279
commit 90c2543f5e
9 changed files with 222 additions and 193 deletions

View File

@ -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);
}, []);
// <div data-mounted={mounted}>
```
@ -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) |

View File

@ -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)

View File

@ -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 | 100160ms |
| Tooltips, small popovers | 125200ms |
| Dropdowns, selects | 150250ms |
| Modals, drawers | 200500ms |
| Marketing / explanatory | Can be longer |
| Element | Duration |
| ------------------------ | ------------- |
| Button press feedback | 100160ms |
| Tooltips, small popovers | 125200ms |
| Dropdowns, selects | 150250ms |
| Modals, drawers | 200500ms |
| 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.90.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.950.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; 3080ms 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.

View File

@ -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"
>
<div className="agent-logo-tile">
<img
src={agent.src}
alt=""
referrerPolicy="no-referrer"
className="agent-logo-image"
/>
<img src={agent.src} alt="" referrerPolicy="no-referrer" className="agent-logo-image" />
</div>
<div className="max-w-full truncate font-mono text-[12px] leading-none tracking-[0.04em] text-[color:var(--fg-dim)]">
{agent.name}

View File

@ -198,9 +198,7 @@ export function LandingFeatures() {
<div className="landing-eyebrow mb-4">What&apos;s inside</div>
<h2 className="landing-heading">
Run the agent you already use.
<span className="landing-heading-muted block">
AO wraps the workflow around it.
</span>
<span className="landing-heading-muted block">AO wraps the workflow around it.</span>
</h2>
</div>
<div className="lg:col-span-5">
@ -384,7 +382,6 @@ function AgentHarnessDemo({
<TerminalLine success text="activity hooks installed, session visible" />
</div>
</div>
</div>
</div>
</article>
@ -423,9 +420,7 @@ function AgentSelectLabel({
function AgentLogo({ agent, className }: { agent: AgentHarness; className: string }) {
if (!agent.logo) {
return (
<div
className={`${className} agent-logo-frame text-xs font-bold text-[color:var(--fg-muted)]`}
>
<div className={`${className} agent-logo-frame text-xs font-bold text-[color:var(--fg-muted)]`}>
{agent.name.slice(0, 1)}
</div>
);
@ -604,7 +599,6 @@ function WorkspaceIsolationDemo({
</div>
</div>
</div>
</div>
</div>
</div>
@ -773,7 +767,6 @@ function FeedbackRoutingDemo({
</div>
</div>
</div>
</div>
</div>
</article>
@ -817,7 +810,6 @@ function DaemonControlDemo() {
))}
</div>
</div>
</div>
<aside className="bg-[#050506] p-4">

View File

@ -556,10 +556,7 @@ export function LandingHero() {
/>
<div className="relative z-10 mx-auto w-full max-w-[1200px] px-5 sm:px-8 lg:px-12 xl:px-16">
<div className="mx-auto text-center">
<h1
data-testid="hero-headline"
className="landing-hero-heading mx-auto font-sans"
>
<h1 data-testid="hero-headline" className="landing-hero-heading mx-auto font-sans">
<span className="block">Stop babysitting coding agents.</span>
<span className="mt-2 block italic">
Start merging <span className="font-[620] text-[#93b4f8]">real work.</span>

View File

@ -198,10 +198,7 @@ export function LandingSocialProof() {
<div className="lg:col-span-7">
<div className="landing-eyebrow mb-4">In the wild</div>
<h2 className="landing-heading">
People are already{" "}
<span className="landing-heading-muted">
building around it.
</span>
People are already <span className="landing-heading-muted">building around it.</span>
</h2>
</div>
<div className="lg:col-span-5">

View File

@ -18,9 +18,7 @@ export function LandingVideo() {
<div className="container-page">
<div className="landing-section-header mx-auto max-w-[1180px] text-left">
<div className="landing-eyebrow mb-4">Demo</div>
<h2 className="landing-heading">
See it in action
</h2>
<h2 className="landing-heading">See it in action</h2>
</div>
<div className="relative mx-auto w-full max-w-[1180px]">

View File

@ -1,17 +1,17 @@
{
"version": 1,
"skills": {
"emil-design-eng": {
"source": "emilkowalski/skill",
"sourceType": "github",
"skillPath": "skills/emil-design-eng/SKILL.md",
"computedHash": "8bdf9e4e6de7a4969147bf4828a4ad2c5aacd9fba4b690b250a85e0467ca387d"
},
"review-animations": {
"source": "emilkowalski/skill",
"sourceType": "github",
"skillPath": "skills/review-animations/SKILL.md",
"computedHash": "ca04aaa0a815f05e0910ded8fedbfbdfe0acdec8f52dfbb88277890070d134d4"
}
}
"version": 1,
"skills": {
"emil-design-eng": {
"source": "emilkowalski/skill",
"sourceType": "github",
"skillPath": "skills/emil-design-eng/SKILL.md",
"computedHash": "8bdf9e4e6de7a4969147bf4828a4ad2c5aacd9fba4b690b250a85e0467ca387d"
},
"review-animations": {
"source": "emilkowalski/skill",
"sourceType": "github",
"skillPath": "skills/review-animations/SKILL.md",
"computedHash": "ca04aaa0a815f05e0910ded8fedbfbdfe0acdec8f52dfbb88277890070d134d4"
}
}
}