# Understand motion review

**Review date:** 2026-08-29  
**Scope:** Interaction and motion review only. No Flutter code or browser artifact was changed.  
**Decision:** The direction is right, but the current Flow motion is not yet a safe implementation contract. Resolve the high-priority findings below before porting the 78-state package.

## Sources and method

I read the design contract, the extraordinary UX brief, the attention model and brand direction, the machine-readable Flow parity rules, the current Flow source, and all 78 state records in the consolidated state package. The four consolidated package files enumerate the same 78 state IDs and state records. Their renderers differ, so this report treats the state records as the canonical inventory and `docs/design/mocks/brand-proof/flow.html` as the motion source.

Sources:

- `docs/DESIGN.md:53-66`, motion and interaction contract.
- `docs/design/extraordinary-ux-brief.md:67-118`, transition-as-introduction, first-play sequence, rewrite reveal, and reduced-motion law.
- `docs/design/brand/attention-model.md:52-66`, handoff vocabulary and focus ownership.
- `docs/design/brand/direction.md:45-58`, ferrofluid states and attention order.
- `docs/design/brand/flow-parity-rules.json`, current parity declarations. Its `source` is `docs/design/mocks/brand-proof/flow.html`.
- `docs/design/mocks/brand-proof/flow.html`, current CSS and transition engine. Relevant declarations include `.view`, `.reader`, `.dock`, `.blob`, `.sheet`, `.dialog`, and the reduced-motion media query. Relevant timers are `GATHER_MS`, `SYNTH_MS`, `EXIT_MS`, and `CHROME_AFTER_IGNITE_MS`.
- `.superpowers/brainstorm/81813-1787986487/content/consolidated-state-package-01.html:34-135`, the complete 78-state inventory. Packages 02-04 contain the same state records.
- `docs/design/brand/STATE-PACKAGE-REVIEW.md`, prior state-package findings and the owner feedback applied in preview 04.

The review uses two rules throughout. Motion must name the attention handoff it explains. Every transition must be cancelable without leaving the user in the wrong semantic state.

## Short verdict

The Flow has a coherent spine: matter first, heard sentence second, chrome last. Its timing numbers are a useful starting point. The problems are in the joins:

1. The first-play sequence is gated by fixed demo timers instead of the first real audio event.
2. Chrome changes `display` and therefore changes the Reader layout after the sentence appears.
3. The recast effect is currently overridden by its own selector, so Original and Retelling do not visibly morph.
4. Playback rewrites the entire Reader DOM on every animation frame, despite `aria-live="polite"`.
5. Hold, back, sheets, dialogs, downloads, and reduced motion are not all interruptible or state-complete.
6. Several controls have competing meanings, incomplete keyboard semantics, or labels that promise pause/cancel behavior the code does not provide.

These are interaction risks, not aesthetic preferences. They can make the listener miss the beginning of a sentence, lose their place, hear one voice while seeing another, or believe an action did something when it did not.

## Current Flow baseline

The current Flow declares these useful budgets:

| Handoff | Current Flow value | Review position |
| --- | --- | --- |
| View enter/exit | `.view` uses `opacity` and `transform` for 450ms | Keep the 420-450ms scene budget. Replace hard-coded route timers with transition completion. |
| Gather | `GATHER_MS = 520ms` | Keep as the one inhale beat. Run it in parallel with route preparation rather than using it as a dead wait. |
| Synthesis demo | `SYNTH_MS = 900ms` | Treat as a demo ceiling only. Production must wait for a real first-audio event, with a recoverable timeout. |
| Voice -> chrome | `CHROME_AFTER_IGNITE_MS = 380ms` | Keep after the first live sentence. Do not reveal chrome before the event. |
| Matter morph | `.blob` transition is 450ms; play uses a 1.1s infinite keyframe | Keep a restrained 450ms handoff. Replace the autonomous keyframe loop with audio-driven matter. |
| Sheet entrance | `.sheet` translates from below for 350ms | Keep the contract for entry. Use a shorter 180-220ms exit. |
| Dialog entrance | `.dialog` opacity/scale transition is 250ms | Keep 250ms entry. Use a 160-180ms exit and wait for it before hiding. |
| Progress | voice progress uses `width .4s ease` | Keep determinate progress, but animate `transform: scaleX()` and expose the numeric value. |

