# Understand design-system and implementation-readiness review

**Status:** Not ready for owner approval or a full Flutter port.
**Review target:** `docs/DESIGN.md`, pinned `docs/design/mocks/brand-proof/flow.html` at `30503a4`, Flutter token/theme code, every modal call site, and consolidated state packages 01 through 04.
**Latest package:** `.superpowers/brainstorm/81813-1787986487/content/consolidated-state-package-04.html`.
**Review method:** the three-layer token model from `design-system`, plus the subject-led type, hierarchy, responsive, focus, and reduced-motion checks from `frontend-design`.

This review changed no existing source or browser artifact. The only intended write is this file.

## Executive read

The product contract is specific: dark field, voice first, heard sentence second, controls third, and one focused sheet or dialog at a time. `docs/DESIGN.md:10-12,18-29,39-51` and the pinned Flow define that direction. The active Flutter Reader now has a measured `Column` branch that can keep the dock out of the text, but the rest of the system does not yet carry one consistent token contract into the sheets, dialogs, state variants, or accessibility tests.

The blockers are concrete:

1. Ferrofluid has color roles but no complete primitive, semantic, and component token stack. The theme bridge maps `ink` to `live`, maps `error` to a color different from the contract's `danger`, and gives most ferrofluid widgets the legacy typography set.
2. The shared Flow sheet and dialog are a useful start, but several reachable sheets bypass them. Radius, background, padding, safe-area ownership, and action target rules then change by route.
3. Package 04 has all 78 state IDs, but its actions are static, several state properties are ignored, and its absolute overlays are not contained by the phone.
4. The large-text Reader has the right 20/26 size provider and a measured dock sibling in the active branch. It still uses the wrong horizontal insets, leaves a top banner as an overlay, does not pass idle chrome visibility into the dock, and hides the required play/pause glyph.

Close the token and component contract first. Then prove the 78 states and the real `ReaderScreen` on device. Do not use a green unit or golden suite as a substitute for that replay.

## Contract and source-of-truth mismatches

### [Blocker] The project has two competing visual contracts

`docs/DESIGN.md:6,10-12,18-29,39-51` names the dark Flow roles and says the active face must not be cream paper/copper. `os_tts_mobile/docs/design-language.md` still calls itself the visual source of truth, describes Warm Paper and Editorial Ink as the primary palettes, and states a 44x44 hit-target baseline. `os_tts_mobile/lib/core/design/tokens.dart:1-9` points contributors to that older file and exposes the warm-paper and editorial-ink palettes at lines 91-130.

The F0 note says the dark roles are the source for ferrofluid at `docs/design/brand/f0-flutter-tokens.md:19-29`, while the new contract says the pinned Flow is the source at `docs/DESIGN.md:89`. A contributor cannot tell whether `paper` means `#F7F3EC`, `#15140F`, or the Flow `field`.

**Handoff:** declare `docs/DESIGN.md` plus the pinned Flow as the active contract. Mark `os_tts_mobile/docs/design-language.md` and the old palette names as legacy rollback documentation, or update the references before any more surface work.

### [High] The package and Flow are flat raw-value systems, not three token layers

Flow declares only 11 flat custom properties at `flow.html:12-22`. Package 04 declares another 11 flat `--p-*` values at `consolidated-state-package-04.html:2`, then adds raw gradients, fills, shadows, radii, and type values throughout `:8-10`. The package is not linked to Flow or a shared token file. A static count finds 21 inline `style` attributes and 36 hex literals in package 04.

The Flutter code has two color objects, `UnderstandPalette` in `tokens.dart:21-130` and `FerrofluidPalette` in `ferrofluid_tokens.dart:35-86`, plus a `ThemeExtension` at `ferrofluid_tokens.dart:113-149`. It has no ferrofluid spacing, radius, typography, surface, control, input, dialog, sheet, disabled, focus, or progress component token layer. Active widgets therefore decide their own values.

Examples include `flow_sheet.dart:19,28-39`, `flow_dialog.dart:56,62-68,159-190`, `retelling_settings_sheet.dart:357-389`, `ferrofluid_text_scale_badge.dart:32,37-48`, and `reader/_flow_listen_top.dart:145-190`. They repeat raw colors and geometry instead of consuming component tokens.

