# Understand visual review

**Status:** Needs corrections before owner approval.
**Review target:** consolidated 78-state package, latest file `consolidated-state-package-04.html` (01-04 contain the same 13 groups and 78 state IDs; 04 contains the latest matter and voice-preview treatment).
**Sources read:** `docs/DESIGN.md`, `docs/design/brand/MISSING-STATES-BRIEF.md`, `docs/design/mocks/brand-proof/flow.html`, current Reader/Flow Flutter code, and the four consolidated package files.
**Method:** first impression, usability, hierarchy, consistency, contrast, touch targets, state clarity, and large-text composition.

## Severity key

- **[BLOCKER]** The state cannot be trusted as a visual reference or violates a locked product rule.
- **[HIGH]** Fix before porting the package into Flutter.
- **[MEDIUM]** Resolve before owner approval or record an explicit decision.
- **[LOW]** Polish after the blocking corrections.
- **[PASS]** No visual correction found in this pass.

## Executive read

The dark-field direction is coherent, and the package keeps matter and heard text ahead of controls. The package is not ready to approve yet. The biggest risks are concrete: several overlays escape the phone, one success state renders without its main copy, the package lies about Original mode, seeking has no visible state change, and the current Flutter dock hides the required play/pause glyph while keeping secondary chrome on screen after idle.

The large-text state also proves only a short three-sentence sample. It does not prove the requirement that a long paragraph reflows, scrolls, preserves the live sentence, and leaves the measured dock clear at 2x.

## Blocking and high-priority findings

### V1. [BLOCKER] Package dialogs and overflow menu are not contained by the phone

`consolidated-state-package-04.html:8` defines `.pkg-phone` without `position: relative`. `pkg-dialog`, `pkg-dialog-backdrop`, and `pkg-menu` are absolutely positioned, but their nearest positioned ancestor is not the phone. They can position against the browser viewport instead of the 390px phone and the backdrop can cover the package page.

**State IDs:** `paste-replace`, `edit-stub`, `report-confirm`, `clear-current-confirm`, `history-delete-confirm`, `model-delete-confirm`, `overflow-open`.

**Correction:** make the phone the containing block (`position: relative`) and verify that the backdrop, dialog, and menu are clipped to the rounded phone. Keep one backdrop only. Do not solve this by adding more z-index layers.

### V2. [BLOCKER] Current Flutter matter has no visible play/pause glyph

The package revision 04 correctly draws a glyph in the matter button. Current Flutter disables it at `os_tts_mobile/lib/presentation/widgets/player_strip.dart:382` with both `showGlyph: false` and `playingGlyph: false`. The matter reads as decoration, not the one playback control required by the brief. Its semantics label cannot replace a visible control for a listener glancing at the screen.

**State IDs:** `reader-idle-original`, `reader-cold-gather`, `reader-synthesizing`, `reader-live`, `reader-paused`, `reader-seeking`, `reader-large`, `reader-retelling-ready`.

**Correction:** render a quiet play glyph when idle/paused and a pause glyph while playing/seeking. Keep the existing 52dp hit target and leave the error matter as a retry affordance rather than pretending it is playback.

### V3. [HIGH] Flutter hides only the top bar, not the dock chrome

Flow fades its secondary dock rows after idle. The current Reader computes `chromeVisible` at `os_tts_mobile/lib/presentation/screens/reader_screen.dart:1481-1487` and passes it to `FlowListenTop`, but `PlayerStrip` only consumes `hideColdChrome` at `os_tts_mobile/lib/presentation/widgets/player_strip.dart:543-545`. After the idle timer, the app bar can disappear while Retelling/Original, voice, speed, and position remain fully visible. That reverses the intended attention order and makes the phone feel like a control panel.

**State IDs:** `reader-live`, `reader-seeking`, `reader-large`, `reader-retelling-ready`.

**Correction:** pass the shared chrome visibility into the secondary dock rows. On idle, leave the matter and the measured dock slot in place. Reveal the rows on tap, scroll, resume, seek, or another explicit user activity. Keep paused, completed, and error states visible as specified by Flow.

### V4. [HIGH] Reader body and badge do not share Flow's 22px plus 2px inset