The demo currently takes about 1,840ms from tap to first visible text on a cold open: 520ms gather, 420ms library exit, then 900ms synthesis. Chrome starts about 380ms later, around 2,220ms after the tap. That sequence can work in a storyboard, but a fixed 900ms cannot prove that the user heard the first word before the text lit up.

The Flow uses the weak built-in `ease` for nearly every transition. New motion should use the custom curves from the design-engineering guidance:

```css
--ease-out: cubic-bezier(.23, 1, .32, 1);
--ease-in-out: cubic-bezier(.77, 0, .175, 1);
--ease-drawer: cubic-bezier(.32, .72, 0, 1);
```

Do not use `ease-in` for UI motion. Entering elements should move immediately. On-screen movement can use `ease-in-out`. A modal remains centered, so `transform-origin: center` is correct. An anchored menu or popover should use its trigger as the origin.

## Findings

The table uses the required Before/After format. State IDs are exact IDs from the consolidated package.

| ID / severity / states | Before | After | Why |
| --- | --- | --- | --- |
| **M-01 / high**  `library-continue`, `library-expanded`, `library-after-paste` -> `reader-idle-original`, `reader-cold-gather`, `reader-synthesizing`, `reader-live` | `openListen()` starts a 520ms gather, waits for a 420ms route exit, then starts a fixed 900ms synthesis timer. Text appears when the timer ends. No first-audio event exists in the Flow model. | Start focus and matter on tap. Keep the 520ms gather beat, but let `firstAudio` move `reader-synthesizing` to `reader-live`. Gate text on that event, then reveal chrome 380ms later. Use a visible, recoverable timeout if no audio arrives. | The brief says sound leads and text joins only after the heard sentence earns attention. A timer cannot enforce that order across a slow or fast device. |
| **M-02 / high**  `library-empty`, `library-continue`, `library-expanded`, `library-after-paste` | Only `.row` peers can receive `is-dim`; the Continue hero is not dimmed. The library remains visible for the full gather delay, and the tapped card has no pressed treatment. | On pointer or keyboard activation, scale the chosen target to `.97` for 100-160ms, dim rivals immediately, and begin the scene handoff within 100-150ms. Let gather overlap the route animation. Carry the selected title into the Reader with a small shared-element or title crossfade. | The first action is an attention narrowing. Waiting before the field changes makes the tap feel ignored and violates the brief's "tap -> dim" beat. |
| **M-03 / high**  `reader-live`, `reader-paused`, `reader-seeking`, `reader-large`, `reader-retelling-ready` | `.segment`, `.voice-row`, and `.speed-row` switch from `display:none` to visible when `.chrome-on` is set. Their height is added after text appears. The Reader is centered, so the live sentence can jump when chrome settles. The `.reader .s` transition list does not include `transform`, so the pre-voice `translateY(6px)` jumps instead of easing. | Reserve the dock's measured slots from the start. Reveal each slot with opacity and a short translate, or use a stable dock layout whose height does not change. Animate only transform and opacity. Keep the 380ms chrome settle as a reveal, not a reflow. | The contract says the reading surface stays primary and the dock is a measured sibling. A layout jump moves the sentence just as the listener is trying to follow it. |
| **M-04 / high**  `reader-live`, `reader-retelling-loading`, `reader-retelling-ready`, `reader-retelling-disclosure`, `reader-quota`, `reader-retry` | `setMode()` adds `is-recast` while the Reader already has `is-text`. The selector `.reader.is-recast.is-text .s` immediately overrides `.reader.is-recast .s`, so the intended blur and dim never appear. The DOM is then replaced after 280ms. | Make recast classes mutually exclusive, or keep one semantic text tree and crossfade its visual content. Use a 220-280ms re-cast, retain the current fraction, and show the original fallback while the new wording is being prepared. Restart audio only after the new segment is ready. | Original -> Retelling is the product's signature transition. A no-op transition feels like a settings toggle and can expose text and audio from different modes. |
| **M-05 / high**  `reader-cold-gather`, `reader-synthesizing`, `reader-live`, `reader-paused`, `reader-seeking`, `reader-completed` | `tick()` calls `renderReader()` every animation frame. `renderReader()` replaces `innerHTML` for every sentence on every frame, while the container is `aria-live="polite"`. | Keep one DOM or Flutter semantics tree. Update the live/past/upcoming class only when the sentence index changes. Announce one concise state change, such as "Now listening" plus the sentence, and never announce a frame tick. | Rebuilding the live region can repeat speech-reader announcements, drop focus, and waste the main thread needed for smooth playback. |
| **M-06 / high**  `reader-cold-gather`, `reader-synthesizing`, `reader-live`, `reader-paused`, `reader-completed`, `reader-error` | Play uses an infinite `@keyframes spike`. Switching to Hold disables the animation and assigns a new border radius, which snaps to a new pose instead of freezing the current pose. End maps straight to idle with no release beat. | Matter should be driven by audio amplitude and persona texture. Gather should be one inhale and one elastic swell. Capture the current pose on Hold and freeze it. Resume from that pose. End with 240-320ms of decay, then settle to a quiet meniscus. | The brief defines matter as voice made visible. A looped pulse reads as a spinner, and a snapped Hold state says the voice disappeared instead of waiting. |
| **M-07 / high**  All timed paths, especially `reader-*`, `retelling-*`, `voice-*`, `library-*`, `contents-*`, `settings-*` | `setMode()` has an uncancelled 280ms timeout. `backLibrary()` hides the view after 180ms without storing or joining its route timer. Sheet and download timers can also outlive the action that created them. A rapid toggle can let an old callback overwrite a newer state. | Give each transition a generation token or cancellable controller. A new intent cancels pending route, recast, gather, preview, and progress work. Use transition completion rather than guessed delays, and make every callback check the current state before committing. | Motion must be interruptible. Without cancellation, a fast listener can land in the wrong mode, on the wrong screen, or with a stale progress result. |
| **M-08 / high**  `intake-sheet`, `retelling-clean`, `retelling-dirty`, `retelling-cloud-gate`, `retelling-loading`, `contents-available`, `contents-current`, `contents-unavailable`, `settings-default`, `settings-large`, `settings-reduced-motion`, `settings-account-disabled`, `paste-replace`, `edit-stub`, `report-confirm`, `clear-current-confirm`, `history-delete-confirm`, `model-delete-confirm` | `closeSheets()` hides a sheet after 300ms even though its transform transition lasts 350ms. `openDialog()` starts a dialog while the previous sheet may still be visible, sharing one backdrop. Dialog close hides immediately, so there is no exit. Focus is not trapped or restored. | Use one modal coordinator. Finish or cancel the old surface before opening the next one. Use a 350ms drawer entry with a 180-220ms exit, a 250ms dialog entry with a 160-180ms exit, and wait for `transitionend` before `hidden`. Move focus to the heading or first action, trap it, close only non-destructive surfaces on backdrop or Escape, and restore focus to the trigger. | One backdrop is the right visual rule, but overlapping semantic trees are not. Destructive actions need a stable Cancel + action pair and no accidental dismissal. |
| **M-09 / high**  `settings-reduced-motion`, `reader-cold-gather`, `reader-synthesizing`, `reader-live`, `reader-paused`, `reader-retelling-ready`, all sheets and dialogs | The media query sets every listed transition and animation to `none`. This removes the blob loop but also removes the calm crossfade. The Flow has no persisted reduced-motion control matching `settings-reduced-motion`. | Combine the OS preference with the app setting. Preserve the order matter -> text -> chrome. Remove transform, scale, blur, travel, and continuous blob motion. Keep only 160-200ms opacity/color crossfades. Update one semantics tree after the visual handoff, never two competing text trees. | Reduced motion means less movement, not lost meaning. The current rule is safe from motion sickness but loses the handoff that tells the listener what changed. |
| **M-10 / high**  `model-downloading`, `model-verifying`, `voice-downloading`, `voice-verifying`, `model-delete-progress`, `reader-retelling-loading`, `reader-retry`, `retelling-loading` | The package names determinate progress, but the Flow only demonstrates voice progress with `width .4s ease`. The Voice action changes to `Pause`, yet `startDownload()` clears and restarts the interval instead of pausing. Verification and cancellation are not consistently represented. | Keep a determinate bar with `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`. Animate a fill with `transform: scaleX()` over 200-300ms. Label the work and the next action. Implement real Pause/Resume or call the action Cancel. Verification must be a distinct non-fake phase, and cancellation must preserve the text and recover to the matching cancelled state. | Progress is trust. A bar that moves smoothly but cannot be paused, canceled, or verified teaches the listener that the status is decorative. |
| **M-11 / high**  `library-expanded`, `voice-ready`, `voice-preview`, `voice-unavailable`, `voice-downloading`, `voice-verifying`, `voice-error`, `overflow-open`, all Reader controls | The Flow has no `:active` feedback. Several visible controls are 32-40px, below the 48dp contract. Library rows are `div role="button"` elements containing a nested button. Voice items are `div role="button"` elements containing Preview/Get buttons, but no keyboard handler selects them. | Use a 48dp hit target with a smaller visual glyph where needed. Add pointer press feedback with `scale(.97)` for 100-160ms. Make row selection and row actions separate siblings, not nested interactive roles. Make each voice choice a real button or list item with explicit Select, Preview, Get, Pause, and Cancel semantics. Add keyboard activation and `aria-pressed` or `aria-current` where selection changes. | A listener should know what a tap did before the scene moves. Nested roles and small hit areas make selection ambiguous and make the same state behave differently for touch and keyboard. |
| **M-12 / high**  `reader-error`, `reader-completed`, `model-error`, `voice-error`, `report-failed`, `clear-current-failure`, `history-delete-failure`, `model-delete-failure`, `recovery-model`, `recovery-cloud`, `recovery-data` | A 900ms long press on the main play button changes matter to `error`, but no error copy or recovery action appears. At completion, `setMatter('end')` maps to idle and leaves the Reader in place; the canonical completed state and its actions never render. | Enter the explicit error or completed state. Use a quiet 240-320ms release for completion, then show the direct recovery action. Do not hide an error behind a gesture. If a long press remains a test hook, keep it out of the customer path. | Errors and completion change what the listener can do next. Matter color alone cannot tell a user whether playback failed, ended, or is waiting. |
| **M-13 / medium**  `library-*` -> `reader-*`, `reader-live` -> `library-*`, `overflow-open`, `library-expanded`, `local-disclosure`, `reader-retelling-disclosure` | `backLibrary()` starts a 450ms view exit but hides the Listen view after 180ms. `reopenListenKeepChrome()` adds `is-enter` and `is-show` in the same task, so the intended enter transition can be skipped. Overflow and disclosure use `display:none`/`display:block` with no enter or exit motion. Row actions also appear by switching `display` with no transition, while the chevron has no transform transition. | Join route commits to the actual transition end, or use one shared scene controller. Use a 150-200ms origin-aware menu reveal, a 120-160ms row-action reveal, and a 160-200ms disclosure fade/translate. Keep interactions available during the reveal. | The current values disagree with each other. A route can be cut short while an expansion or disclosure pops into place, which makes the same product feel inconsistent. |
| **M-14 / medium**  `voice-ready`, `voice-preview`, `voice-unavailable`, `voice-downloading`, `voice-verifying`, `voice-error`, `reader-live`, `reader-paused` | Selecting a downloaded voice changes its name and matter persona immediately, with no audible handoff or status. Preview auto-stops after 900ms, but the rendered action stays `Preview` and has no explicit Stop state. Voice texture is also changed by voice ID, which can look like a persona change. | Selecting a voice should show a short casting acknowledgement, preserve the current passage, and rebuffer or crossfade the audio before claiming the new performer is active. Preview should have explicit Previewing/Stop preview states and not silently steal the current play primary. Keep persona texture tied to persona, with voice changes affecting voice response rather than identity. | Voice is a performer change. Silent mutation makes the label, audio, and matter disagree, and the current preview action does not tell the listener whether it is still active. |

