> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sesameterminal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Motion

# Motion

Implements PLAN §4.1. Motion explains a change; it never delays one.

## Hard rules (from PLAN §4.1 and CLAUDE.md)

1. Only the motion tokens: `--dur-instant`, `--dur-fast`, `--dur-base`, `--dur-slow`,
   `--dur-flash`, `--ease-out`, `--ease-in`. No literal durations or easings in
   components.
2. Only `opacity` and `transform` are transitioned or animated. **Do not** use
   `transition-all` or `transition-colors` (shadcn ships these; FE-Platform replaces them
   with `transition-[opacity,transform]`). A colour that should fade is drawn on a layer
   whose opacity changes.
3. Prices, sizes, P\&L, balances, scores and counts are never tweened or counted up. They
   change on the frame the data arrives.
4. Ladders and books never scroll or re-centre on their own while the pointer or a
   finger is over them, and never with animation.
5. No layout animation (height, width, top, grid rows) on anything holding streaming
   data. Layout changes happen in one frame; the content inside may then fade.
6. Actions fire on input, not on animation end. An exiting element is already inert
   (`pointer-events: none`, `inert`) on its first exit frame.
7. Enter uses `--ease-out`. Exit uses `--ease-in` at 0.75× the enter duration:
   `calc(var(--dur-base) * 0.75)`.
8. Nothing is longer than `--dur-slow` (240 ms) except `--dur-flash` (400 ms, set by the
   PLAN table for the price flash and reused for the two other one-shot highlights: score
   change and Safe nudge) and the delay countdown ring (a timer that runs for the real
   delay, not a transition).
9. No looping decoration: no shimmer, no pulsing dots, no spinning except a 14 px loading
   spinner while a request is in flight.

## Reduced motion

`tokens.css` sets `--dur-fast`, `--dur-base`, `--dur-slow` and `--dur-flash` to `0ms`
under `prefers-reduced-motion: reduce`, so token-based CSS turns instant by itself.
Libraries and APIs that do not read the tokens need their own switch:

| Thing | Reduced-motion behaviour |
| - | - |
| View Transitions | Do not call `document.startViewTransition`; swap the route directly |
| vaul (bottom sheets) | Open and close without the slide (duration 0). Dragging still follows the finger (direct manipulation, not animation); release snaps instantly |
| sonner (toasts) | Appear and disappear in place, no slide; swipe to dismiss still works |
| Price flash | Replaced by a static `[arrow-up]` / `[arrow-down]` marker (below) |
| Delay countdown ring | Hidden; the seconds number stays and updates each second |
| Smooth scroll jumps | `scroll-behavior: auto` |
| Loading spinner | Replaced by a static `…` after the label |

## Per component

"Instant" = `--dur-instant` (no transition).

| Component | Property | Enter | Exit | Notes |
| - | - | - | - | - |
| 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](index.md#open-questions-for-the-tech-lead)) |
| 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](#price-flash) | | |
| 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 |

## Price flash

A short background flash when a displayed price changes, green for up and red for down.
Opacity only; layout never moves.

* **Which values flash:** best bid/ask prices on price buttons and price cells, last
  price, and P\&L cells. Sizes, depth and counts do not flash.
* **Where:** a `::after` layer (`inset: 0`, `pointer-events: none`, `--radius` of its cell),
  under the text, with background `--flash-up` or `--flash-down`.
* **Animation:** on a change, the layer jumps to opacity 1 and fades to 0 over
  `--dur-flash` with `--ease-in`. A new change during a flash restarts it (use the Web
  Animations API `el.animate()` or re-key the layer; do not queue flashes).
* **Tinted buttons:** the tint layer (`--bid-muted` / `--ask-muted`) drops to opacity 0 at
  the same moment and returns to 1 over `--dur-flash`, so flash and tint never stack
  (contrast is checked for that case in tokens.md).
* **When not to flash:** on first render, on a snapshot or resync, on the first value
  after stale recovers, while stale, and while the element is hovered as a trade target
  (the solid hover fill already covers it).
* **Reduced motion:** no flash. A static lucide `[arrow-up]` in `--bid` or `[arrow-down]`
  in `--ask` appears after the value, positioned absolutely outside the value's text box
  so the value stays centred and nothing shifts, and clears on the next change or after
  5 s.

`--dur-flash` (400 ms) is the one duration above `--dur-slow`. PLAN §4.1 sets 400 ms for
the flash explicitly, and CLAUDE.md caps durations at `--dur-slow`;
[open question 1](index.md#open-questions-for-the-tech-lead) asks to record this
exception in CLAUDE.md.

## Navigation speed (more important than motion)

Covered by FE-Platform; listed here so reviews check it with the motion:

* Routes preload on hover or focus (`preload: "intent"`).
* `placeholderData: keepPreviousData` on route queries: the old screen stays until the
  new data is ready; skeletons only on a cold load.
* Scroll position is restored per route; board, watchlist and portfolio keep their state
  across tab switches (mobile tabs stay mounted).
* Route chunks are prefetched when idle.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.