**Handoff:** define one token inventory with:

- Flow primitives for the 10 roles plus tonal surface, focus, scrim, and progress values.
- Semantic roles that preserve `ink` versus `live`, `danger` versus other status colors, and `spike` versus identity text.
- Component tokens for Reader dock, text tiers, buttons, chips, rows, sheet, dialog, input, menu, progress, and banner states.
- A lint or static check that rejects raw color, radius, spacing, and type literals in active ferrofluid widgets, except in the token definition file and documented painter math.

### [High] The ferrofluid theme bridge changes semantic meaning and typography

`FerrofluidPalette.asUnderstandPalette` maps `ink: live` at `ferrofluid_tokens.dart:90-105`. The contract assigns `ink` to `#e7e1d4` and `live` to `#f3ebd8` at `docs/DESIGN.md:21-23`. Any ferrofluid widget that reads `UnderstandTheme.of(context).palette.ink`, such as the intake rows and proof settings rows, receives live text and can compete with the heard sentence.

The same bridge maps `error` to `#D26B5F` at line 105. The active contract's destructive role is `Danger #c4785a` at `docs/DESIGN.md:27`. `FerrofluidColors` has no danger, disabled, scrim, success, or warning fields.

`UnderstandTypography.ferrofluid` exists at `tokens.dart:361-445`, but `UnderstandThemeProvider` always constructs `UnderstandTypography.from(resolved.ink)` at `theme.dart:99-103`. The legacy set is body 17/1.6 and CTA 17px; Flow uses body 15/1.45 and CTA 12px. Most active sheets and controls call `UnderstandTheme.of(context).typography`, so they do not receive the ferrofluid set. `TextDisplay` pins its own size and line height at `text_display.dart:138-143`, but its captions, rows, and sheet controls still drift.

**Handoff:** make brand selection resolve palette, typography, spacing, and component tokens together. Keep `ink` as `#e7e1d4`; add the exact `danger` role; do not let the bridge silently turn generic ink into live.

### [High] The shared hit-target token is 44 while the active contract is 48

`UnderstandLayout.minInteractiveDimension` is 44 at `tokens.dart:201-215`, matching the old design-language file but not `docs/DESIGN.md:49` or the existing package review's 48dp requirement. Some controls add 4px around that value, but `FlowDialogAction` has only `8px` vertical padding at `flow_dialog.dart:182`, `_ActionChip` has no minimum at `selected_range_sheet.dart:241-262`, and the Flow Reader back, overflow, and retelling-settings controls are 32 to 36px (`reader/_flow_listen_top.dart:66-156`, `retelling_view_control.dart:174-182`).

**Handoff:** set the active token to 48dp and center the small visual glyph inside it. Add a focus ring and a pressed-state test to every interactive component. Preserve 32 to 40px visual dimensions only when the hit box is transparent and 48dp.

### [High] Rollback selection does not match its own documentation

`BrandThemeNotifier.build()` returns `BrandTheme.ferrofluid` in both branches at `presentation/providers/brand_theme_provider.dart:26-28`. F0 says an unset or false compile flag should reach the legacy path at `docs/design/brand/f0-flutter-tokens.md:37-49,75-77`. The explicit `legacy` preference still works, but a false flag with no preference does not provide the documented rollback.

**Handoff:** make the false/unset path deterministic and add one test for the compile-flag plus preference matrix. This is a delivery safety issue, not a new product choice.

## Modal and dialog audit

The inventory below includes every `showModalBottomSheet`, `showDialog`, and `AlertDialog` call found under `os_tts_mobile/lib`, including calls in files whose names do not contain `sheet` or `dialog`.