| **M-15 / medium**  all 78 state IDs | The state package lets a reviewer select a static state card, but its primary actions do not advance through the next state. The package therefore proves coverage, not behavior. | Implement the transition checklist below in the canonical Flow or an equivalent replayable fixture. Every primary, secondary, Cancel, Close, Retry, Apply, and platform handoff gets a named target state and a preserved source context. | A complete state inventory is only useful if the listener can reach and recover from every state. Static cards hide wiring gaps until Flutter. |\n\n## Timing and implementation recommendations

Use these as the next motion token pass. They preserve the pinned Flow values where they express product meaning and shorten only routine exits and feedback.

| Interaction | Recommended budget | Motion and state rule |
| --- | --- | --- |
| Press feedback | 100-160ms | `transform: scale(.97)` on pointer press. Keep focus visible. Do not animate repeated keyboard actions. |
| Library focus handoff | 120-180ms | Dim rivals with opacity only. Keep the chosen item at full energy. Do not blur a list on every tap. |
| Scene enter/exit | 420-450ms total | Use opacity plus a small translate. The route controller, not an arbitrary timeout, commits the new scene. A title may travel or crossfade to preserve continuity. |
| Gather | 520ms | Matter inhales, retracts, then gives one restrained swell. It can overlap scene preparation. It must be cancelable. |
| Synthesis | Event-driven | Keep `900ms` as a demo reference, not a minimum. `firstAudio` starts the ignite handoff. A timeout moves to an explicit retry or recovery state, never to text that claims it was heard. |
| Ignite text | 160-240ms | Reveal the one live sentence after first audio. Past and upcoming lines remain in their wake and quiet tiers. |
| Chrome settle | 380ms after ignite | Reveal reserved slots with opacity and translate. Never add layout height after the live sentence appears. |
| Matter morph | 350-450ms | Use a subtle, interruptible pose transition. During play, audio drives the pose. Hold freezes the current pose. |
| Recast | 220-280ms | Blur or dim the old text briefly, swap one semantic tree, then settle the new text. Keep the current fraction and expose an original fallback while rewriting. |
| Sheet enter/exit | 350ms in, 180-220ms out | Use the drawer curve on transform. Backdrop fades in 180-220ms and fades out with the exit. One backdrop only. |
| Dialog enter/exit | 250ms in, 160-180ms out | Centered scale from `.96` to `1` is appropriate. Do not scale from zero. Keep Cancel non-destructive. |
| Menu/popover | 150-200ms | Reveal from the trigger origin with opacity plus a small translate or scale from `.97`. Support Escape and focus return. |
| Row expansion | 120-160ms | Fade and translate action controls. Avoid a width/height animation that reflows the reading list. Do not block a second tap. |
| Disclosure/banner | 160-200ms | Fade and translate a single message. Reserve or measure its space so the live sentence does not jump. |
| Progress update | 200-300ms | Animate `scaleX` on a determinate fill. Do not fake a final 100%. Keep the numeric label synchronized. |
| Completion release | 240-320ms | Let matter decay to rest, then show `reader-completed` actions. No celebration, confetti, or bounce. |
| Success/error feedback | 160-240ms | Use an explicit status state and a direct action. Error must not use a decorative alert animation. |
| Library population | 30-60ms stagger | Stagger only when content first appears. Never delay interaction while the stagger runs. |