Flow uses 22px view inset plus 2px reader inset. The dock already uses the 22px value. Current `TextDisplay` uses 16px horizontal padding at lines 249, 300, and 738, and `FerrofluidTextScaleBadge` uses 16px at line 32. The passage and badge therefore sit 8px farther out than the dock's content edge.

**State IDs:** `reader-idle-original`, `reader-live`, `reader-paused`, `reader-seeking`, `reader-large`, `reader-retelling-ready`.

**Correction:** express the geometry explicitly as an outer 22px Reader inset and a 2px inner reading inset, or use the equivalent 24px total padding. Align the Large text badge and disclosure to the same inner edge. Keep the 16px paragraph rhythm and 1.38 line height.

### V5. [HIGH] Large text is not a real long-text proof

`reader-large` renders the same three short sentences as the normal Reader. The artifact has a scrollable `.pkg-reader`, but the fixture is too short to show wrapping, top/middle/bottom scroll, the live sentence after reflow, or dock reservation. Current Flutter has a scroll view, but its 16px inset still misses the composition contract.

**State ID:** `reader-large`.

**Correction:** add a long, at least two-paragraph fixture with one active sentence. Capture the top, a middle position, and the bottom at 412x915 and wide layouts. Verify that 26px text does not clip, the active sentence remains findable, the dock stays a measured sibling, screen-reader order stays one reading stream, and reduced motion removes only ornament.

### V6. [HIGH] The package renders Original as Retelling

`reader-idle-original` carries `mode: 'original'`, but `readerScreen()` at line 164 always marks `Retelling` with `class="on"`. The state note says Original idle while the phone says Retelling selected.

**State ID:** `reader-idle-original`.

**Correction:** bind the selected segment to the state mode. The same rule must use the active transform view in Flutter, including restored sessions.

### V7. [HIGH] Seeking has no visual state in the package

`reader-seeking` and `reader-live` share the same body, matter glyph, dock, and no-banner treatment. The renderer omits the scrubber and timecode entirely, and `playGlyph()` returns pause for both modes. Removing the banner was correct, but removing every visible seeking consequence leaves the state indistinguishable.

**State ID:** `reader-seeking`.

**Correction:** keep the banner out of the passage, then show seeking through a moving or active scrub position, timecode, a restrained progress indicator, or a temporary matter/transport treatment. Match the current Flutter `PositionRow` spinner without adding a second status message.

### V8. [HIGH] `paste-success` loses its headline and body

The state supplies `headline` and `body`, but its `library-success` kind routes to `libraryScreen()`, which reads only `title` and `meta`. The phone card can therefore show `TEXT READY` with an empty title and metadata, while the note beside it still contains the copy.

**State ID:** `paste-success`.

**Correction:** give this state a dedicated renderer or change the state shape. The phone must show "Your text is ready." and the local-storage explanation, followed by one clear `Listen now` action and the secondary browse/settings action.

### V9. [HIGH] Quiet text fails contrast where it carries meaning

The package uses `#5c574e` (`quiet`) on `#070806` (`field`) at about **2.80:1**. It is used for 10px state labels, sheet headings, speed labels, and the 11px centered title. That is below the 4.5:1 normal-text target. The same role is used in current Flutter voice and dock labels.

**Scope:** all 78 state IDs have at least one `pkg-label`, quiet title, or equivalent low-contrast state label.

**Correction:** use `wake` for text the listener must read, especially state labels, sheet headings, and control labels. Reserve `quiet` for non-essential metadata or raise its contrast and size. Measure the Flutter render, not only the hex pairs.

### V10. [HIGH] Reader control hit areas are below the 48dp contract

The Flow top back and overflow controls are 32px in `os_tts_mobile/lib/presentation/screens/reader/_flow_listen_top.dart:66,155-156`. The Ferrofluid Retelling settings affordance is only 36px high at `os_tts_mobile/lib/presentation/widgets/retelling_view_control.dart:178-182`. Speed chips have only text padding and no 48dp minimum. The visual glyph can stay small, but the interactive bounds cannot.

**State IDs:** `reader-idle-original`, `reader-live`, `reader-paused`, `reader-seeking`, `reader-large`, `reader-retelling-ready`, plus `overflow-open` for the menu trigger.