| Surface and source | What matches | Exact mismatch and readiness action |
| --- | --- | --- |
| Shared sheet, `presentation/widgets/flow_sheet.dart:10-40` | Correct 20px top radius, 22/16/22/28 container padding, dark sheet branch, one backdrop, and drag handle suppression. | Sheet color and barrier are raw values. The scaffold does not enforce the Flow 78% max height or a scroll viewport. `useSafeArea: true` is owned here, but callers sometimes add another `SafeArea`. Add sheet component tokens, max-height/scroll ownership, and one safe-area owner. |
| Shared dialog, `presentation/widgets/flow_dialog.dart:10-190` | Card radius 16 and 18/16/14 padding match `docs/DESIGN.md:51`. | Background, border, action fill, danger border, and danger text are hardcoded. `FlowDialogAction` has no 48dp constraint or explicit focus treatment. `FlowDialogFrame` has no bounded scroll region for long copy or an editor at 2x. Add `DialogTokens`, a 48dp action box, focus restoration, and a max-height scroll body. |
| Legacy shared sheet, `presentation/widgets/ferrofluid_sheet.dart:6-87`, used by `understand_onboarding_sheet.dart:24-90` | It has a custom handle and a title row. | It uses `palette.paper`, which is Flow `field` under the bridge, rather than the sheet role `#0e0d0b`; its header padding is 20/14/8/8, not 16/22/28. `show()` owns `useSafeArea: true` and the onboarding body adds `SafeArea` again, while the body also paints a second handle. It has no 78% bound. Replace it with `showFlowSheet` or make this the one canonical sheet primitive. |
| Add text, `presentation/widgets/intake_sheet.dart:26-53` | It has a scroll view and truthful unavailable PDF/EPUB rows. | It bypasses `FlowSheet`, uses `palette.paper` instead of sheet background, radius `UnderstandRadius.xl` = 28, and padding 20/16/20/24. This is the primary intake route. Port it through the shared sheet and align the action label/copy with the canonical transition matrix. |
| Contents, `presentation/widgets/contents_sheet.dart:47-238` | It uses `FlowSheetTitle`, a bounded list, live/current row coloring, and a close action. | The list is capped at 420px rather than using the sheet's percentage bound. Titles are `maxLines: 1` with ellipsis at `:226-232`, which loses chapter names at large text. The unavailable variant is a boxed body inside a sheet, while package 04 routes `contents-unavailable` to centered content at `:171`. Define the unavailable variant and a large-text row policy before porting. Close still inherits the undersized `FlowDialogAction`. |
| Retelling settings, `presentation/widgets/retelling_settings_sheet.dart:27-389` | The ferrofluid branch uses Flow sheet, a 0.78 max-height, and a scroll view. Persona and location are separated from the voice picker. | The branch repeats raw field colors at `:357-375`, with no field component token. The package's `retelling-dirty` banner and `retelling-cloud-gate` banner/action are absent. The `draft == null` branch is only a spinner at `:301-309`, not the package's named loading state. Outer `viewInsets.bottom` padding at `:147` can combine with the shared route's safe area. Add dirty, cloud, loading, and one-safe-area variants. |
| Voice selector, `presentation/widgets/voice_selector_sheet.dart:33-38,648-740` | It reuses `FlowSheet` and has separate preview/download code paths. | The outer call sets `isScrollControlled: false` while the child `DraggableScrollableSheet` requests `initialChildSize: 0.92` at `:648`. The parent can clamp the sheet to the default non-scroll-controlled height, making the intended full picker and its scroll contract unreliable. Set one scroll-control strategy and test keyboard/nav-inset sizes. The Flutter picker also adds avatars, tags, and nested row/preview interactions while package 04 uses a plain selected card and hairline rows. Record this as a component variant, not accidental drift. |
| Proof settings, `presentation/widgets/proof_settings_hub_sheet.dart:15-170` | It reuses Flow sheet and exposes the four package rows. | Rows use radius 8 at `:144-157` while package settings rows use radius 12. Outer padding is 16/12/16/20 at `:40-45`, not the shared 16/22/28 contract. `Close` is a stock `TextButton`, and the selected large-text row has no selected border/live variant. Add the row component states and use the shared pill action. |
| Paywall, `presentation/widgets/paywall_sheet.dart:56-60,174-338` | It is scrollable in the lower section and keeps local copy visible in cloud messaging. | It bypasses Flow sheet, sets the route background to `palette.paper`, has no 20px shape or top hairline, and makes the child exactly `MediaQuery.size.height` at `:190`. It uses stock FilledButton/TextButton styling and a `Colors.white` spinner at `:319`. Cloud selection from retelling and the Ask path can reach this modal while ferrofluid is active. Treat it as a documented commercial exception or bring it through the same sheet surface. |
| Transform diff, `presentation/widgets/transform_diff_sheet.dart:29-55` | Paragraphs scroll independently. | It uses `showDragHandle: true`, no Flow background/shape, the default modal surface, fixed 85% height, and 20/8/20/16 padding. The Reader opens it after transform completion at `reader_screen.dart:1239-1259`, so it is reachable from the active path. Add a Flow sheet wrapper or explicitly defer this entry for ferrofluid. |
| Selected range, `presentation/widgets/selected_range_sheet.dart:53-94,225-262` | It has explicit semantics keys and a safe bottom child. | It bypasses Flow sheet, uses radius 28 and `paperRaised`, has no route `useSafeArea`, and has no scroll bound for a long selected preview. Its action chips have no 48dp minimum. The Reader disables this callback in ferrofluid at `reader_screen.dart:2039-2046`, so mark it deferred rather than silently calling it branded. |
| Notes panel, `presentation/widgets/notes_panel.dart:70-145` | It has an intentional single panel and an explicit no-blur barrier. | It is outside the current state package per `INFORMATION-ARCHITECTURE.md:228-234`, but if retained it bypasses Flow, uses barrier alpha .30 instead of the sheet .55, radius 28, and no `useSafeArea`. Keep it out of the port gate or bring it under the same modal exception list. |
| Voice-note delete dialog, `presentation/widgets/voice_note_callout.dart:257-273,752-813` | It has a named custom card and a non-stock action pair. | It still uses a raw `showDialog`, .32 barrier, `palette.paper`, radius 20, and 20/20/16 padding, not the dialog contract. Reader disables voice-note callbacks in ferrofluid, so this is deferred. Add a clear deferred marker and do not count it as dialog parity. |
| Settings and its dialogs, `presentation/screens/settings_sheet.dart:55-61,216-340` | The legacy fallback has a draggable list and explicit destinations. | It has no Flow shape and uses stock `AlertDialog` for clear history, account deletion, model deletion, and a progress dialog. This path is allowed only as rollback. Make that exception explicit and cover the legacy branch separately. |
| Account dialog, `presentation/screens/settings/_delete_account_dialog.dart:49-95` | The typed `DELETE` guard is a clear destructive state. | It intentionally preserves stock `AlertDialog`, default typography, default input, and default radius. That conflicts with the active dialog contract. Use the Flow dialog for the ferrofluid path; retain stock only in a named legacy exception. |
| Library delete, `presentation/screens/history_screen.dart:693-725,816-840` | Long-press provides an accessible alternative to swipe. | `_confirmDelete` always opens stock `AlertDialog` at `:821-840`, including ferrofluid Library. The swipe background also uses `Colors.red.shade700` and white at `:704-708`, not Danger. Route both to the shared delete dialog and danger token. |
| Offline LLM delete, `presentation/screens/settings/_offline_llm_tile.dart:80-111` | It names the downloaded model and has a retryable operation. | It opens stock `AlertDialog` and uses `colorScheme.error`, not the Flow dialog and exact Danger role. This is legacy settings today, but the exception must be recorded. |
| Legacy Reader overflow add-ons, `presentation/screens/reader/_reader_app_bar.dart:441-458` and `wand_sheet_body.dart:25-66` | The call is gated out of ferrofluid and the body scrolls. | It uses a default draggable sheet, default drag handle, stock `ChoiceChip`, and Theme text slots. Keep it legacy-only or migrate when that state becomes active. Ferrofluid must not expose a second modal design through a future gate. |