Performance rules:

- Animate transform and opacity wherever possible. Progress is the exception, and `scaleX` is preferable to width.
- Keep blur out of per-frame Reader updates. The current `.row.is-dim` blur is unnecessary. A 1-2px recast blur is acceptable only for the short handoff.
- Keep the matter glow to one restrained layer. The existing 16px play glow is a reasonable ceiling; do not add multiple large shadows to every frame.
- Use CSS transitions for predetermined UI handoffs and a shader or an interruptible programmatic animation for dynamic matter. Do not port an autonomous CSS loop as if it were audio response.
- Never block input during stagger, scene, or sheet motion. Retarget from the current pose instead of restarting from the first frame.

## Reduced-motion contract

The app setting in `settings-reduced-motion` and the OS preference must produce the same semantic ordering:

1. Matter enters or becomes present.
2. The first live sentence appears only after first audio.
3. Chrome settles after the live sentence.
4. The listener can still pause, seek, recast, or leave.

In reduced motion:

- Remove matter morph, looping pulse, scale, translate, drawer travel, menu travel, and blur.
- Keep 160-200ms opacity and color changes where they explain an update.
- Freeze matter at a calm playing frame rather than replacing it with a spinner or a dead error glyph.
- Keep one Reader text tree. Do not retain original and retelling as two simultaneous semantic trees during a crossfade.
- Keep progress numeric and determinate. It may update without a visual tween.
- Test a live preference change while a sheet, dialog, recast, and first-play sequence are in flight. The current transition must settle into the reduced state, not restart.

