high priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

The widget renders without RenderFlex overflow warnings at textScaleFactor 1.0, 1.5, 2.0, and 3.2
At textScaleFactor 3.2 all critical content (logo, denial heading, denial message, portal link, logout button) remains visible and reachable by scrolling — nothing is clipped or hidden behind the keyboard/safe area
A SingleChildScrollView (or equivalent scrollable container) is the outermost layout primitive of the widget body, enabling vertical scrolling when content height exceeds viewport height
The logo does not scale proportionally with text — it uses a fixed size or a maxHeight constraint so it does not crowd out textual content at large scales
No hardcoded pixel heights are used for text containers; all text containers use intrinsic sizing or Flexible/Expanded to allow growth
The layout passes flutter_test pump at all four scale factors with zero exceptions and zero overflow assertions
On a physical device set to the largest accessibility font size, a user can scroll to the logout button and activate it without the button being cut off
The bottom safe area (home indicator on iOS, navigation bar on Android) is respected at all scale factors using SafeArea or equivalent padding

Technical Requirements

frameworks
Flutter
performance requirements
ScrollView must not rebuild unnecessarily — use const constructors for static children
Avoid ListView.builder for a single-screen static layout; SingleChildScrollView with a Column is sufficient and more performant
ui components
SingleChildScrollView as the root scrollable container
Column with mainAxisSize: MainAxisSize.min for flexible stacking
Expanded or Flexible for text sections that should grow
FittedBox or SizedBox with explicit maxHeight for the organisation logo
SafeArea for system UI insets

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The most common failure mode for this widget will be a Column inside a fixed-height container — remove any fixed height constraints from the Column's parent and replace with SingleChildScrollView > Column(mainAxisSize: MainAxisSize.min). For the logo, use Image.asset with a BoxFit.contain inside a ConstrainedBox(constraints: BoxConstraints(maxHeight: 80)) so it shrinks proportionally but never grows to fill available space at large text sizes. For body text, avoid wrapping Text in a SizedBox with a fixed height — let the Text widget self-size. The denial message paragraph may need a flexible container: use Padding rather than SizedBox for spacing between elements.

Test with MediaQuery.textScalerOf override in tests rather than setting device font size, for deterministic results. The 3.2 scale factor maps to the Android 'Largest' font size setting and the iOS 'AX5' Dynamic Type size — these are real user settings and must work correctly per the workshop requirements for users with cognitive and visual accessibility needs.

Testing Requirements

Write parameterised widget tests using @TestVariants or a loop over textScaleFactors [1.0, 1.5, 2.0, 3.2]. For each scale: (1) wrap the widget with MediaQuery(data: MediaQueryData(textScaler: TextScaler.linear(scale)), child: widget), (2) pump and settle, (3) assert no RenderFlex overflow exceptions via expectLater with throwsA matcher negation, (4) assert that find.byKey(Key('no-access-logout-button')) exists in the widget tree (not clipped). Also run the AccessibilityGuideline checks at scale 3.2 specifically, as this is the most likely failure point.

Component
No-Access Screen
ui low
Epic Risks (2)
medium impact medium prob technical

Flutter's live region (SemanticsProperties.liveRegion) announcement may be delayed or swallowed by the OS accessibility engine if the Semantics tree is not fully built when the screen mounts, causing screen-reader users to miss the denial announcement.

Mitigation & Contingency

Mitigation: Trigger the live region announcement from a post-frame callback (WidgetsBinding.addPostFrameCallback) to ensure the Semantics tree is committed before the announcement fires. Test on both VoiceOver (iOS) and TalkBack (Android) physical devices.

Contingency: If live region timing is unreliable, fall back to using SemanticsService.announce() directly in the initState post-frame callback, which provides more deterministic announcement timing.

low impact low prob scope

The organisation logo may fail to load (network error, missing asset) leaving a broken image in an otherwise functional screen, degrading the professional appearance and potentially confusing users.

Mitigation & Contingency

Mitigation: Wrap the logo widget in an error builder that renders a styled fallback (organisation name text or a generic icon) when the logo asset or network image fails to load.

Contingency: If logo loading is persistently unreliable across organisations, remove the logo from the no-access screen entirely in favour of a text-only header using the organisation's display name from the design token system.