## 78-state package audit

All four consolidated files contain the same 78 unique state IDs in 13 groups. The counts are: Library 4, Add text 6, First run 9, Reader 14, Voice 6, Retelling 4, Contents 3, More actions 8, Settings 4, Premium 6, Data and deletion 8, Platform contracts 3, and Recovery 3. Package 04 is the latest file in the session log, but none of the four package files are tracked by Git. The existing state review therefore names package 01 while the latest visual correction lives in package 04. Pick one tracked canonical package before approval.

### [Blocker] The package's absolute overlays are not contained

Package 04 defines `.pkg-phone` without `position: relative` at line 8. `.pkg-dialog`, `.pkg-dialog-backdrop`, and `.pkg-menu` are absolute at lines 10 and 172. Their containing block can be the page rather than the phone, so the backdrop and dialog can escape the 390px frame. This affects `overflow-open`, `paste-replace`, `edit-stub`, `report-confirm`, `clear-current-confirm`, `history-delete-confirm`, and `model-delete-confirm`.

Add a positioned phone and verify clipping against the rounded edge. Keep one backdrop. Do not solve this with another z-index layer.

### [High] Action buttons do not transition

`buttons(s)` only returns buttons at package 04 line 158. There are no action listeners in the package. The only action hooks are group/state navigation at lines 18 and 152-153, plus the scale toggle at line 179. The `path` field is a note, not a transition. The package is an inspection gallery, not a replayable 78-state flow.