## State-ID coverage

This is the complete 78-state ledger used by the review. The compact grouping keeps the document readable while preserving every exact ID.

| Group | Count | State IDs | Motion responsibility |
| --- | ---: | --- | --- |
| Library | 4 | `library-empty`, `library-continue`, `library-expanded`, `library-after-paste` | Empty-to-content reveal, focus handoff, row expansion, and now-playing continuity. |
| Add text | 6 | `reader-empty`, `intake-sheet`, `paste-replace`, `paste-success`, `share-return`, `imports-unavailable` | Intake sheet, destructive replace dialog, success return, and honest unsupported fallback. |
| First run | 9 | `welcome`, `how-it-works`, `model-idle`, `model-downloading`, `model-verifying`, `model-ready`, `model-error`, `model-cancelled`, `local-disclosure` | Page handoff, determinate setup progress, cancel/retry, and first-use disclosure. |
| Reader | 14 | `reader-idle-original`, `reader-cold-gather`, `reader-synthesizing`, `reader-live`, `reader-paused`, `reader-seeking`, `reader-completed`, `reader-error`, `reader-large`, `reader-retelling-loading`, `reader-retelling-ready`, `reader-retelling-disclosure`, `reader-quota`, `reader-retry` | First play, live sentence, hold, seek, completion, error, large text, recast, quota, disclosure, and retry. |
| Voice | 6 | `voice-ready`, `voice-preview`, `voice-unavailable`, `voice-downloading`, `voice-verifying`, `voice-error` | Casting, preview, download, verify, cancel, retry, and performer continuity. |
| Retelling | 4 | `retelling-clean`, `retelling-dirty`, `retelling-cloud-gate`, `retelling-loading` | Sheet entry, dirty draft warning, explicit cloud choice, and loading fallback. |
| Contents | 3 | `contents-available`, `contents-current`, `contents-unavailable` | Sheet entry, current chapter indication, jump/close, and honest unavailable state. |
| More actions | 8 | `overflow-open`, `edit-stub`, `report-confirm`, `report-sent`, `report-failed`, `clear-current-confirm`, `clear-current-success`, `clear-current-failure` | Menu origin, dialogs, report feedback, destructive confirmation, and recovery. |
| Settings | 4 | `settings-default`, `settings-large`, `settings-reduced-motion`, `settings-account-disabled` | Sheet entry, text-size reflow, reduced-motion preference, and disabled stub. |
| Premium | 6 | `premium-choice`, `premium-checkout`, `premium-cancelled`, `premium-success`, `premium-error`, `premium-local-fallback` | Explicit cloud choice, checkout result, error, cancel, and local continuity. |
| Data and deletion | 8 | `history-delete-confirm`, `history-delete-success`, `history-delete-failure`, `model-delete-confirm`, `model-delete-progress`, `model-delete-success`, `model-delete-failure`, `data-delete-success` | Destructive confirmation, determinate deletion, success, failure, and fresh-start transition. |
| Platform contracts | 3 | `share-handoff`, `notification-contract`, `permission-denied` | Native handoff, lock-screen contract, permission fallback. Do not animate fake native surfaces. |
| Recovery | 3 | `recovery-model`, `recovery-cloud`, `recovery-data` | Quiet recovery, retry, local fallback, and preserved text. |
| **Total** | **78** |  |  |