**Correction:** give back, overflow, Retelling settings, and speed choices transparent 48dp hit boxes. Keep the Flow visual scale and spacing by centering the glyph or label inside the larger target. Recheck semantic focus order and narrow width.

## Group review

Every group and every state ID is listed below. Findings above give the proposed correction; this table records the group-level decision.

| Group | Exact state IDs | Review |
| --- | --- | --- |
| Library | `library-empty`, `library-continue`, `library-expanded`, `library-after-paste` | **[MEDIUM]** Empty hierarchy is right. The two primary actions in `library-continue` and `library-after-paste` are unbounded text lines in the package, not 48dp actions. Make the card or its action a real target in the port and transition map. |
| Add text | `reader-empty`, `intake-sheet`, `paste-replace`, `paste-success`, `share-return`, `imports-unavailable` | **[HIGH]** `paste-success` is missing its phone copy. **[BLOCKER]** `paste-replace` has the escaping dialog issue. The empty, intake, share, and unsupported states have clear next actions. |
| First run | `welcome`, `how-it-works`, `model-idle`, `model-downloading`, `model-verifying`, `model-ready`, `model-error`, `model-cancelled`, `local-disclosure` | **[MEDIUM]** The local-first copy is clear. Decide what `Not now` means when no installed voice exists. Do not leave a user in Library with an unusable play control and no explanation. Table key/value hierarchy also needs the correction in V12. |
| Reader | `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` | **[BLOCKER/HIGH]** This is the main approval gate. Fix matter glyph, measured insets, idle dock fade, Original selection, seeking clarity, and the long-text proof. `reader-completed`, `reader-error`, quota, disclosure, and retry copy have a usable primary recovery path. |
| Voice | `voice-ready`, `voice-preview`, `voice-unavailable`, `voice-downloading`, `voice-verifying`, `voice-error` | **[MEDIUM]** `voice-ready` and `voice-preview` show no Close action. `voice-preview` visibly says `Stop preview` but keeps `aria-label="Preview voice"`. The unavailable and download recovery copy is clear. |
| Retelling | `retelling-clean`, `retelling-dirty`, `retelling-cloud-gate`, `retelling-loading` | **[MEDIUM]** Dirty and cloud choices name the consequence. Normalize sheet geometry. `retelling-loading` is visually the same centered treatment as a quiet status; add a restrained busy indication or make the copy itself the deliberate loading treatment. |
| Contents | `contents-available`, `contents-current`, `contents-unavailable` | **[MEDIUM]** `contents-current` supplies current-status copy but the renderer ignores `current: true`, so the current row is not visually selected. Use a live/selected row and keep the action label `Close` consistent with Flow rather than `Close Contents`. |
| More actions | `overflow-open`, `edit-stub`, `report-confirm`, `report-sent`, `report-failed`, `clear-current-confirm`, `clear-current-success`, `clear-current-failure` | **[BLOCKER]** `overflow-open` and all three dialog states have the escaping overlay defect. Report and clear outcomes retain the text and offer recovery. Add the transition links in V11. |
| Settings | `settings-default`, `settings-large`, `settings-reduced-motion`, `settings-account-disabled` | **[MEDIUM]** The static sheet does not show the Library/Reader context or a single backdrop, so it can read as a separate screen rather than a focused sheet. Preserve the selected-row treatment, use Flow sheet geometry, and provide 48dp row targets in Flutter. |
| Premium | `premium-choice`, `premium-checkout`, `premium-cancelled`, `premium-success`, `premium-error`, `premium-local-fallback` | **[MEDIUM]** Local fallback remains visible in every outcome. In `premium-checkout`, table labels and descriptions receive the same wake color, which flattens hierarchy. See V12. |
| Data and deletion | `history-delete-confirm`, `history-delete-success`, `history-delete-failure`, `model-delete-confirm`, `model-delete-progress`, `model-delete-success`, `model-delete-failure`, `data-delete-success` | **[BLOCKER/HIGH]** Both confirm dialogs have the escaping overlay defect. `model-delete-progress` has no cancel, close, or recovery action. Add an interruptible operation or explicitly approve a non-cancellable policy. |
| Platform contracts | `share-handoff`, `notification-contract`, `permission-denied` | **[MEDIUM]** The copy correctly says the OS owns these surfaces, but the generic phone plus matter can be mistaken for a fake native share sheet or lock screen. Label these as `System-owned contract` and show only the title/actions the app hands to the platform. |
| Recovery | `recovery-model`, `recovery-cloud`, `recovery-data` | **[PASS with MEDIUM follow-up]** Each reason has a concrete recovery path and protects local text. Keep the three paths distinct in the transition matrix; do not collapse them into one generic error route. |