Add a state transition table keyed by canonical IDs. Wire primary, secondary, Close, Cancel, Retry, Apply, Back, and platform handoff actions. Preserve the originating Reader state and playback position on return.

### [High] Shared renderers ignore state variants

- `reader-idle-original` carries `mode: 'original'` at line 59, but `readerScreen()` hardcodes Retelling selected at line 166.
- `paste-success` carries `headline` and `body` at line 43, but `libraryScreen()` reads only `title` and `meta` at line 160. The rendered card can lose the success copy.
- `contents-current` carries `current: true` at line 90, but `contentsScreen()` does not read that field at line 171. The selected row has no visual variant.
- `voice-preview` says `preview: 'Stop preview'` at line 76, but the button keeps `aria-label="Preview voice"` at line 169. Visible copy and semantics disagree.
- `voice-error` uses kind `error` at line 80 and therefore routes through the generic setup renderer in the switch at line 177, unlike the `voice-unavailable` renderer.
- `reader-paused` and `reader-seeking` intentionally remove banners, but the renderer provides no other seeking consequence. Those two states share the same visual output except for the play glyph selection.
- `retelling-dirty`, `retelling-cloud-gate`, and `retelling-loading` have distinct data, but their action and loading treatment remains generic.

Define a component variant contract before adding more IDs. A state property must either affect the rendered tree or be removed from the fixture.

### [High] Package geometry is not the Flow geometry

Package 04's shared `.pkg-sheet` uses `padding:20px 18px 24px` at line 10. The active contract and pinned Flow use 16/22/28 at `flow.html:320-324` and `docs/DESIGN.md:39-50`. Package has no max-height or overflow rule on `.pkg-sheet`, while Flow has `max-height:78%; overflow:auto`. Package uses 21 inline styles and raw color values, so future variants will drift further unless it consumes a shared token export.

The package Reader is structurally promising: `.pkg-reader` has `overflow:auto` and `.pkg-dock` is a flex sibling at line 9. It is not sufficient proof because the fixture has only three short sentences, no top/middle/bottom long-text captures, and no safe-area model.

### [Medium] Package semantics are incomplete

The package renderer emits `pkg-sheet` and `pkg-dialog` divs without `role="dialog"`, `aria-modal`, labelled headings, focus management, or action semantics. The pinned Flow has explicit dialog roles at `flow.html:550-616`. The package can remain a visual storyboard, but the handoff must not call it an accessibility-ready implementation contract.

## Large-text Reader readiness

### What is safe to keep

The active branch uses a real sibling layout when ferrofluid is on:

- `reader_screen.dart:1743-1749` puts `Expanded(buildReaderStack(flowLayout: true))` and `playerStrip` in a `Column`.
- The fixed `_kLegacyPlayerStripHeight` reservation is only used by the legacy branch at `reader_screen.dart:1533-1534` and should remain rollback-only.
- `TextDisplay` uses a scroll view for its paragraph body at `text_display.dart:245-305`.
- `ferrofluid_text_scale_provider.dart:9-10,27-56` resolves the intended 20px normal and 26px large presets and synchronizes the existing reader-size provider.
- Sentence tiers are present at `text_display.dart:489-524`: live, wake, and quiet are separate styles, and cold mode keeps non-live text transparent rather than moving it over the dock.
- `PlayerStrip` owns bottom safe-area padding at `player_strip.dart:567-613`, and the active dock uses 22px horizontal padding with a 20px bottom value at `:601-613`.