## Transition checklist

Each row is a named handoff, not an anonymous fade. For every row, capture the start, an intermediate frame, and the settled frame in normal and reduced motion.

| Done | Transition and state IDs | Handoff and timing | Interruptibility and acceptance |
| --- | --- | --- | --- |
| [ ] | `library-empty` -> `intake-sheet` -> `paste-success` -> `reader-idle-original`; `reader-empty` -> `intake-sheet`; `intake-sheet` -> `paste-replace` -> `paste-success`; `intake-sheet` -> `share-handoff` -> `share-return`; `intake-sheet` -> `imports-unavailable` | Intake focus. Sheet in 350ms, dialog in 250ms. Success returns in 180-240ms. Unsupported formats stay in-app with no fake import motion. | Backdrop is singular. Cancel returns to the exact source state. Paste success says the text is saved and exposes one next action. Share is a native handoff, not a simulated OS sheet. |
| [ ] | `welcome` -> `how-it-works` -> `model-idle` -> `model-downloading` -> `model-verifying` -> `model-ready` -> `local-disclosure` | Quiet first-run progression. Scene changes use the 420-450ms budget. Progress updates use `scaleX` over 200-300ms. | Cancel at every allowed point lands in `model-cancelled` or the documented browse fallback. Error never claims readiness. The disclosure is dismissible and does not stack with another first-run banner. |
| [ ] | `library-continue`, `library-expanded`, `library-after-paste` -> `reader-idle-original` -> `reader-cold-gather` -> `reader-synthesizing` -> `reader-live` | Dim -> Gather -> Ignite -> Settle. Gather is 520ms. First audio, not `SYNTH_MS`, reveals the live sentence. Chrome waits 380ms after ignite. | Press, Back, and a second Play intent cancel or pause each pending phase. The selected title remains continuous. No text appears before first audio. |
| [ ] | `reader-live` <-> `reader-paused`; `reader-live` -> `reader-seeking` -> `reader-live` or `reader-paused`; `reader-live` -> `reader-completed`; `reader-live` -> `reader-error` | Hold freezes matter in its current pose. Seek gives immediate numeric and sentence feedback. Completion releases over 240-320ms. Error uses a direct recovery state. | Seek preserves whether playback was active before the gesture. Resume does not restart a morph from zero. Completion exposes Listen again and Back to Library. Error preserves text and position. |
| [ ] | `reader-idle-original` <-> `reader-retelling-loading` -> `reader-retelling-ready`; `reader-live` -> `reader-retelling-loading` -> `reader-retelling-ready`; `reader-retelling-ready` -> `reader-retelling-disclosure`; `reader-quota` -> local or cloud; `reader-retry` -> loading or original | Re-cast in 220-280ms. Keep the same passage fraction. Show original while rewriting. The disclosure is one 160-200ms message. | Rapid Original/Retelling taps settle to the last intent. Audio and visible wording switch together. Local fallback remains available without account, payment, or network claims. |
| [ ] | `voice-ready` -> `voice-preview`; `voice-unavailable` -> `voice-downloading` -> `voice-verifying` -> `voice-ready`; download -> `voice-error`; retry -> downloading | Preview starts with 100-160ms press feedback and has an explicit Stop state. Download progress is determinate. Verify is a distinct phase. | Preview can stop immediately. Get becomes Pause only if Pause really pauses. Cancel is explicit and restores the current voice. Selecting a voice does not silently change persona identity. |
| [ ] | `retelling-clean` -> `retelling-dirty` -> Apply -> `reader-retelling-loading` or `reader-retelling-ready`; `retelling-cloud-gate` -> `premium-choice`; `retelling-loading` -> original or ready | Sheet entry 350ms. Apply closes in 180-220ms, then recast begins only if needed. Dirty and cloud messages use one quiet banner. | Close does not apply dirty changes. Apply has one clear result. Cloud choice never traps the local path. Loading can be canceled or replaced by Use original. |
| [ ] | `overflow-open` -> `contents-available` or `contents-current` or `contents-unavailable`; `overflow-open` -> `edit-stub`; `overflow-open` -> `report-confirm` -> `report-sent` or `report-failed`; `overflow-open` -> `clear-current-confirm` -> `clear-current-success` or `clear-current-failure` | Menu reveals from the overflow trigger in 150-200ms. Sheets and dialogs use their budgets above. Report and clear use status feedback, not celebration. | Escape and outside tap close the menu. Destructive dialogs require Cancel plus the explicit action. A failed report or clear leaves playback and text unchanged. |
| [ ] | `settings-default` -> `settings-large` -> `reader-large`; `settings-default` -> `settings-reduced-motion`; `settings-default` -> `settings-account-disabled` | Text scale changes in 180-240ms only when motion is allowed, with measured Reader and dock siblings. Reduced motion may change immediately or crossfade in 160ms. | Preserve the live sentence and scroll position. Large text never overlays the dock. The reduced-motion choice persists and applies to all active transitions. Account remains a labeled disabled stub. |
| [ ] | `premium-choice` -> `premium-checkout` -> `premium-success`; checkout -> `premium-cancelled` or `premium-error`; all cloud paths -> `premium-local-fallback` | Cloud is a deliberate recast of processing, not an animated paywall trap. Use 180-240ms status entry and no purchase celebration. | Cancel and error return to the same local text and position. No cloud state removes local listening or local rewrite. |
| [ ] | `history-delete-confirm` -> `history-delete-success` or `history-delete-failure`; `model-delete-confirm` -> `model-delete-progress` -> `model-delete-success` or `model-delete-failure`; `data-delete-success` -> `welcome` or the documented fresh start | Dialog 250ms in, 160-180ms out. Deletion progress is determinate and quiet. Fresh start is a scene handoff, not a reset flash. | Confirmations state what remains safe. Cancel makes no change. Failure is retryable. A model deletion never deletes text or saved sessions. |
| [ ] | `share-handoff` -> native share surface -> `share-return`; `notification-contract` while `reader-live`/`reader-paused`; `permission-denied` -> local playback or system settings | OS owns share, notification, lock-screen, and permission motion. The app only introduces the handoff copy and returns cleanly. | Do not draw a fake lock screen or share sheet. Returning from a native surface preserves playback, position, voice, and mode. Permission denial leaves local listening available. |
| [ ] | `recovery-model`, `recovery-cloud`, `recovery-data` -> their retry, installed-voice, local, or close actions | Recovery enters in 160-240ms. Retry uses press feedback only. No alert bounce or red flash. | Each recovery state names what survived and the next action. Retry cannot duplicate a request. Close returns to the exact prior surface. |

