critical priority medium complexity backend pending frontend specialist Tier 3

Acceptance Criteria

DynamicTypeScaleService exposes a `getLayoutBudget(TextRole role, double scaleFactor)` method that returns reduced size budgets when scaleFactor > 1.5
Budget adjustments are defined per-role (display, headline, body, label) and prevent font sizes from exceeding screen-width-fraction thresholds at any scale
ScaleOverflowGuard is a mixin applicable to State<T> classes that provides a `isCompactScale` bool getter returning true when textScaleFactor >= 2.0
Screens using ScaleOverflowGuard automatically rebuild when textScaleFactor crosses the 2.0 threshold
When `isCompactScale` is true, the screen switches to a single-column layout (no horizontal splits, reduced padding, compact list item height)
The layout switch is smooth — no layout error boxes or overflow indicators appear at any scale between 1.0 and 2.0
A `ScaleTestHelper` class is provided with a static `pumpWithScale(WidgetTester tester, Widget widget, double scale)` method for use in widget/golden tests
ScaleTestHelper correctly overrides MediaQuery.textScaleFactor for the pumped widget subtree only
All existing DynamicTypeScaleService tests continue to pass without modification
Golden tests at 100%, 150%, and 200% scale show progressively adjusted layouts with no RenderFlex overflow

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
MediaQuery (textScaleFactor)
data models
DynamicTypeScaleService (existing)
TextRole (enum — display/headline/title/body/label)
LayoutBudget (new value object)
performance requirements
Scale threshold check must be synchronous and execute in O(1) — no async calls in the getter
Layout rebuild triggered by scale change must complete within one frame (16ms budget)
security requirements
No user data involved; MediaQuery access must not cache scale factor across widget tree updates
ui components
ScaleOverflowGuard (mixin for State classes)
ScaleTestHelper (test utility class)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement ScaleOverflowGuard as a mixin on State that calls `MediaQuery.of(context).textScaleFactor` in build and exposes `bool get isCompactScale => MediaQuery.of(context).textScaleFactor >= 2.0`. The mixin does not need to call setState — since it reads MediaQuery in build, Flutter's rebuild mechanism handles scale changes automatically. For ScaleTestHelper, wrap the widget in a MediaQuery override: `MediaQuery(data: MediaQuery.of(context).copyWith(textScaleFactor: scale), child: widget)`. For budget definitions, use a const map keyed by TextRole with two tiers: `>1.5` budget (80% of nominal size) and `>=2.0` budget (65% of nominal size).

Document the compact layout contract clearly: single column means `Column` with no `Row` children containing text — apply a lint comment `// compact: no Row with text` at relevant layout branch points so code reviewers can verify compliance.

Testing Requirements

Unit tests (flutter_test): verify `getLayoutBudget` returns full budget at scaleFactor 1.0 and 1.4; verify reduced budget at 1.5, 1.75, and 2.0 for each TextRole; verify budget values are strictly decreasing as scaleFactor increases. Widget tests: apply ScaleOverflowGuard to a test screen, pump at scale 1.9 and verify `isCompactScale` is false; pump at scale 2.0 and verify `isCompactScale` is true and layout switches to single-column. ScaleTestHelper tests: verify MediaQuery.textScaleFactor in the pumped subtree matches the provided scale value. Golden tests: using ScaleTestHelper, capture the activity registration form at 1.0x, 1.5x, and 2.0x scale; verify zero RenderFlex overflow errors in all three golden images.

Regression: run full widget test suite with scale 2.0 on all form screens to surface any unguarded overflow.

Component
Dynamic Type Scale Service
service medium
Epic Risks (4)
medium impact high prob integration

Flutter's textScaleFactor behaviour differs between iOS and Android, and third-party widgets used across the app (date pickers, bottom sheets, chips) may not respect the per-role scale caps applied by the dynamic-type-scale-service, causing overflow in screens this epic cannot directly control.

Mitigation & Contingency

Mitigation: Enumerate all third-party widget usages that render text. For each, verify whether they honour the inherited DefaultTextStyle and MediaQuery.textScaleFactor or use hardcoded sizes. File issues with upstream packages and wrap non-compliant widgets in MediaQuery overrides scoped to the safe cap for that role.

Contingency: If upstream packages cannot be patched within the sprint, implement a global MediaQuery wrapper at the app root that clamps textScaleFactor to the highest per-role safe value (typically 1.6–2.0), accepting that users at extreme OS scales see a safe cap rather than full scaling for those widgets.

high impact medium prob dependency

The CI accessibility lint runner depends on the Dart CLI toolchain and potentially custom_lint or a bespoke Dart script. CI environments differ from local dev environments in Dart SDK version, pub cache configuration, and platform availability, risking intermittent CI failures that block all pull requests.

Mitigation & Contingency

Mitigation: Pin the Dart SDK version in the CI workflow configuration. Package the lint runner as a self-contained Dart script with all dependencies vendored or declared in a dedicated pubspec.yaml. Add a CI smoke test that runs the runner against a known-compliant fixture and a known-violating fixture to verify the exit codes are correct.

Contingency: If the custom runner proves too fragile, fall back to running dart analyze with the flutter-accessibility-lint-config rules as the sole CI gate, and schedule the custom manifest validation as a separate non-blocking advisory check until the runner is stabilised.

medium impact medium prob technical

Wrapping all interactive widgets with a 44 pt minimum hit area via HitTestBehavior.opaque may cause unintended tap interception in widgets where interactive elements are closely stacked, particularly in the expense type selector, bulk confirmation screen, and notification filter bar.

Mitigation & Contingency

Mitigation: Conduct integration testing of the touch target wrapper specifically in dense layout scenarios (expense selector, filter bars, bottom sheets with multiple buttons). Use the Flutter Inspector to visualise hit areas and confirm no overlaps. Pair with the interactive-control-spacing-system to ensure minimum 8 dp gaps between expanded hit areas.

Contingency: If overlapping hit areas cause mis-tap regressions in specific screens, allow the touch target wrapper to accept an explicit hitAreaSize parameter that can be reduced below 44 pt only in contexts where the interactive-control-spacing-system guarantees sufficient gap, with a mandatory code review flag for any such override.

high impact medium prob scope

The contrast-safe-color-palette must guarantee WCAG AA ratios for both light and dark mode token sets. Dark mode color derivation is non-trivial — simply inverting a light palette often produces pairs that pass in one mode but fail in the other, and the token manifest must encode both sets explicitly.

Mitigation & Contingency

Mitigation: Define both light and dark token sets explicitly in the accessibility-token-manifest rather than deriving one from the other programmatically. Run the contrast-ratio-validator against both sets as part of the token manifest generation process and include both in the CI lint runner's validation scope.

Contingency: If time pressure forces a dark mode deferral, ship with light mode only and add a prominent in-app notice. Gate dark mode colour tokens behind a feature flag until the full dual-palette validation is complete.