Keep this structure. Do not put the dock back into the Reader stack or reintroduce a fixed height for the active brand.

### [Blocker] The one playback control has no visible glyph

`player_strip.dart:370-383` passes `playingGlyph: false` and `showGlyph: false` to `FerrofluidMatter`. The painter only draws a glyph when `showGlyph` is true at `ferrofluid_matter.dart:184-216`. Package 04 draws Play or Pause in its matter button at `:163-166`, and the state review says this is required at `docs/design/brand/STATE-PACKAGE-REVIEW.md:110-119`.

Keep the 48px hit box and 46px visual matter, but show a quiet Play glyph when idle/paused and Pause while playing or seeking. Keep error matter as Retry, not as a fake transport state.

### [High] Reader body and badge use 16px instead of Flow's 22px plus 2px

Flow uses 22px view padding at `flow.html:45-48` and 2px Reader inner padding at `:184-190`. The dock already uses 22px. Flutter `TextDisplay` uses 16px horizontal padding at `text_display.dart:249` and `:300`; the badge uses 16px at `ferrofluid_text_scale_badge.dart:32`. The passage and badge sit 6px outside the Flow view edge, and the text body lacks the explicit 2px inner inset.

Make the geometry visible in the widget tree: outer Reader content 22px, badge aligned to that edge, and text scroll view with 2px horizontal inner padding. Keep 16px paragraph spacing and 1.38 line height.

### [High] Idle chrome visibility stops at the top bar

Reader computes `chromeVisible` from the orchestrator at `reader_screen.dart:1481-1487` and passes it to `FlowListenTop` at `:1724-1730`. `PlayerStrip` receives only `hideColdChrome` at `player_strip.dart:543-545`; it never receives the settled/hidden `chromeVisible` value. Its secondary segment, voice row, speed row, and position remain visible after the idle timer while only the top bar fades.

Pass the same visibility state into secondary dock rows. Keep matter and its measured slot present. Restore rows on tap, scroll, seek, or resume. Keep paused, completed, and error policies explicit.

### [High] A top banner still paints over the measured Reader body

The active flow branch removed the dock overlay, but `buildReaderStack()` still places Quota, first-run, or share content in a `Positioned(top: 0)` overlay at `reader_screen.dart:1563-1611`. The body below starts independently at `:1533-1561`. A multi-line banner can cover the first paragraph, especially at 26px or system 2x text. The comment says one overlay, but the contract requires the reading viewport to remain honest.

Reserve banner height in the column, move the banner into the body flow, or prove that the banner never overlaps readable content at 412x915 and wide sizes. Keep only one backdrop for modal surfaces; these banners are not a reason to add more layers.

### [High] Safe-area ownership is not tested on the real Reader route

`PlayerStrip` wraps its contents in `SafeArea(top:false)` at `:567`, while `FlowListenTop` adds a `SafeArea(bottom:false)` inside a custom `PreferredSize` at `reader/_flow_listen_top.dart:52-75`. The real Reader body also has the top `Positioned` `SafeArea` at `reader_screen.dart:1571-1573`. This may be correct on one device, but the route has several independent inset owners and no test that captures status-bar, gesture-nav, keyboard, and cutout variants.

Use one owner for each edge. Capture 412x915 with normal nav, gesture nav, a nonzero `viewPadding`, and an open keyboard. Fail on `RenderFlex overflow`, text behind the dock, text behind a banner, or a control below the gesture inset.

### [High] Existing large-text goldens do not prove the real path

`reader_dock_golden_test.dart:105-194` builds a direct `Scaffold` with `ReaderAppBar`, a body fixture, and `PlayerStrip`. It does not build `ReaderScreen`, `FlowListenTop`, the `buildReaderStack` overlay, or the actual `Column` branch. It sets `disableAnimations: true`, so it also cannot prove the 520ms gather, first voice, 400ms ignite, or 380ms chrome handoff.