## Final verification ladder

Before calling this motion contract ready:

- [ ] Instrument the cold open with timestamps for tap, dim, gather start/end, first audio, first live sentence, and chrome settle. Prove `firstAudio < firstLive < chrome` on a slow and fast device.
- [ ] Interrupt every long phase at 0ms, 100ms, 300ms, and 75%: Back, Play, Original/Retelling, open/close sheet, Escape, and app background/foreground. The final state must match the last user intent.
- [ ] Verify Hold freezes the exact matter pose and Resume continues from it. Verify End has a release frame before `reader-completed`.
- [ ] Verify the Reader updates one sentence class at a time, emits one screen-reader announcement per state change, and never replaces its live region on every frame.
- [ ] Verify every sheet and dialog has one backdrop, a focus target, focus restoration, a non-destructive Cancel or Close, and a transition end before hiding.
- [ ] Verify every progress state has a truthful value, a next action, a real cancel or pause contract, and a retry path where the state package calls for one.
- [ ] Verify 48dp touch targets and pressed feedback for all controls. Test keyboard Enter/Space and Escape for rows, voice items, menus, sheets, and dialogs.
- [ ] Verify `settings-reduced-motion` and OS reduced motion during each in-flight transition. Confirm no movement, looping matter, blur, or duplicate semantics tree remains.
- [ ] Capture 412x915, wide, 2x text, screen-reader, and reduced-motion variants for all motion states. Keep the dock measured beside the Reader body.
- [ ] Re-run the 78-state action matrix. Every primary action must land in a named state, preserve the text and position where promised, and leave one recoverable next action.

The current Flow is a good storyboard for the intended order. It is not yet evidence that the order survives real audio timing, interruption, accessibility, or the full state inventory. The next pass should make those behaviors explicit before any Flutter motion is ported.
