| Route change | opacity (View Transitions root cross-fade) | --dur-base, --ease-out | old view calc(--dur-base × 0.75), --ease-in | Top bar, rail, ladder dock, bottom dock and mobile header/tab bar get their own view-transition-name so they stay still. Previous data stays on screen while the next route loads; no blank frame |
| Theme switch (user choice) | opacity (View Transitions root cross-fade); CSS transitions off until it ends | --dur-base | none | Instant under reduced motion, without the API, and for changes the user did not make (settings load, OS theme). The class flips in the same task as the click; prices still update during the fade |
| Hover on generic controls | Overlay layer opacity 0→1 | --dur-fast, --ease-out | calc(--dur-fast × 0.75), --ease-in | Buttons, rows, tabs, menu items |
| Hover on trade targets | Solid fill layer | Instant | Instant | Price buttons, ladder cells, order chips. What you see at pointerdown is what you get |
| Press | transform: scale(0.98) | --dur-fast, --ease-out | --dur-fast, --ease-in | The action fires on pointerdown, before the scale finishes |
| Focus ring | Outline | Instant | Instant | Includes the panel focus indicator line |
| Toggles and segmented controls (Safe/Armed, stake, theme) | Active-segment layer opacity | --dur-fast, --ease-out | calc(--dur-fast × 0.75), --ease-in | The Safe/Armed switch changes only on server confirmation; the Armed top line fades in with it |
| Tooltip | opacity | --dur-fast, --ease-out | calc(--dur-fast × 0.75) | Open delay 300 ms for non-trade tooltips (a behaviour delay, not a transition). Disabled-reason tooltips on trade targets open with no delay |
| Cursor tag | transform: translate() per pointer move | None: appears on the first hover frame | Instant | Follows the pointer in a requestAnimationFrame loop, never eased |
| Dropdown, menu, popover, select | opacity + transform: scale(0.98)→1 from the trigger side | --dur-base, --ease-out | calc(--dur-base × 0.75), --ease-in | Contents render fully on the first frame |
| Command palette | Backdrop opacity; panel opacity + scale(0.98)→1 | --dur-base, --ease-out | calc(--dur-base × 0.75), --ease-in | The input is focused on the first frame and typing works immediately. Result rows never animate, including when the list changes |
| Dialog (hotkey sheet, settings confirm) | Backdrop opacity; panel opacity + scale(0.98)→1 | --dur-base, --ease-out | calc(--dur-base × 0.75) | |
| Bottom sheet (mobile ladder, market detail, action sheets) | transform: translateY(100%)→0; overlay opacity | --dur-slow, --ease-out | calc(--dur-slow × 0.75), --ease-in | vaul, configured with the token values (its default is 500 ms). Drag follows the finger 1:1; release dismisses when dragged > 25% of height or flung faster than 0.4 px/ms |
| Ladder dock slot (desktop) | Slot width: instant. Contents opacity 0→1 + translateX(8px)→0 | --dur-base, --ease-out | calc(--dur-base × 0.75) | A closed slot stays as an empty gap until the pointer leaves the dock, then collapses in one frame (no width animation) |
| Portfolio row expand | Expanded area height: instant. Its contents opacity + translateY(-4px)→0 | --dur-slow, --ease-out | calc(--dur-slow × 0.75), --ease-in, then height removed in one frame | PLAN lists “height and opacity”; height is not animated because rows stream data (open question 2) |
| Bottom dock show/hide and resize | Height: instant. Contents opacity | --dur-base | calc(--dur-base × 0.75) | Resize by dragging follows the pointer 1:1 |
| Tabs (dock tabs, portfolio tabs) | Indicator transform: translateX() + scaleX(); panel content opacity | --dur-base, --ease-out | Old panel removed instantly | The new tab’s data shows on the first frame |
| Toast | transform: translateY(8px)→0 + opacity | --dur-base, --ease-out | calc(--dur-base × 0.75), --ease-in | sonner, with its CSS transition durations overridden to the tokens. Stack collapse uses its transform. Swipe follows the finger |
| Order chip: appear, move, remove | Position and presence | Instant | Instant | PLAN: “order state on the ladder” is --dur-instant |
| Order chip: state colour | Previous-state layer opacity 1→0 over the new-state layer | --dur-fast, --ease-in | none | PLAN: chip colour cross-fade |
| Partial-fill bar | transform: scaleX() | Instant | Instant | It is a quantity, so it never tweens |
| Delay countdown ring | transform: rotate() on two half-discs | linear, duration = the real remaining delay | Removed instantly when the state changes | A timer, not a transition, so it is exempt from --dur-slow. Restarted from the server’s remaining time, never from zero |
| Depth bars | transform: scaleX() | Instant | Instant | Never tweened |
| Price flash | Flash layer opacity 1→0 | See below | | |
| Score change | --ring outline layer on the score badge, opacity 1→0 | --dur-flash, --ease-in | none | The score value itself changes instantly |
| Safe nudge (trade click or hotkey while Safe) | --ring outline layer on the Safe/Armed switch, opacity 1→0 | --dur-flash, --ease-in | none | One-off; restarts on each blocked click |
| Stale state | Stale styling | Instant | Instant | Stale must be visible on the first frame |
| ”N updates” pill (deferred list changes) | opacity | --dur-fast | calc(--dur-fast × 0.75) | Applying the updates is instant |
| Skeleton → content | Swap | Instant | | Skeletons are static |
| Loading spinner | transform: rotate() loop | Linear, only while a request is in flight | Removed instantly | The one allowed loop |
| In-page jumps (“jump to league”, “back to top”) | scroll-behavior: smooth | Browser | | Never inside ladders or books. Lists use native momentum scrolling |
| Ladder re-centre | scrollTop set directly | Instant | | Only under the conditions in price-ladder.md; never while hovered or touched |