The 2x test at `reader_dock_golden_test.dart:270-294` is useful regression coverage, but it is not a no-overlay or safe-area test. Add a real Reader fixture with seeded providers and a timed probe.

### [Medium] Large-text tracking is not token-derived at both sizes

`TextDisplay` pins `letterSpacing: -0.3` at `text_display.dart:138-143`. Flow uses `-.015em` at `flow.html:184-190`, which is -0.30px at 20px and -0.39px at 26px. The 26px state therefore has a different type rhythm. Derive tracking from the selected text size or give normal and large their own typography component tokens.

### [Medium] Large text can lose meaningful Contents labels

`ContentsSheet` forces chapter titles to one line and ellipsizes them at `contents_sheet.dart:226-232`. This avoids height growth but makes the large-text state less useful. Let rows grow to two or more lines at large text, or provide a deterministic accessible label and test the visible truncation policy.

## State and component reuse decisions

The reusable map in `INFORMATION-ARCHITECTURE.md:216-226` is the right boundary. The implementation does not yet honor it:

- Bottom sheet reuse is split among `FlowSheet`, `FerrofluidSheetScaffold`, direct `showModalBottomSheet`, and default Material routes.
- Dialog reuse is split among `FlowDialogFrame`, custom cards, and stock `AlertDialog`.
- Matter has one painter, but the Reader transport turns its glyph off and generic palette reads can turn ink into live.
- Progress uses custom and stock indicators interchangeably. `BufferStatusLabel` is token-aware, while paywall/model rows use stock `CircularProgressIndicator` and `LinearProgressIndicator`.
- State IDs exist in the package, but Flutter keys are per widget feature, not a canonical map of all 78 IDs. Map every approved state to a stable key and fixture name.
- The state package's `reader-idle-original`, `contents-current`, `voice-preview`, `paste-success`, and loading variants need corrections before their names become test contracts.

## Handoff checklist

### Contract and tokens

- [ ] Owner approves `docs/DESIGN.md` plus pinned Flow as the active contract. Mark the older design-language source as legacy or reconcile it.
- [ ] Add a complete ferrofluid token export with primitive, semantic, and component sections. Include `field`, `fieldLift`, `matter`, `ink`, `live`, `wake`, `quiet`, `line`, `spike`, `danger`, scrim, disabled, focus, sheet, dialog, input, button, row, dock, and progress roles.
- [ ] Fix `asUnderstandPalette`: generic `ink` stays `#e7e1d4`; destructive/error uses exact Danger `#c4785a`; generic status colors do not masquerade as Danger.
- [ ] Make `UnderstandThemeProvider` select the ferrofluid typography set when the brand is ferrofluid. Remove widget-level copies of Flow type values.
- [ ] Set the active interactive dimension to 48dp. Add visible focus, pressed, disabled, loading, and error variants that do not rely on color alone.
- [ ] Make the brand rollback matrix pass: compile flag true, compile flag false, preference unset, preference legacy, preference ferrofluid.
- [ ] Add a raw-value lint or review script. Exceptions must name the token definition or painter geometry file.

### Shared components and modals

- [ ] Use one Flow sheet route for Add text, Voice, Retelling settings, Contents, Settings, and any active Premium entry. Its contract is `#0e0d0b`, 20px top radius, 16/22/28 padding, one 78% scroll viewport, one backdrop, one safe-area owner, and the 350ms entrance.
- [ ] Use one Flow dialog route for Replace, Report, Clear, Delete, and Edit. Its contract is `#12110e`, 16px radius, 18/16/14 padding, one bounded scroll body, 48dp actions, focus restoration, and Danger for destructive actions.
- [ ] Decide and document exceptions for paywall depth, selected range, Notes, voice notes, and legacy-only settings. An exception must state brand, state IDs, route, safe-area owner, and test coverage.
- [ ] Normalize component geometry. No active sheet uses radius 28 or default Material shape; no active dialog uses stock `AlertDialog`; no active progress state uses an unthemed stock indicator.
- [ ] Add state variants for clean/dirty/cloud/loading, unavailable/current Contents, voice preview/download/error, and destructive progress/success/failure. Keep visible copy and semantics in sync.
- [ ] Add 48dp hit boxes around Flow back, overflow, retelling settings, speed, sheet close, dialog actions, list rows, and voice preview. Keep visual glyphs small where the Flow calls for it.