## Additional package corrections

### V11. [MEDIUM] The 78 cards are inspectable but not connected by actions

The package's state navigation works, but the rendered primary and secondary buttons have no transitions. The `path` field is documentation only. The output contract asks for action transitions, and recovery cannot be reviewed fully from isolated cards.

**Scope:** all 78 state IDs listed above.

**Correction:** add a compact transition matrix to the package. Wire at least primary, secondary, Cancel, Close, Retry, and Back for every state. Keep platform actions as contracts, but show the handoff and returned state. Do not port until destructive, offline, premium, and model paths are explicit.

### V12. [MEDIUM] Table key/value hierarchy is flattened

`.pkg-table .row span` colors every span wake. This affects the left labels and right descriptions in `how-it-works`, `premium-checkout`, and `notification-contract`.

**Correction:** render a key/value distinction. Use ink or live for the key when it names the choice, and wake for the explanation or status. Do not raise every cell to live.

### V13. [MEDIUM] Sheet geometry differs from the contract

The package's shared `.pkg-sheet` uses `padding: 20px 18px 24px` at line 10. Flow and `DESIGN.md` call for 16/22/28 padding, 20px top radius, and a scrollable max-height.

**State IDs:** `intake-sheet`, `voice-ready`, `voice-preview`, `retelling-clean`, `retelling-dirty`, `retelling-cloud-gate`, `contents-available`, `contents-current`, `settings-default`, `settings-large`, `settings-reduced-motion`.

**Correction:** use the contract values in the package and port. Keep a single scroll viewport inside each sheet so 2x text does not push actions below the phone.

### V14. [MEDIUM] The package voice sheet lacks a close action and has stale preview semantics

`voiceScreen()` at line 169 does not render `pkg-sheet-actions`. Its preview button hardcodes `aria-label="Preview voice"` even when `s.preview` is `Stop preview`.

**State IDs:** `voice-ready`, `voice-preview`.

**Correction:** add `Close`, and make visible copy and semantics agree: `Preview voice` when idle, `Stop preview` while previewing. Preview must never look like the Reader matter control.

## Approval questions

1. Are V1, V2, V4, V5, V6, and V7 required package corrections before any Flutter port, or should any one of them be explicitly deferred?
2. Should idle playback leave only the matter and its measured dock slot, with all secondary rows returning on tap or scroll? Confirm the exact 5-second timer for Flutter.
3. When the user selects `Not now` in `model-idle` and no voice is installed, should Understand allow Library browsing, or require model setup before showing a playable Reader?
4. Approve the long-text fixture for `reader-large`: two paragraphs, 26px, 412x915 top/middle/bottom captures, wide layout, screen-reader order, and reduced-motion capture.
5. Should the current Contents row use `live` text, the spike accent, or both? The answer should be the same in package and Flutter.
6. Should closing the Voice sheet stop an active preview, including system Back and outside tap? Keep the answer separate from Reader playback.
7. Are platform contract cards acceptable inside the phone frame, or should they be clearly labeled system-owned and omit matter so they cannot be mistaken for native UI?
8. Is `Retelling settings` a shipped action for this pass? If yes, the package needs its real dirty, apply, cancel, and loading transitions. If no, do not present a disabled affordance as if it were ready.

## Port gate

Do not approve or port this package until the dialog containment, matter glyph, large-text proof, Original selection, seeking distinction, Reader inset, and idle dock behavior have a recorded decision. After that decision, run contrast and 48dp checks against the same 78 IDs and attach the transition matrix as the acceptance evidence.