### 78-state package

- [ ] Choose one canonical, tracked package file. Preserve all 78 unique IDs and the 13 group counts.
- [ ] Add `position: relative` to the phone and verify dialog/menu/backdrop clipping at narrow and wide page sizes.
- [ ] Add a transition table keyed by state ID. Wire every primary, secondary, Close, Cancel, Retry, Apply, Back, and platform handoff action. Preserve prior Reader state, mode, voice, and playback position.
- [ ] Fix ignored state properties: Original selection, paste success copy, Contents current row, Voice Stop preview semantics, Voice error renderer, paused/seeking visual consequence, and retelling dirty/cloud/loading variants.
- [ ] Make package sheets use the same 16/22/28 and 78% scroll contract as Flow. Remove inline style drift or export the shared tokens into the preview.
- [ ] Add package roles and labelled dialog/sheet semantics if the package is used for accessibility review. Otherwise label it visual-only in the approval record.

### Large-text Reader proof

- [ ] Keep the active `ReaderScreen` `Column` path with `PlayerStrip` as a measured sibling. Do not use `_kLegacyPlayerStripHeight` in ferrofluid.
- [ ] Show the matter control's visible Play/Pause glyph. Test idle, gather, synth, live, paused, seeking, completed, error, and retelling states.
- [ ] Align body geometry to 22px outer inset plus 2px inner reading inset. Align the Large text badge and disclosure to the intended edge. Preserve 16px paragraph spacing and 1.38 line height.
- [ ] Pass orchestrator chrome visibility into secondary dock rows. Verify idle fade, tap/scroll reveal, pause/error visibility, and reduced-motion behavior.
- [ ] Move or reserve the top Quota/demo/share banner so it cannot cover the first paragraph. Verify long copy at 26px.
- [ ] Pick one safe-area owner per edge. Test status bar, gesture navigation, display cutout, keyboard, and rotation/wide layouts.
- [ ] Seed at least two long paragraphs with a known active sentence. Capture top, middle, and bottom scroll positions at 412x915 and wide. Test normal preset, Large text preset, and OS 2x text scaling separately.
- [ ] Assert no `RenderFlex overflow`, no clipped active sentence, no body pixels behind the dock, no banner overlap, and no control below the bottom inset.
- [ ] Assert one semantics reading stream and one announcement per state change. Reduced motion keeps the order matter -> text -> controls and removes ornamental motion.

### Mechanical parity and delivery

- [ ] Extend `scripts/design/parity/render_design.py` beyond its current Flow subset at `:16-42`, or create an explicit 78-state renderer registry. Every approved visual state must have a canonical fixture, design render, app capture, and report.
- [ ] Keep `flow-parity-rules.json` tied to the same token export. It currently captures Flow colors and selectors, not the package state IDs or component-token ownership.
- [ ] Run focused Flutter tests for Flow sheet/dialog geometry, state variants, semantics, and the real Reader route. Keep existing goldens, but do not treat the direct fixture as the safe-area proof.
- [ ] Run `flutter analyze` and targeted widget tests after the token/component pass. Then replay the user story on a real device: Library -> paste/sample -> first voice -> live sentence -> chrome settle -> Large text -> switch Original/Retelling -> return to Library while playback continues.
- [ ] Record all remaining exceptions and attach the 78-state transition/parity evidence before owner approval. No release, store, billing, or external mutation is part of this review.

## Review verdict

The Reader sibling layout is implementable, but the design system is not ready to support a safe 78-state port. Resolve the token source conflict, semantic bridge, component token layer, modal normalization, package containment/actions, and the Reader glyph/inset/banner tests first. Then the owner can approve a smaller vertical slice with evidence instead of approving a gallery and hoping the routes converge later.
