A living tracker of open design-team-facing questions surfaced while mapping the Figma components to the mobile component library. Each item is something we need a design decision or confirmation on before the spec can be called final — distinct from backend data gaps, which live in the Backend API Mapping Report.
Figma file reference
There are two Figma files for this project. Questions below that link to Figma nodes use one of these two files:
| Label used in questions | File ID | What it is | Open in Figma |
|---|---|---|---|
| Base design | owEYzHf7FrHRvWC2u82UOl |
Original ALPA Mobile app design — base components and screens, no MEC customizations | Open base design |
| MEC customization file | gpO3masyyNNHxjvRtdcRo7 |
Copy of the base file extended by Interactive Strategies — contains the per-airline MEC screens (UAL, FDX, DAL) and design annotations | Open MEC file |
How to use: design reviews each open question, records the decision in the Design answer row, and we fold it back into the affected component spec(s). Resolved items move to the Decided section with a date.
Futura PT token vs Futura LT Pro layer overrideWhat we found (from the Figma source):
Font Family/Body resolves to "Futura PT"
(alongside Font Family/Heading = Lora, Font Family/Eyebrow = Inter).4099:5230) renders only Futura PT —
its generated code uses Futura PT Book / Demi and no "LT Pro".Font Family/Body token.Why it matters: Futura PT and Futura LT Pro are different physical typefaces. If the overrides are unintentional, those buttons/labels would render in a different font from the rest of the app. Our component library standardizes on the Futura PT token, so today we treat the LT Pro instances as overrides to reconcile.
Is Futura LT Pro intentional on those button / hero layers, or should they be re-bound to the
Font Family/Body (Futura PT) token? If it's a deliberate display face for buttons, we'll add it
to the token set explicitly; otherwise please clear the override in Figma so the layers inherit Futura PT.
Design answer (2026-06-22): Futura PT is confirmed as the single body font family for the app. The Futura LT Pro overrides on Button, Button Group, and Hero layers are not intentional — those layers should be re-bound to the Font Family/Body (Futura PT) token in Figma. All component specs and token tables updated to reflect Futura PT throughout.
Font files confirmed available (2026-06-22): Futura PT Book and Futura PT Bold (.otf). Weights referenced in Figma layer tree but not yet confirmed as available files: Medium, Demi, Heavy — to be resolved at the ALPAMobile.Presentation font-bundling pass (deferred; the library shipped as a Blazor RCL 2026-07-15, D61).
Affected specs updated: button, button-group, card-hero, design-tokens.
What we found: the card-lg (CardHero / Document Hero) image carries a 6 pt left-border
accent. The default is Border/Blue-lt
#007BC2; some instances show a Border/Yellow
#F7BB0D variant. The Figma source has no annotation explaining when each applies.
Figma reference: base design — card-lg master (node 4033:1436) — both the blue and yellow accent variants are in the component variant set. The home screen (node 4099:5230) shows card-lg instances with each color in context.
(1) What drives the yellow variant — is it tied to a content attribute (e.g. category / priority / status on the document, which would make it data-driven and require a backing field), or is it static per placement? (2) Is the accent limited to these two colors, or is there a fuller set we should tokenize?
Why it matters: the answer decides whether the backend needs an
accent/category field on DocumentItem. Tracked as the deferred design item in the
Backend API Mapping Report (Part 1 §4, Open Questions).
Design answer (2026-06-26): Blue only, static. The 6 pt left-border accent on card-lg is always Border/Blue-lt #007BC2 — it is not data-driven and does not vary by content category or status. The yellow variant (#F7BB0D) has no confirmed usage and may be removed from the Figma file entirely. Backend impact: none — no accent field is needed on DocumentItem; the color is hardcoded to the token value at render time.
Affected specs: card-hero, home-screen-data-gap, backend-api-mapping-report (Part 1 §4 — accent field question closed).
What we found: DQ-1 confirmed Futura PT as the single body font family with Book and Bold .otf files in hand. A full Figma MCP sweep of all master component nodes (2026-06-26) found three additional weights in use with no confirmed font file for any of them. Node list below, grouped by weight — for procurement reference.
| Component | Figma Node | Layer / Text | Size | Color |
|---|---|---|---|---|
| Flight Segment Card | 4555:7483 |
Date/time header — text node 4555:7208 |
18px | #05273e (navy) |
| Flight Segment Card | 4555:7483 |
Airport codes (DEN, MIA) — I4555:6999;4322:5670, I4555:7004;4322:5670 |
18px | #05273e |
| Section Title | 4596:11818 |
"Flight Segments" — 4589:9728 |
18px | #05273e |
| Home Screen (main-feed) | 4099:5230 |
Survey banner title "Survey Now Open" — I4099:5230;4038:893;20652:13451;5512:12934 |
18px (Font Size/Body/M) |
#ffffff |
| Component | Figma Node | Layer / Text | Size | Color |
|---|---|---|---|---|
| Home Screen (main-feed) | 4099:5230 |
Emergency Hotline button label — I4099:5230;4038:893;4270:5843;5348:29578 |
15px, uppercase, ls 0.6px | #ffffff |
| Segmented Control (tab group) | 4361:8479 |
Pill labels "Nonstop / One Stop / Multileg" — I4361:8464;4245:5756, I4361:8465;4245:5756, I4361:8466;4245:5708 |
15px, uppercase, ls 0.6px | #05273e / #ffffff |
| List (listy card generic) | 5172:15482 |
Title "Lorem" — 5172:15472 |
20px (Font Size/Body/L) |
#05273e |
Futura:Medium via the Body font token — same physical file required)| Component | Figma Node | Layer / Text | Size | Color |
|---|---|---|---|---|
| Bottom Navigation | 4038:1037 |
Tab labels (Home, Jumpseat, KCM, FTDT, My MEC) — I5035:16371;4020:3420 ×5 |
12px, uppercase | #ffffff |
| Flight Segment Card | 4555:7483 |
"Flight Time" label — 4555:7007 |
16px | #05273e |
| Home Screen (main-feed) | 4099:5230 |
Survey banner subtitle "Closes June 10 at 11:59PM PT" — I4099:5230;4038:893;20652:13451;5512:12935 |
12px (Font Size/Body/S) |
#ffffff |
| Home Screen (main-feed) | 4099:5230 |
Welcome "View Contract" link — I4099:5230;4038:893;4185:10450 |
18px, underline | #007bc2 |
| Home Screen (main-feed) | 4099:5230 |
Feed header "View My MEC" link — I4099:5230;4038:893;4099:5247;4143:8273 |
18px, underline | #007bc2 |
| Home Screen (main-feed) | 4099:5230 |
Section "View All" link — I4099:5230;4038:893;20652:13468 |
18px, underline | #007bc2 |
| Home Screen (main-feed) | 4099:5230 |
Button card mini-labels (PDR, FTDT, SOAR) — I4099:5230;…;4735:13034 ×3 |
14px | #05273e |
| Home Screen (main-feed) | 4099:5230 |
Card-lg description — I4099:5230;4038:893;4116:6869;4033:1339 |
12px (Font Size/Body/M) |
#05273e |
Card Large (card-lg) |
4033:1436 |
Description body — 4033:1339 |
18px (Font Size/Body/M) |
#05273e |
Card Medium (card-md) |
4718:11125 |
Description body — 4718:11132 |
18px (Font Size/Body/M) |
#05273e |
Button Card (card-btn) |
4737:13038 |
Label text "United Airlines" — 4735:13034 |
18px (Font Size/Body/M) |
#05273e |
| List (listy card generic) | 5172:15482 |
Subtitle "Fri April 24" — 5172:15473 |
18px (Font Size/Body/M) |
#05273e |
Notification Card (card-notification) |
4864:31901 |
Body text — 4864:31878 |
20px (Font Size/Body/L) |
#05273e |
| Carousel Scroll Indicator (Slider) | 4317:5239 |
Range labels & value display — 4317:5220, 4317:5231, 4317:5237, 4317:5238 |
16px | #05273e |
Node list generated 2026-06-26 via Figma MCP sweep of all master component nodes. 6 node IDs returned "invalid" errors (moved/removed from file): 5452:679, 4864:32242, 5515:55, 4038:221, 5452:434, 5515:52 — excluded above. Run sweep again after any Figma revision to keep current.
Why it matters: Missing weight files at the ALPAMobile.Presentation font-bundling pass mean those text layers will silently fall back to a system font or the nearest available weight, breaking visual fidelity. This needs to be resolved before the font-bundling pass (post #2087).
Tentative answer (2026-06-22): Sticking with Futura PT as the single body font family across all weights. Futura PT Medium, Demi, and Heavy .otf files are expected to be provided alongside Book and Bold before the ALPAMobile.Presentation font-bundling pass. No fallback substitutions planned — the full weight set will be bundled. Confirm file delivery before implementation begins.
Correction (2026-07-23, José, via AB#2358): The above is superseded — only two weights are being sourced: Futura (Regular) and Futura Book. Medium, Demi, Heavy, and Bold are confirmed not coming. As of this correction, zero Futura/Futura PT font files of any weight exist in the repository (this doc's "Book and Bold .otf files in hand" claim above could not be verified against the codebase — worth confirming with whoever holds them, since they aren't committed anywhere the app can bundle). Figma nodes above needing Medium/Demi/Heavy/Bold will need an explicit fallback decision (nearest-available weight, or CSS-synthesized bold) rather than the "full weight set, no substitutions" plan this answer assumed. See DQ-8 for the heading-font resolution built on this corrected scope.
Affected specs: design-tokens (Typography section), Flight Card, button-group. Deferred to the ALPAMobile.Presentation font-bundling pass.
What we found: The 2026-06-17 Figma revision introduced or updated several components whose exact height values were not extracted during the sync pass. Widths are confirmed; heights were marked TBD pending a dedicated sizing deep-dive.
| Component | Width | Height (resolved) | Source |
|---|---|---|---|
card-button Default | 353 px content / 393 px viewport | 189 px ✓ | Live Figma node 4074:4624 |
card-button Variant2 | 353 px | 129 px ✓ | Live Figma node 4104:5452 |
flight card-og Default / Swipe | 353 px | 164 px ✓ | Live Figma node 4713:19868 |
flight card-og Expanded | 353 px | 1361 px ✓ | Live Figma node 4713:19985 |
flight card-recent searches | 353 px | 115 px ✓ | Live Figma node 4450:11208 |
card-duty period Default | 345 px | 238 px ✓ | Live Figma node 4596:11976 |
card-duty period Variant2 | 345 px | 1010 px ✓ | Live Figma node 4601:12136 |
navBar-bottom-tablet | 800 px ✓ | TBD (not in live Figma metadata) | .fig binary 5452:267; tablet deferred (D12) |
flight card-TABLET | TBD | TBD | Deferred (D12) |
Why it matters: Heights are required before scaffold ViewModel layout constraints can be written and before MAUI HeightRequest decisions can be made for the native-XAML surface of ALPAMobile.Presentation.
Answer: Sizing pass completed 2026-06-22 via get_metadata on live Figma file. All phone-scope components confirmed. Tablet components remain deferred (D12). navBar-bottom-tablet height is the only remaining TBD — it was not discoverable in the live Figma page metadata; tablet scope is deferred anyway. Component Dimensions table in design-tokens.html and button-card spec page updated with confirmed values.
What we found: The 2026-06-17 Figma revision introduced topNav-og alongside an existing topNav entry in figma-component-specs.json. Both entries share the same dimensions (393×98 px), matching the preliminary analysis.
Confirmation (2026-06-22): get_metadata run on both nodes via the live Figma API:
| Entry | Node ID (.fig) | Width | Height |
|---|---|---|---|
topNav | 4864:32242 | 393 px | 98 px |
topNav-og | 4153:3892 | 393 px | 98 px |
Both are 393×98 px. topNav-og is the canonical master (the -og pattern follows D15). The plain topNav entry is a stale reference. The figma-component-specs.json inventory retains both entries for traceability; active work references topNav-og. No rename decision required — this follows the already-established D15 pattern.
Answer: Confirmed identical at 393×98 px. topNav-og is authoritative. No further action required; topNav entry in the JSON is a stale alias (same resolution pattern as D15 for flight segment card).
What we found: The MEC theming token architecture is fully documented in design-tokens.html — the theme token set is the per-MEC customization layer, and the United Airlines palette is the Figma example. The token paths were confirmed; hex values were blank pending a variable read.
Answer (2026-06-22): All five United Brand palette hex values confirmed via get_variable_defs on the MEC frame node 4684:7914 in the Figma app page:
| Token Path | Hex | Swatch | Role |
|---|---|---|---|
United Brand/Blue-800 | #002243 | Nav bar background — replaces Surface/Brand | |
United Brand/Blue-600 | #001262 | Active states, hover | |
United Brand/Blue-500 | #0008ce | Buttons, borders — replaces Border/Brand | |
United Brand/Blue-400 | #94ebfe | Links, CTA accent — replaces Accents/Blue | |
United Brand/Grey-100 | #f7f7f7 | Surface backgrounds, dividers |
The MEC palette table in design-tokens.html has been updated with confirmed hex values and inline swatches. The same call also confirmed font families for DQ-8: heading = Lora (SemiBold 600), eyebrow = Inter.
What we found: DQ-1 confirmed Futura PT as the body font. The DQ-7 variable read (2026-06-22) confirmed the Figma tokens bound Lora SemiBold (heading) and Inter Bold (eyebrow) as separate families.
| Token | Role | Figma family | Resolution |
|---|---|---|---|
Font Family/Heading | Card headers, hero titles, pilot card, MEC card title | Lora SemiBold (600) | Normalized to Futura PT |
Font Family/Eyebrow | Eyebrow labels, section headers, date stamps | Inter Bold (700) | Unchanged — out of scope for this decision |
Resolved 2026-07-23 (José): Heading normalizes to Futura PT — Lora is no longer the base heading font. Eyebrow (Inter) was not part of this confirmation and stays as-is pending a separate decision. Only two Futura weights are being sourced: Futura (Regular) and Futura Book — see the corrected scope note on DQ-4, which supersedes that question's "full weight range" tentative answer.
Implementation (AB#2358): --font-heading-family in alpa-components.css updated from the Lora stack to the Futura PT / Futura fallback chain (matching --font-body-family). Separately, --alpa-heading / --alpa-body were found to duplicate literal font stacks instead of referencing var(--font-heading-family) / var(--font-body-family) — meaning a MEC theme's Font/Heading / Font/Body override (injected via BlazorThemeService → alpa-theme.js → #mec-theme) never reached any component. Fixed to bridge through the semantic tokens, matching the existing --alpa-navy: var(--surface-brand) color-alias pattern. No physical Futura PT font files exist in the repo yet (base or MEC) — rendering currently relies on iOS's built-in system "Futura" via the CSS fallback chain until the licensed files are sourced and registered via @font-face (Blazor) / AddFont() (MAUI XAML).
MEC scope: Documented per-MEC brand fonts (UAL = LeagueSpartan, DAL heading = BebasNeue / body = LeagueSpartan, FDX = TBD per DQ-14) remain the design target and are unchanged by this decision — those come from decision D33. None of those files exist either, so every MEC theme (e.g. theme.UAL.json) renders the interim Futura PT value until its real brand font is delivered.
Verified live (2026-07-23): iOS simulator, /home-preview against live production data. Confirmed via getComputedStyle before and after, not just visual inspection — --font-heading-family moved from "Lora", Georgia, "Times New Roman", serif to "Futura PT", "Futura", -apple-system, "Segoe UI", Roboto, sans-serif, and --alpa-heading (what components actually consume) now resolves the same way, proving the token bridge works end to end.


CardViewModel sufficient or needs FlightSegmentCardViewModel?Decision: Option B — FlightSegmentCardViewModel : CardViewModel (recorded as D16 in the Decisions Record).
Field inventory vs CardViewModel:
| Required field | In CardViewModel? | Option A mapping |
|---|---|---|
Origin (IATA code) | No | Concatenate into Header or ContentText |
Destination (IATA code) | No | Same blob |
DepartureTime | No | Same blob |
ArrivalTime | No | Same blob |
Duration | No | Same blob |
FlightNumber | No | Same blob |
AircraftType | No | Same blob |
StatusBadge (text + color) | No | Cannot represent color in a plain string |
Why Option A fails the threshold: 8 fields must be independently rendered in discrete visual slots (origin/destination city codes, separate time row, leg detail strip, status badge). No fewer than 6–7 fields would have to be concatenated into ContentText — well above the 3–4 concatenation threshold. StatusBadge additionally requires a color value that has no representation in a string property. Type safety in the factory and clarity in the XAML bindings both require typed properties.
Answer: Option B adopted as D16. FlightSegmentCardViewModel : CardViewModel was initially the 10th scaffold component. Amended by D17: Flight Card demoted to Track B (typed domain component — FlightCardView : ContentView with 8 BindableProperty fields). The scaffold ViewModel is no longer used; direct property binding replaces the factory. Scaffold count: 10 → 9. Spec page: flight-segment-card-component.html (Track B display-data reference).
Updated specs: domain-controls, architecture, index, decisions record.
flight detail flight info: new scaffold component or extend FlightSegmentCardViewModel?What we found (2026-06-22 FTDT exploration): The Figma symbol flight detail flight info (node 4325:6399, 297×154 px) appears in two places — inside the expanded flight card-og detail panel, and on FTDT/Jumpseat detail pages. It shows a single flight leg from one direction (Departure OR Arrival), not the paired A→B route of the Flight Card.
| Field | FlightSegmentCardViewModel | flight detail flight info |
|---|---|---|
Origin | ✓ (e.g. "ORD") | ✓ (e.g. "DEN") |
Destination | ✓ (e.g. "SFO") | ✗ (not shown — one-directional) |
DepartureTime | ✓ | ✓ (e.g. "12:00") |
ArrivalTime | ✓ | ✗ |
Duration | ✓ ("4h 30m") | ✗ |
FlightNumber | ✓ | ✓ (e.g. "DL 302") |
AircraftType | ✓ ("Boeing 737") | ✓ (model number, e.g. "321") |
StatusBadge | ✓ ("ON TIME" / "DELAYED") | ✓ — richer: includes delay duration ("Delayed 34m") |
Gate | ✗ | ✓ (e.g. "Gate H14") — new field |
ActionLinks | ✗ | ✓ KCM / Jumpseat Policy — new field |
DirectionLabel | ✗ | ✓ "Departure" or "Arrival" — new field |
UpdatedTimestamp | ✗ | ✓ (e.g. "Updated April 21, 9:30am") — new field |
(1) Is flight detail flight info a new scaffold component (e.g. FlightLegInfoViewModel) with its own typed properties (Gate, ActionLinks, DirectionLabel, UpdatedTimestamp), or should it be a richer variant of FlightSegmentCardViewModel with optional additional fields? (2) Are Gate and ActionLinks (KCM, Jumpseat Policy) available from the existing Flight/Leg domain model or do they come from a separate API? (3) Is the delay duration in the StatusBadge derived from the same data as the Flight Card's status, or is it a separate field?
Initial assessment: The 4 unique fields (Gate, ActionLinks, DirectionLabel, UpdatedTimestamp) and the fundamentally different layout intent (single-directional leg detail vs paired-route card) suggest a new scaffold component. The overlap is 5 shared fields out of 12 total — below the reuse threshold established by DQ-9. However, the final call depends on whether Gate/ActionLinks come from the same data fetch as the flight card (which could push toward extension) or a separate detail API (which confirms a separate component and factory). See D15 precedent: one domain control, multiple Figma masters — the same could apply if it's one factory producing different scaffold types.
Design answer: Track B typed ContentView — FlightLegInfoView : ContentView with BindableProperty fields. The 4 unique fields (Gate, ActionLinks, DirectionLabel, UpdatedTimestamp) and the fundamentally different layout intent confirm this is not an extension of the flight card. Decided by D17 (two-track component pattern, 2026-06-22). See D17.
card-duty period-alt: separate FlightSegmentRowViewModel or embedded in the Duty Period template?What we found (2026-06-22 FTDT exploration): When card-duty period-alt is expanded (Variant2, 345×1051 px), the "MANAGE FLIGHTS & DETAILS" panel contains inline flight segment rows. These rows show a 3-line format per segment:
→ Destination with a plane icon (e.g. "DEN ——✈—— MIA")These instances are named flight detail flight info in Figma and are constrained to ~178 px wide by the duty period card container. Their layout is bidirectional (Origin AND Destination) — different from the standalone flight detail flight info symbol (DQ-10) which is one-directional. Fields visible: Origin, Destination, FlightNumber, AircraftType, FlightTime/Duration — 5 fields, no StatusBadge.
(1) Should the flight rows inside a duty period card be a reuse of FlightSegmentCardViewModel rendered in a compact/embedded variant (controlled by a ConstrainedWidth or Mode property), or should DutyPeriodCardViewModel own an inner collection with its own row template? (2) Does the Duty Period CRUD model (DutyPeriodExpanded) expose segment sub-entities with the needed fields (Origin, Destination, FlightNumber, AircraftType, FlightTime)?
Initial assessment: Two viable options — (A) DutyPeriodCardViewModel owns an IEnumerable<FlightSegmentSummary> collection of lightweight row objects (inline, no separate scaffold component); or (B) reuse FlightSegmentCardViewModel with a compact rendering mode. Option A is simpler and keeps the flight segment rows as internal detail of the duty period card. Option B enables shared XAML templates but requires adding a compact-mode discriminator to FlightSegmentCardViewModel. The 5-field, no-StatusBadge subset and the 178 px constrained width favor Option A (lightweight inline rows rather than a full scaffold component).
Why it matters: determines the implementation boundary of DutyPeriodCardView and whether sub-factories are needed.
Design answer: Option A — compact flight rows are internal to DutyPeriodCardView. The 5-field, no-StatusBadge subset and 178 px constrained width do not warrant a separate scaffold component. DutyPeriodCardView owns an inner collection (IEnumerable<FlightSegmentSummary> or equivalent lightweight row type) rendered via its own embedded template. No sub-factory needed. Decided by D17 (two-track component pattern, 2026-06-22). See D17.
Context: The dynamic feed loads a PageDto response. The app has a HomeSkeleton.razor / ComponentSkeleton.razor scaffold. The update strategy has been partially discussed but the skeleton UX has not been designed.
Agreed approach: client passes a since date; server returns the current PageDto (or the subset of containers updated since that date). Client compares the response against its cached state by id and resolves adds, removes, updates, and reorders itself. Server stays stateless — no diff computation, no client-state tracking required.
Flow:
| Scenario | Client behaviour | API call |
|---|---|---|
| First load / cold start | No cache — show skeleton, fetch full PageDto, render and store with fetch timestamp. |
GET /page/{pageId} |
| Return visit (unchanged) | Render from cache immediately; background refresh returns 304. | GET /page/{pageId}?since={lastFetched} → 304 |
| Return visit (content updated) | Render from cache; new payload arrives; client diffs by id — inserts added containers, removes missing ones, updates changed ones, reorders as needed. |
GET /page/{pageId}?since={lastFetched} → PageDto |
(1) Please provide Figma frames for the initial load skeleton (full-page, cold start). (2) Should in-place container updates (delta refresh) be visually silent, or should they animate in? (3) Do container skeletons vary by type (carousel skeleton vs grid skeleton vs stack skeleton), or is there one generic placeholder shape? (4) Is there a visual treatment for "content just updated" after a delta arrives (e.g. a brief flash or no visual signal)?
Engineering dependency (Option A): The backend needs to track a page-level or container-level last-modified timestamp so it can respond correctly to ?since= queries — returning only updated containers or a 304. The client already has id on every container and item (established in the API contract), which is the reconciliation key. No server-side diff computation required.
Design answer: — pending skeleton frames and animation decision —
Cross-reference: dynamic-feed-api-contract.html §9 Open Items (incremental page updates).
What we found: The Figma file (gpO3masyyNNHxjvRtdcRo7) has no spacing variables defined. The get_variable_defs call on the UAL home frame (node 20716:1802) returned empty. All spacing values in the current component spec are inferred or placeholder — none are sourced from Figma.
Figma references (base design — no spacing annotations yet): Home screen (node 4099:5230) · Carousel container (node 4317:5239) · card-lg (node 4033:1436) · card-md (node 4718:11125). These frames are the reference points for measuring spacing manually if variables cannot be added to the Figma file in time.
Why it matters: Spacing is a frontend concern — the frontend resolves it using its existing MEC context, so the backend does not need to carry it. But the frontend needs a canonical source of truth for the spacing scale and per-component usage before implementation. Without Figma annotations, any values baked into component styles will be guesses.
Please add spacing variable definitions and per-component annotations to the Figma file. Specifically needed:
Spacing/XSmall = 12px, Spacing/Small = 16px, etc.) as Figma variables so they can be read programmatically.Tentative answer (2026-06-26): Auto-layout inference sweep across 9 component nodes confirmed a strict 4 px grid. The scale below is implementation-ready for all components except Flight Card and Notification Card (3 outliers still need design confirmation). UAL MEC uses identical spacing — no MEC delta.
| Token | Value | Key usage |
|---|---|---|
Spacing/XTiny | 4 px | Button card body gap, inline icon gaps |
Spacing/Tiny | 8 px | Dominant step (14×) — card top padding, card-btn outer, navBar-bottom, carousel gap |
Spacing/XXSmall | 12 px | card-lg / card-md content item gap, button icon-text gap |
Spacing/Small | 16 px | Page margin (--page-margins), nav item py, card inner padding |
Spacing/Medium | 20 px | Notification card outer gap |
Spacing/Large | 24 px | card-lg bottom padding, notification outer padding |
Resolved (2026-06-30): The foundations reconciliation located the canonical Figma spacing scale (named 14-step scale, node 117:176 — xtiny 4 · tiny 8 · xxs 12 · xs 16 · small 20 · medium 24 · large 28 …), which supersedes this question's original "no spacing variables" premise. The 3 outliers are confirmed off that grid and are resolved by the consistency rule below; exact px confirmation rides on the reconciliation values pull (decision D-S1).
| Outlier | Lives in | Reusable? | Resolution |
|---|---|---|---|
| 5 px — notification text gap (4864:31875) | card-notification (Notifications + Comms) | Yes | Normalize → 4 px (Spacing/XTiny) |
| 10 px — flight-card section / route gaps (4555:7483, 4555:7000) | Flight Card (FTDT + Flight Finder) | Yes | Normalize → 12 px (Spacing/XXSmall) — conform to the card content-gap convention / round up |
| 18 px — bottom-nav inter-tab gap (5035:16370) | BottomNav (app chrome) | Yes (layout) | Flex-distributed — a computed result of even tab distribution (justify-content), not a spacing token. No 18 px token created; to be annotated in design-tokens' Spacing section under D-S1. |
Normalizations are decisions recorded here; the live token / CSS change is applied under D-S1 alignment (gated — must not shift shipped mock layouts). Full scale in design-tokens.html — Spacing & Layout Tokens. Cross-reference: dynamic-feed-api-contract.html §9 (spacing token strategy).
What we found: The Figma MEC screens use per-airline typefaces in addition to the base Futura PT. From the design captures so far:
Why it matters — bold requires a separate font file. Each font weight is a distinct binary file. The browser/rendering engine cannot generate a true bold from a single Regular file — it will apply synthetic bold (algorithmically thickened strokes) which looks noticeably worse, especially at display sizes. For a professional app this is not acceptable.
We currently have two purchased weights for Futura PT: FuturaPT-Book.otf (Book/400) and FuturaPT-Bold.otf (Bold/700), with matching .ttf files registered natively in MauiProgram.cs. In Futura PT, "Book" is the regular weight — the family has no separate face named "Regular", so Book and Regular are two names for the same 400 file, not two files. Weights between the two do not all resolve the same way: Medium (500) resolves to Book 400, while Demi/SemiBold (600) resolves to Bold 700 — CSS font matching rounds down below 500 and up at 600 and above. Both are real purchased faces, so neither is synthesised.
For the MEC airline fonts (League Spartan, Bebas Neue, etc.), we have no purchased files yet. Before implementation can begin on any MEC screen, the correct font files must be procured — one file per weight used in the design.
font-weight: 400 and font-weight: 700 in CSS, with no intermediate weights that could fall back to the wrong file. If the design currently uses Medium or SemiBold, can those be rounded to Regular or Bold?Partial update (2026-06-26):
| Airline | Typeface(s) confirmed | Font files | Status |
|---|---|---|---|
| UAL | League Spartan (Regular 400 + Bold 700 per D33) | ual.alpa.org/Admin/File-Management → ALPA App > Branding > Fonts | Confirmed |
| DAL | Bebas Neue (headings/section labels) + League Spartan (body/card text) | Not yet located — pending design team delivery | Partial |
| FDX | Not yet confirmed from Figma capture | — | Open |
Questions 1–4 above still apply for each airline not yet at Confirmed. Full resolution is a procurement blocker for DAL and FDX implementation.
FDX re-checked 2026-07-06: the MEC Figma file was edited again on 2026-06-30, so the FDX page was re-scanned in full (every fill color and font on the page). Result: still no confirmed FDX typeface — the page shows a wide generic mix (League Spartan, Lora, Raleway, Futura PT, Futura, SF Pro, Merriweather, Inter) with nothing that reads as an intentional FDX choice, and zero FDX brand colors either. More Figma activity has not translated into FDX progress — this needs a direct design-team nudge, not another automated re-check.
Resolved 2026-07-29 — weight policy settled:
The base design system uses Regular/Book only. The Medium/Demi/Heavy Futura PT set previously assumed as forthcoming (see the tentative DQ-4 / DQ-8 note in naming-decisions-record.html) is not being procured. Design has accepted Regular/Book as the base text weight everywhere, which removes the licensing question for intermediate weights entirely.
The single exception is MEC customisation, which is a featured capability. A per-MEC theme may supply its own licensed typefaces and weights — that is the point of the MEC branding layer, and it is why the UAL League Spartan Regular 400 + Bold 700 pairing above remains valid. Those overrides ride on the MEC theme tokens (D33), not on base component rules.
Practical effect on the codebase: any base rule specifying an intermediate weight (500 / 600 / 800) has no real face of its own. .alpa-list-headline was corrected to font-weight: 400 under AB#2358. The remaining intermediate-weight declarations have not yet been swept — measured 2026-07-30: alpa-components.css 52 × 600, 16 × 500, 3 × 800; ftdt.css 36; widget.css 2 — 109 in total. Declarations at 400 and 700 are unaffected, since both files are purchased.
This entry previously stated that intermediate weights "would render synthesised" and that the sweep "is a visible change wherever a real Bold face is currently being selected". Both are incorrect. Measured on device (iOS simulator, Blazor WebView) by rendering identical text at each weight in Futura PT and comparing widths:
| Requested weight | Rendered width | Face actually used |
|---|---|---|
| 300 · 400 · 500 | 516.960 px | Book 400 — a real face |
| 600 · 700 · 800 · 900 | 579.400 px | Bold 700 — a real face |
No synthesis occurs: CSS font matching selects a real purchased face in every case. Normalising 500→400 and 600/800→700 therefore selects the same face that is already being drawn — the sweep is visually null and does not require per-component design sign-off. It makes the stylesheet state what already renders. See Typography Font Mapping § 3.1.
League Spartan is not MEC-only (2026-07-30). This entry frames League Spartan as per-airline MEC branding (UAL). The 2026-07-30 census found Other/Button = League Spartan Regular 400 in the base design system of the working file owEYzHf7FrHRvWC2u82UOl — the button style for button pill, tab group, card-button, profile and the saved/recent searches card. No League Spartan file ships, so those buttons render Futura PT today. Under the two-file direction that is the correct outcome, but the base-level usage is a separate fact from the MEC branding layer. See Typography Font Mapping § 2.
Raised 2026-06-26. Weight policy resolved 2026-07-29. Related: D33 (font-family as MEC theme token — pending). Figma references: UAL home node 20716:1802 · DAL home node 21247:2989 · FDX home node 21010:2758.
What we found: The DAL home screen (node 21247:2989) uses two distinct dark navies on the same screen:
#002a50 (Delta brand navy, the primary)#09243d (noticeably darker, a different shade)2026-07-06 update — pixel-confirmed via rendered screenshot (not just raw Figma fills), extends the pattern beyond the original two elements:
#002a50 also appears on the Contract Comparison featured content card, not just the Welcome card.#09243d-family (sampled #0b2443, same shade within rendering tolerance) also appears on the TOOLS section cards (Scheduling Ticketing System, DART), not just the NAVIGATE grid.#05273e — the base ALPA navy, neither DAL candidate. If navBar-bottom is meant to always track Surface/Brand (the rule confirmed for UAL), this screen's nav bar doesn't yet reflect that for DAL — worth asking whether this mock is simply unfinished there, or whether DAL's nav bar intentionally stays unbranded.#c01933 — close to, but distinct from, ALPA's own base "red" (#c02126, used elsewhere in this doc suite) and from Delta's real-world corporate red (#e31837). Not yet confirmed whether this is a genuine DAL-specific accent or an unrelated near-shade.Hypothesis: This may be an intentional primary/secondary brand surface pattern — e.g. Surface/Brand = #002a50 (primary, used for header/featured-content surfaces) and a second token (e.g. Surface/Brand-Dark or Surface/Brand-Secondary) = #09243d for quick-access/tool card surfaces. If so, this is a new token slot that doesn't exist in the current theme endpoint contract and needs to be added.
Alternatively it may be a draft inconsistency — the ButtonCard grid was painted with a slightly wrong navy and should be #002a50 to match the welcome card. The now-broader, more consistent pattern (extending to TOOLS and Contract Comparison) makes this less likely than originally thought, but doesn't rule it out.
We currently have no documented primary/secondary dark-brand surface pattern in the token set. The existing Surface/Alt token is light grey (#efefef) — an elevated card surface, not a secondary brand dark.
On the DAL home screen (node 21247:2989), #002a50 appears on the Welcome card and the Contract Comparison featured card, while #09243d appears on the NAVIGATE and TOOLS quick-access cards. Is this a real two-surface pattern?
Surface/Brand-Dark) so we can add it to the theme endpoint contract and spec it as a distinct override slot for DAL (and potentially other MECs).#002a50 to match the welcome/featured-content surfaces.navBar-bottom also carry a DAL brand color on this screen? It currently renders as base ALPA navy (#05273e), not either DAL candidate.#c01933 (the heart/favorite fill on this screen) a genuine DAL-specific red, or just this mock's local choice? It doesn't match Delta's real corporate red (#e31837).Design answer: — pending —
Raised 2026-06-26. Figma node: DAL home 21247:2989. Pixel-sampling pass 2026-07-06 (screenshot-confirmed, extends context). Blocks DAL theme token spec (D35 pending).
What we found: Several components accept a remote Image URL from the API at runtime:
CardHeroViewModel.Image) — 200 px tall editorial photo slot · Figma node 4033:1436CardTextViewModel.Image) — optional content image · Figma node 4718:11125ButtonViewModel.Image) — icon or thumbnail for feature buttons · Figma node 4737:13038PilotCard.AvatarUrl) — profile photo (currently falls back to fa-circle-user; confirm this is the intended fallback or provide a branded alternative)CardSmallViewModel.Image) — compact content card image slot · node not yet confirmed from Figma capture
None of these have a defined fallback state in Figma. The Figma designs always show an image present.
We do not currently have bundled placeholder assets for any of these slots. The pilot avatar currently
falls back to fa-circle-user (icon-based), but the card image slots have no equivalent
treatment defined.
Why it matters: Without a fallback, a missing or slow-loading remote image leaves
a blank or broken image box visible to the user. This needs a design decision before the ALPAMobile.Presentation
is built — either a neutral branded placeholder graphic (e.g. a lightly-tinted ALPA mark or grey
rectangle with icon) or an icon-based treatment, per slot type. The assets (if graphical) must be
delivered via the SharePoint folder alongside other UI Refresh assets.
What should display in each image slot when the remote URL is missing or fails to load?
fa-circle-user; confirm or provide a branded alternative.If a graphical placeholder is chosen, please provide the SVG/PNG export in the SharePoint asset folder (see Asset Inventory → SharePoint Delivery Checklist).
Design answer: — pending —
Raised 2026-06-26. Affects Hero Card, Text Card, Small Card, Button Card, Pilot Card. Blocks the ALPAMobile.Presentation image-slot implementation.
What we found (from the AB#1821 scaffold):
<svg> paths repeated in every Razor page — not glyphs from the design's icon set.
They are visual approximations and do not match the Figma icons.materialdesignicons-webfont.ttf and fa-solid-900.ttf —
live in Resources/Fonts/ for native MAUI (XAML) use only. The Blazor
BlazorWebView cannot see them: they are not copied to wwwroot/fonts/ and there is
no @font-face declaration or <link> in wwwroot/index.html
referencing an icon font.Why it matters: the tab bar is on every screen, so an icon mismatch is the single most visible fidelity gap in the scaffold. It also blocks a clean implementation: hand-drawn SVGs cannot track the design system if the icon set is revised, and they make MEC theming of the tab bar (per-airline icon treatment) impractical.
@font-face into wwwroot and reference glyphs by class), or
(b) SVG exports of the canonical icon set.Proposed interim workaround: export the 5 tab-bar icon assets
(plus the high-frequency in-app icons) from Figma to wwwroot/images/icons/ as SVG/PNG
and reference them directly, replacing the hand-drawn paths — a temporary measure until the icon font is
confirmed and wired into the WebView. This unblocks visual accuracy without committing to a font-licensing
decision. (Tracked alongside the scaffold; see Asset Inventory.)
3:7, not FontAwesome/Material). ~29 were exported
as SVG and the BottomNav tab bar now references the real Figma vectors (PR #1664) — the hand-drawn
approximations are being retired. We are "limping along" on these vectors for now. Catalogued in
asset-inventory.@font-face, glyph-by-class, MEC-themeable). The interim vectors do not retire this.
Licensing + wiring remain to be decided — this is the open part of DQ-17.3:7, section
"General Use Icons" 233:302) as the canonical icon family, delivered as
SVG exports to wwwroot/images/icons/ and rendered via the CSS
mask-image technique — MEC-themeable through background-color, so per-airline
icon tinting needs no additional assets. The icon-font @font-face track is
deferred, not abandoned — revisit when icon count or theming pressure demands it
(that forward capability stays on the books, but it should no longer gate implementation).
Licensing: none needed — these are ALPA-authored vectors. The bundled
fa-solid-900.ttf / materialdesignicons-webfont.ttf are
native-XAML-only and irrelevant to the WebView, so no web-font license question arises.
Design's sign-off here is a yes/no on the vector set as canonical; the interim wiring (PR #1664) already
matches the proposal.
Design answer (icon-font path): — pending —
Raised 2026-06-29 (AB#1821 scaffold accuracy pass); reframed 2026-06-29 to the dual interim-vector / forward-font strategy. Affects Page Template bottom nav and every screen. Related: DQ-4 (Futura PT weight gap — text fonts, distinct from icon fonts).
What we found: The Page Template spec
documents the navBar-bottom container (bg #05273e, px-16px,
pb-8px) and the five tab items, but there is no annotation in Figma for what the
selected/active tab looks like — no highlighted color, underline, pill, icon-fill, or
label-weight change is specified.
The reusable BottomNav component (built during the Option B chrome work, AB#2190) derives
the active tab from the current route and needs a defined selected state. The scaffold currently
improvises: the active item's label and icon switch to ALPA blue
(.alpa-nav-item--active → color/fill: var(--alpa-blue)) while inactive items stay
white. This is a placeholder, not a confirmed design.
What is the canonical selected-tab treatment for the bottom navigation?
Findings (2026-06-30, two sources):
navBar-bottom (Figma KPK3kvOU1zPHWIovXPB6Wo, node 4099:5232, Home screen) renders all five tabs identically: white labels (#ffffff) + light-blue icons (#c7dff6) on the navy (#05273e) bar — the Home tab is not differentiated (no highlight, indicator, pill, or opacity change). The active state is presumably a NavItem State=active variant not applied in this static frame.#c7dff6 icon; the scaffold improvises a dark-blue #007bc2 active treatment, which is both the wrong base color and the lower-contrast choice (≈3.3:1 on navy — below AA for the label; less prominent than the white inactive tabs).Sharpened ask: the requirement says "highlighted" but the design doesn't show how. Design to apply/specify the NavItem active variant — which token, on icon and/or label, plus any indicator (top bar / pill / fill). Whatever is chosen must be more prominent than the white inactive tabs and meet AA on navy. Two near-term fixes regardless: (1) move the scaffold base icon to light-blue #c7dff6 to match design; (2) replace the dark-blue active placeholder.
Evidence recap: the canonical navBar-bottom
(component 4038:1037, instance 4099:5232) draws all five tabs
identically — white 12 px Futura PT Medium labels, #c7dff6 28 px icons,
navy #05273e bar. The active-state variant exists in intent (Teamwork Space 4173:
"active tab visually highlighted") but is not drawn — this is a design-side gap.
Candidate on navy #05273e | Contrast | Verdict |
|---|---|---|
White #ffffff label (inactive base) | 15.37:1 | Pass (AAA) |
Light-blue #c7dff6 icon (inactive base) | 11.21:1 | Pass (1.4.11) |
Scaffold active #007bc2 label | 3.38:1 | FAILS WCAG 1.4.3 — the 12 px label needs 4.5:1; also less prominent than inactive white |
Yellow #f7bb0d | 8.83:1 | Pass |
Key fact: color alone cannot compliantly differentiate active
from inactive — white↔#c7dff6 is only 1.37:1, and no on-navy candidate
reaches 3:1 against white — so a shape affordance is required regardless of color.
Proposal: active = yellow #f7bb0d
(--alpa-yellow / Border/Yellow token) on the icon plus a
2–3 px top indicator bar; the label stays white. Inactive = white label +
#c7dff6 icon, per the design base above.
Scaffold implementation notes to fix when DQ-18 lands:
.alpa-nav-item--active .alpa-nav-icon { fill: … } is dead —
fill does nothing on a mask-image span; the active icon actually stays white.#c7dff6 —
correct alongside the active-state change.Design answer: — active-state visual still pending; intent + base treatment captured 2026-06-30 (Teamwork Space 4173 + Figma KPK3kvOU1zPHWIovXPB6Wo) —
Raised 2026-06-29 (BottomNav component, AB#2190). Affects Page Template bottom nav and the BottomNav component. Related: DQ-17 (tab-bar icon font).
What we found:
Why it matters: sets the host architecture and the native/Blazor surface split — but the chrome abstraction makes it low-regret: start mixed and escalate to full-native if testers reject the Blazor feel, without discarding the component work.
Current leaning (J. Castro — to validate with design + eng, not decided): XAML-first Shell + native static content, Blazor for the dynamic-UI portion (data-driven feed/widgets), with full native XAML held as the ultimate fallback.
Design/eng answer: — pending —
Raised 2026-06-29. Related: D12 · D17 · AB#2190 (chrome/host + full-bleed) · AB#2191 (AppShell/MainLayout split) · AB#2087 (Presentation Extraction) · DQ-20.
What we found: Figma renders saved rows with swipe-to-reveal delete (i-trash, native idiom); the scaffold uses a persistent visible action column (♥ save · ↻ re-search; 🔔 alerts · ✕ delete).
Why it matters: swipe fits a single destructive action and is costly to replicate in a WebView; these cards need multiple, mostly non-destructive actions, and accessibility needs a visible alternative regardless.
Leaning / baseline: visible action column as the default (multi-action + a11y + WebView fit); swipe-to-delete deferred as an optional enhancement for the destructive delete only — or moot if this surface is promoted to native XAML per DQ-19 (where swipe becomes free/idiomatic).
Design answer: — pending —
Raised 2026-06-29 (first concrete instance of DQ-19). Related: DQ-19 · Saved Search Card · Flight Segment Card.
Yes — the app should honour the OS text-size setting. Design also confirmed Body/Body M (18px) is the page default, which had been the second half of the same ask.
The two answers interact: because text must scale, the base is declared in relative units rather than a fixed pixel value, so settling the default alone would have been rework.
Current state (audited 2026-07-30) — text cannot be enlarged at all. 320 font-size declarations are px with zero rem/em; text-size-adjust is unset; the viewport in index.html and home.html carries maximum-scale=1.0, user-scalable=no, so pinch-zoom is disabled too; and no base font size is declared anywhere, meaning unstyled text inherits the WebView's 16px. Together these fail WCAG 1.4.4 Resize Text, and user-scalable=no is itself a flagged anti-pattern.
Load-bearing constraint: iOS WKWebView does not apply Dynamic Type to web content automatically — converting to rem alone will not satisfy the answer. Either the font: -apple-system-body WebKit hook (iOS-only behaviour) or a native bridge that reads the OS scale and pushes it into the WebView is required. The bridge is recommended, since the app ships both iOS and Android heads. Scope, including the 90 fixed height declarations that are the 200%-reflow risk surface, is on AB#2391.
Full record: typography-font-mapping.html; the question as put to design is docs/requirements/design-question-typography.md.
What we found: the two render surfaces honour OS text scaling very differently:
Label.FontAutoScalingEnabled is on by default, so controls honour iOS Dynamic Type / Android font-scale automatically.Why it matters: text scaling is a baseline accessibility requirement; the WebView gap means it must be engineered, not assumed. It also bears on the DQ-19 surface decision (another "free in native, manual in WebView").
Recommended approach (proposed):
rem/em anchored to a root size (extends the D33 Font/Heading · Font/Body tokens with a size scale); surface-agnostic; do first.UITraitCollection.PreferredContentSizeCategory (iOS) / Configuration.FontScale (Android) natively, inject as a CSS variable (--text-scale) / root font-size, and re-apply on trait/config change (same pattern as the AB#2190 safe-area-on-rotation work). Clamp the multiplier so Larger Accessibility Sizes don't break layout.min-height, never fixed height; explicit wrap-vs-truncate per component. (The 393×852 scaffold's fixed heights must become flexible — a component-spec + CLAUDE.md CSS rule.)Design/eng answer: — pending —
Raised 2026-06-29 (#6 accessibility pass). Related: D33 (font-family tokens) · design tokens · DQ-19 (render surface) · AB#2087.
Withdrawn (2026-07-01): this question assumed a composite content-item id (ItemTypeId + prefixed ItemId, e.g. menu-123/doc-456/msg-789). That premise was retracted — the source table was erroneous. The real Content Contract has no composite key: content is a MenuItem (navigation) or a DocumentItem (content), served by the MobileContent API. No key-field / prefix-authority / namespace question remains.
Withdrawn (2026-07-01): premised on the retracted composite-key contract — there is no /api/content/resolve batch resolver. Content is fetched by Scope/Category (documents, via DataManager.GetDocumentsForScope…Async → IEnumerable<DocumentItem>) or through the Mobile Menu (MenuItem navigation), served by the MobileContent API. See Content Contract §2/§5.
What we found: content types carry different fields, so not every type can back every component — a navigation entry has no description/date, so it can't back a full content card the way a content document can.
Resolved (2026-07-01): answered by the corrected Content Contract §4 component↔content-type mapping — MenuItem-backed navigation (card-sm, ButtonCard, TextCard) vs DocumentItem-backed content (card-lg/HeroCard, Carousel). There are exactly two content types; the earlier "message / msg-" type is dropped (retracted premise). TextCard's richer navigation display is covered by MenuItem.Description (Change 2).
Raised 2026-06-30; resolved 2026-07-01. See content-endpoint-contract §4 · backend-api-mapping §2.
What we found (from the Figma source, live file): three text layers across the two newly-captured Flight Finder components render in League Spartan instead of the app's base font. Per D33, League Spartan is a MEC-theme-only font (the UAL heading/body slot) — base (non-MEC) app text should be Futura PT Book/Bold, same rule DQ-1 already established for the "Futura LT Pro" override.
expander-flight finder-advanced search component set (node 4361:5537), Underline Button/Text layer, both states:
slot/expanded wrapper/Secondary button/Text in both the Default and Open states. This is an instance-override text node (no stable standalone node id to deep-link directly) — open the component set link above and expand the "Secondary button" nested instance in each state.search secondary buttons "Recent" label — component set (node 4381:10609), both variants:
main-feed slot/Primary button/btn with point/btn/LOGIN. Same pattern as the "Save Search" bullet above — an instance-override text node, no independent node id. Implementation used the existing ActionButton/.alpa-btn component as-is (Futura PT, per its established base-app rendering) rather than replicate the override.
Figma reference: base design — file owEYzHf7FrHRvWC2u82UOl (not the MEC customization file; see the file reference legend above). All five links open with Dev Mode inspector active.
Is League Spartan intentional here — a deliberate distinct "Button Label" type style separate from body copy — or is it an unintentional override that should be cleared so these layers inherit the base Font Family/Body (Futura PT) token, per the DQ-1 precedent?
Found during the 2026-07-02 Flight Finder scoped sync (D41–D45). Flagged (not fixed) in design-tokens.html. Affected specs: flight-finder-advanced-search, search-secondary-button.
Context: the current shipped implementation renders the page banner as one full-bleed image (PageBanner.FeaturedImageLink) with all text baked into the pixels. Backend Change 5 proposes additive Title/Subtitle/LogoImageLink fields so the client renders real text instead.
Evidence for structured intent (from the D43 capture, naming-decisions-record): the Figma master banner (component_set 5512:12925) is built structured, not as a flat image:
#05273e fill rather than a background photo.Is that structure a requirement (MEC theming of the fill, accessibility of real text, client-formatted expiry dates) or incidental to how the mock was drawn — i.e. would a supplied flat image per campaign be adequate? Note: Dev-Mode annotations on the banner node have not been reviewed (no .fig export available) — if annotations exist for 5512:12925 they should be checked as part of answering this.
Consequence: backend Change 5 is ON HOLD pending this answer — if a flat image is adequate, Change 5 is withdrawn; if structure is required, Change 5 proceeds as documented. See the Backend Handoff Tracker.
Context: the document-list screens (Internal Comms 4565:9260 → ALP Mag 4867:34739,
file R6hPjBdjSIn6946qTJasUC) render their rows as card-notification instances showing
date · summary · chevron — no visible unread indicator. But the same component's Notifications usage
documents an unread element mapped to NotificationCenterMessage.HasNotBeenRead
(notifications-screen-data-gap §2), and the legacy native documents
list has always shown one: UnreadListItemViewModel.IsStatusIndicatorVisible ←
DocumentItem.Read, with mark-read-on-tap and mark-all-read-on-leave.
Current implementation (2026-07-09): the Blazor /documents page keeps legacy
parity — an unread dot per row driven by DocumentItem.Read, with the same read lifecycle; the row's
accent line renders in the navy/blue shade shown in the design regardless of state.
Should document lists surface read/unread state at all in the redesign? If yes, which affordance:
(a) the dot, as the Notifications inbox does; or (b) the suggestion on the table (J. Castro, 2026-07-09):
signal unread via the row's accent line color — a shade of red for unread (e.g. the
existing --color-error #c02126
family), returning to the current blue/navy shade once read — no dot needed. Note: Dev-Mode annotations on
4867:34739 / the card-notification master have not been reviewed (no .fig export
available) — check them as part of answering.
Consequence: if (b) is adopted it
needs a token decision (which red) plus a card-notification/Card spec update, and the unread treatment should be
unified across Notifications and document lists rather than diverging per screen.
Context: the Figma screen compositions (file R6hPjBdjSIn6946qTJasUC) are drawn as
fixed frames with no allowance for iOS platform chrome: nothing reserves the status bar / Dynamic Island
area at the top (~59px on Pro Max hardware) or the home-indicator gesture zone at the bottom (~34px).
The Blazor WebView runs full-bleed (viewport-fit=cover in wwwroot/index.html),
so any element the design places at a screen edge lands under that chrome unless the implementation compensates.
Implementation policy adopted (2026-07-10): safe-area insets are owned by the page shells,
not by individual components — every shell pads with env(safe-area-inset-top/bottom) and keeps the design's
dimension as the fallback/floor, so Figma-spec'd components themselves stay untouched:
.alpa-statusbar (AlpaScreen spacer) and .alpa-top-nav (54px bar + inset);
.alpa-login-statusbar on the login shell; .ftdt-page padding on the FTDT sub-pages;
.dev-dash-header on the tester dashboard; .alpa-jsff-modal (Flight Finder full-screen filter)..alpa-bottom-nav (nav bar clearance); .ftdt-cta (sticky
Save/Conclude bar on FTDT form pages, which render no bottom nav); .dev-dash scroll-end padding;
.alpa-jsff-modal-actions.Audit note (2026-07-10): a full sweep of every @page component found two shells missing
bottom clearance — the FTDT sticky CTA and the tester dashboard — both fixed the same day. All 41 routes now flow through
an inset-aware shell (AlpaScreen ×29, login shell ×2, FTDT shell ×8, dev dashboard ×2).
navBar-bottom (AB#2536, 2026-08-22)A tester reported excessive navy below the tab-bar labels. Measuring it turned the general question above into a specific, numeric one, and the answer decides how every other bottom-anchored surface should behave.
What the node draws. navBar-bottom
5387:31928 is 393 × 88, positioned at y = 764 in an 852pt frame — flush to the
artboard's bottom edge. Inside it, slot is 337 × 80 at x = 28, y = 0,
so 8pt of bar is left beneath the content block. Each NavItem-primary is
53 × 80, five of them 18pt apart; the icon is 28 × 28 at y = 16 and the
label sits at y = 52, 12pt tall. That yields 24pt below the label — 16pt inside
the slot plus the 8pt beneath it.

What each phone actually reserves. Both values below were read out of the running app, not computed by us:
| Platform / device | Reserved | What occupies it | Channel |
|---|---|---|---|
| iOS 26.4 · iPhone 16 · 393 × 852 | 34pt | Home indicator plus Apple's margin around it | env(safe-area-inset-bottom) = 34, an Apple constant in portrait |
| Android 36 · Pixel 9 · 412 × 924 · gesture nav | 24dp | Gesture bar | Real WindowInsets; Chromium reports env() as 0 here (AB#2499) |
| Android · 3-button navigation | ~48dp | Back / home / recents — opaque buttons, not a thin glyph | Same WindowInsets channel; not yet exercised on a device |
| Phone with no home indicator | 0 | — | Both channels report 0; the node's 88pt is exact here |
The ambiguity, stated precisely. The node's 24pt below the label can be read two ways, and the two readings differ by 26pt on an iPhone. Read as decorative padding, the reserved strip has to be added underneath it. Read as the reservation itself — drawn at the size a phone with no home indicator needs — it absorbs the strip. The node does not say which, and this is the question we need answered.
Each capture below is cropped to the tab bar alone — nothing else of the app is in frame. The striped magenta band
marks the strip the OS reserves and the two chips report the measured values; both are a debug overlay we inject at
capture time, not part of the app. The home indicator and the Android gesture handle in these images are the
real OS glyphs, not drawn by us: the iOS ones come from video frames, because simctl still
captures omit the compositor overlay while recordVideo includes it. Measured off those frames on the
iPhone 16, the indicator is 142.1pt × 5pt, centred, sitting 8pt above the bottom edge, and it
renders as the light-over-dark variant — RGB ~(45, 59, 68) against the bar's navy (3, 37, 61) —
low contrast because iOS blends rather than painting solid white.
Option A — reservation added on top of the design's spacing. What shipped until 2026-08-22: the 80pt content block with the whole OS reservation appended. Rejected in testing; it is also 26pt off the node. It is nonetheless what Material's own navigation-bar component does.


Option B — the node taken literally, 88pt everywhere. Shipped 2026-08-22 (PR 2155). On iOS this reproduces the node exactly — 88pt bar, 53pt slots, 18pt gaps, 28pt side insets, icon at 16pt, label at 52pt, 24pt below — and places the labels 24pt above the physical bottom edge, which is inside Apple's 34pt reserved strip. They clear the drawn home-indicator glyph and stay readable, but Apple's guidance asks for essential content to stay out of that strip and Apple's own tab bar keeps its labels roughly 34pt up. Android is unaffected: 24pt clears a 24dp gesture bar.


Option C — the node's 24pt treated as the reservation. This is our recommendation. Keep the
rhythm above the label untouched (16pt, 28pt icon, 8pt, 12pt label = a 64pt content block) and make the region below
the label whichever is larger, the node's 24pt or what the OS reserves:
max(24pt, platform bottom inset). Nothing is invented — the node's own fixed 8pt simply becomes a
minimum instead of a constant.
| Device | Below the label | Bar height | Against the node's 88pt |
|---|---|---|---|
| Android, gesture nav (24dp) | 24pt | 88pt | Exact |
| Phone with no home indicator | 24pt | 88pt | Exact |
| iPhone with home indicator (34pt) | 34pt | 98pt | +10pt |
| Android, 3-button nav (48dp) | 48pt | 112pt | +24pt, and unavoidable — the buttons are opaque |


The Android capture above is the same file used for option B — the two renders are byte-identical, which is the point: the recommendation costs nothing on Android.
Why we prefer C to shipping 88pt flat: it is the node on three of the four cases above, including every Android phone in normal gesture-navigation use; it places labels where both platforms expect them without ever producing option A's empty slab; it degrades correctly on Android 3-button navigation, where a fixed 88pt would put opaque system buttons on top of the labels; and it survives future hardware, since the reservation is read from the OS rather than baked into a number. The cost, stated plainly: the bar is 10pt taller than drawn on home-indicator iPhones — the most common device our members carry — and the two platforms are no longer pixel-identical (98pt vs 88pt). If cross-platform identity matters more than platform-correct label placement, option B is the right call and we will hold it.
Measurement method: every app value is a
getBoundingClientRect() and computed-style read taken from the running build on both devices, on the same
screen, signed in with the same five tabs — not derived from a screenshot or read off the CSS. The striped band
and the two chips in each capture are a debug overlay injected at capture time, reporting both inset channels. The iOS
captures are extracted from simctl io recordVideo frames rather than simctl io screenshot
stills, because the still path omits the compositor overlay and therefore the home indicator; the video path includes
it, so the glyph in these images is the real one. Options A and B are shipped code; option C was applied live to produce its captures and is one CSS
rule if adopted.
Worth knowing before any treatment is chosen, because it bounds what design can ask for:
UIViewController.prefersHomeIndicatorAutoHidden (hide it after inactivity) and
preferredScreenEdgesDeferringSystemGestures (defer the swipe). Neither changes its colour. The only way
to alter how it looks is to alter what we draw underneath it.WindowInsetsControllerCompat.setAppearanceLightNavigationBars(bool) — true draws the
handle dark for a light background, false draws it light for a dark one.
Window.setNavigationBarContrastEnforced(false) drops the translucent scrim in 3-button mode.
setNavigationBarColor() is deprecated under Android 15's enforced edge-to-edge.We currently set none of these — a search of the codebase finds no call to
setAppearanceLightNavigationBars or its neighbours, so Android is on the system default. Since our bar is
navy, the handle should be explicitly set to the light variant rather than left to chance. That is an engineering fix,
not a design decision, and is noted here only so the asymmetry is visible: on iOS we cannot make the equivalent
guarantee.
Confirm the intended safe-area treatment so the shells implement design intent rather than an engineering default:
(a) what fills the top inset on each composition — should the page background simply extend (current behavior), or do
hero/banner images bleed under the status bar? (b) should bottom-of-screen CTAs sit flush above the home indicator with
extended background (current behavior), or float with a gap? (c) add safe-area guides to the master frames so future
screens are drawn with the reserved zones visible. (d) For navBar-bottom specifically — is the
node's 24pt below the label the home-indicator reservation, or padding on top of it? Is a 98pt bar on home-indicator
iPhones acceptable in exchange for labels sitting on the safe-area boundary rather than inside it, or should 88pt be
held everywhere? And can the spec state the bottom region as max(24pt, platform inset) rather than a fixed
8pt, so it covers gesture nav, 3-button nav and future hardware without another revision?
Consequence: until answered, any new full-bleed composition (e.g. the future ALPA-brand home, photo-hero screens) inherits the engineering default — background-extends, content-inset — which may not match the intended art direction.
What we found: navBar-bottom 5387:31928 draws exactly five slots. The bar does not always carry five: signed out it carries three — JUMPSEAT · KCM · LOGIN (AB#2528) — and the tab set is data-driven from the menu API, so other counts are possible. The node has no variant for a reduced count, so the behaviour was an engineering choice by default.
What it used to do: the slots stretched to fill the same 337pt content width, 100.3pt each. That preserves the bar's width but reads as three unrelated controls pinned across the screen rather than one navigation group, and it pushes the outer targets away from a thumb.
What we shipped: a bar with fewer than five slots now holds the node's own 53pt slot and 18pt gap and centres the cluster — 3 × 53 + 2 × 18 = 195pt, centred, giving 99pt each side on a 393pt screen. The five-slot bar is untouched and still flexes, which is what lets its labels share slack rather than collide at larger OS text sizes (AB#2391) — a constraint the three-slot bar does not have, with 142pt of slack to spare.
Both captures below are the real signed-out bar — JUMPSEAT · KCM · LOGIN — taken on a device that has never been signed in, and cropped to the 88pt bar alone so nothing else on the login screen competes with the slot positions. Measured: stretched 100.3pt slots at 28pt side insets; clustered 53pt slots, 18pt gaps, a 195pt cluster centred 99 / 99.


The rule we shipped is count-based: it clusters a bar of four slots or fewer and leaves the five-slot bar flexing. That was sized for a phone, where flexing five slots lands within a few points of the design — 53pt on a 393pt iPhone, 56.8pt on a 412pt Pixel. A tablet breaks that assumption: the same five slots stretch to 141.2pt each, 2.7× the designed 53pt, spread across 778pt of bar.
| Device | Slots | Flexed slot width | Clustered slot width | Clustered side insets |
|---|---|---|---|---|
| iPhone 16 — 393pt | 5 | 53pt | 53pt | 28 / 28 (identical — nothing to fix) |
| Pixel 9 — 412pt | 5 | 56.8pt | 53pt | 37.5 / 37.5 |
| iPad Pro 11-inch — 834pt | 5 | 141.2pt | 53pt | 248.5 / 248.5 |
| iPad Pro 11-inch — 834pt | 3 | 247.3pt | 53pt | 319.5 / 319.5 |
Captured on an iPad Pro 11-inch (M4) simulator, iOS 26.4,
834 × 1210, data-alpa-idiom="tablet", signed in so the bar carries its full five slots. Cropped to the
88pt bar alone. The stretched capture is the behaviour before the fix; the clustered one now ships.


Shipped 2026-08-22. On the phone the flexed bar is the design, so the count-based rule costs nothing there. On the tablet it is not: the icons drift to the edges and stop reading as one navigation group. The bar therefore clusters at any slot count on a tablet, five included.
The rule is keyed on the idiom, not on a width breakpoint:
:root[data-alpa-idiom="tablet"] .alpa-bottom-nav .alpa-nav-item { flex: 0 0 53px; }
data-alpa-idiom is already published once at startup by the head
(IDeviceDisplayService → Main.razor → alpa-immersive.js) and is the seam this
codebase already uses to say “tablets are not space-constrained”. A width breakpoint would be a second, parallel answer
to a question that already has one — and it would not be free: a threshold set at the design's 337pt content block
catches the 412pt Pixel too and would quietly move its slots from 56.8pt to 53pt, a visible change on a phone that
nobody asked for. Keying on the idiom leaves every phone exactly as it renders today.
Measured after the change: iPad Pro 11-inch in landscape (1210pt) goes from 216.4pt slots to 53pt with the 337pt cluster centred 436.5 / 436.5; in portrait (834pt) from 141.2pt to 53pt centred 248.5 / 248.5. iPhone 16 unchanged at 53pt slots and 28pt insets, its three-slot bar still clustering 99 / 99; Pixel 9 unchanged at 56.8pt slots.
Confirm the centre-clustered treatment, or specify the reduced-count bar explicitly. If clustering is right, please add a reduced-count variant to the component so the rule is in the design source rather than only in CSS — and say whether the same rule should apply to four slots, which the menu API can also produce. And confirm the tablet behaviour now shipped: the five-slot bar clusters at 53pt on a tablet rather than stretching to 141pt (portrait) or 216pt (landscape) slots. If a tablet should instead have its own slot size rather than reusing the phone's 53pt, say so and we will spec it — the current rule reuses the phone value because the node offers no tablet variant.
Related: DQ-28 (bar height and the safe area) — same component, same work item. Work item AB#2536 · PR 2155.
The tablet work scoped under AB#2563 is Adaptive Layout, not "responsive layout" and not "smart promotion" (an earlier, imprecise working name). Standing vocabulary going forward:
Platform prior art, for reference: Apple's Size Classes (horizontal/vertical, each Compact or Regular) and Material's Window Size Classes (Compact/Medium/Expanded/Large/Extra-large) are both adaptive breakpoint systems in this sense — neither shares breakpoint values with the other, which is why AB#2563 also carries an explicit iOS-vs-Android convention-divergence question rather than assuming one shared rule set.
Why it matters: the distinction is load-bearing for how the breakpoint math gets calculated — locked component sizes are the fixed inputs the breakpoint rules are computed against. Calling this "responsive" would misdescribe the mechanism to anyone picking up the work later.
Related: DQ-34 (tab-bar cluster-vs-stretch behaviour at tablet width — same problem family, narrower scope) · AB#2409 (tablet audit — breakpoint set + simulator matrix, this decision's prerequisite) · AB#2563 (implementation).
Shipped with AB#2591 (which also deleted the legacy
WebViewPage/MauiAuthenticatedWebView pair and replaced every call site with
AuthenticatedWebViewPage/AuthenticatedWebViewHost). The immersive treatment
joins the same standing rule as everywhere else — phone-only; tablets keep their chrome
— rather than opening the scope question further. On a phone, rotating to landscape auto-hides the
header (the ShowCompact=False variant's single bar — see DQ below on that variant) and the
page fills edge-to-edge; rotating back to portrait restores it.
Implemented natively, not via the Blazor
LandscapeAllowed/alpa-immersive.js marker DQ-36 originally asked about
joining — that marker only reaches Blazor DOM chrome (.alpa-top-nav etc.), not this
page's own native header. WebViewPageViewModel instead subscribes directly to
IDeviceDisplayService.DisplayChanged (the same idiom/orientation snapshot
BannerPageViewModel already uses to hide the CMS hero banner in landscape), forcing
ShowCompact=True on entering landscape and restoring whatever it was before on returning
to portrait. No tap-to-reveal timer, unlike the Blazor mechanism — the floating browser-control cluster
(AB#2542) stays visible regardless of ShowCompact, so there is always a way out without
needing the header back.
Related: AB#2538 (immersive landscape, original scope) · AB#2591 (this decision's implementation) · Tester Walkthrough — In-App Browser, Check 6.
Design questions that have already been answered, kept here so reviewers can see what's settled.
| Item | Decision | Date | Reference |
|---|---|---|---|
| DQ-3 — Welcome greeting fallback | Decided No-rank fallback approved. When pilot rank
is unavailable from the backend, the Welcome card shows "Welcome {FirstName} {LastName}" (full
name, no rank prefix) — e.g. "Welcome Jose Johnson". When rank is present it prefixes as designed ("Welcome
Captain Johnson"). This is the design sign-off on the Backend Report's "Option C". |
2026-06-08 | card · backend report |
| Favorite (heart) indicator | Decided Rendered on all favoritable components (D31, 2026-06-26). The favorite feature persists as client state (IsFavorite). See also D7 (original removal, superseded). |
2026-06-05 | naming-decisions-record |
Context: the flight card-og expanded master (4713:19985) renders every
status chip — "On time" and "Delayed 34m" alike — in the same amber (#F7BB0D). The 2026-06-10 token
capture, however, defines a per-status set and names the badge use explicitly: Status/On-time #00EA75
("Badge on-time state · flight status badge"), Status/Delayed #F7BB0D ("Badge delayed"),
Status/Error #C02126. The On-time token was added in that revision — the card master predates it
and was never repainted.
Implementation decision (AB#2316, 2026-07-21): tokens win. Chips color by category —
on-time green #00EA75, delayed yellow #F7BB0D (also the fallback for free-text carrier
statuses), cancelled error red #C02126 with white text. "Early" has no token of its own
and rides the on-time green as a good-outcome state — that mapping is the judgment call wanting design sign-off.
Question for design: confirm the per-status mapping (esp. Early → green), and repaint the
flight card-og master's chips so the component library agrees with the token set.
Resolved 2026-07-29 (José) — the implemented mapping is correct; no code change required.
Early → green is signed off. Early and On-time both render
Status/On-time #00EA75. Early has no token of its own and rides the on-time green as a
good-outcome state, exactly as implemented under AB#2316.
Cross-checked against three independent sources on 2026-07-29 and all three agree, which is what closes this:
| State | Shipping CSS | Legacy XAML AlpaTheme.xaml | Figma |
|---|---|---|---|
| On-time / Early | #00ea75 | StatusOnTime #00EA75 | #00EA75 |
| Delayed (and free-text default) | #f7bb0d | StatusDelayed #F7BB0D | #F7BB0D |
| Cancelled | #c02126 + white text | StatusError #C02126 | #C02126 |
Note on Status/Warning #B7770D: the general
Status/Warning token in design-tokens.html lists
"delayed-status indicators" among its usages, which reads as a competing value for the delayed chip. It is
not — Status/Delayed #F7BB0D is the named per-status token and is what both Figma and the
legacy XAML paint. #B7770D must not be applied to these chips: it would also fail WCAG AA at this
chip's 12px size (4.14:1 on navy, 3.71:1 on white, against 4.5:1).
White text on cancelled is required, not stylistic.
#C02126 gives 1.44:1 against navy body text — effectively illegible — and 5.87:1 against white.
The other two chips keep navy text; yellow measures 8.83:1.
Still outstanding (design task, not engineering): the
flight card-og master (4713:19985) still paints every chip amber and predates the
On-time token. Repainting it removes the discrepancy that raised this DQ; until then the token set is
authoritative and the code follows it.
Context. The revised bottom-nav design (file owEYzHf7FrHRvWC2u82UOl, node 4436:5027 "navBar-bottom") specifies five slots — HOME · JUMPSEAT · KCM · PDR · {MEC} MEC — implemented in the cold-start tab seed under AB#2272. Three slots had a clean asset; two did not, and both were worked around rather than blocked.
1. PDR has no icon of its own. The design's PDR slot carries data-name="ftdt", and its exported path data matches the FTDT icon the app already ships (nav-ftdt.svg) to within sub-pixel rounding. PDR therefore renders the FTDT glyph today. That is likely a placeholder rather than an intentional shared icon — PDR and FTDT are different destinations and probably should not be visually identical in a five-slot bar. Ask: is a distinct PDR icon coming, or is sharing the FTDT glyph intended?
2. The MEC slot is a fixed-tint raster. The design exports that slot as ALPA-App-BottomNav-Logo 1, a 700×699 PNG. Decoded it is a flat single tint (#C4DCF6) with genuinely transparent holes for the globe grid, swoosh and wordmark, so it drops in as a background-image and matches the design exactly — no mask, filter or recolouring. That is what shipped.
The cost is that it cannot be themed. Every other nav icon is an SVG consumed through a CSS mask with background-color, so it follows the nav's colour; a fixed-tint raster cannot. The nav background tracks Surface/Brand, which per-MEC themes do override (theme.UAL.json sets #002243), so on a themed nav this one icon keeps its ALPA tint while its four siblings recolour. Ask: supply the ALPA globe as a flat monochrome SVG — single path, or silhouette plus true knockouts — so it can be masked and themed like the others; or confirm a fixed brand tint is intended and should deliberately not follow the theme.
Why the in-repo logo could not be reused. Recorded so it is not re-attempted: ALPAMobile/Resources/Images/alpa_logo_notification.svg is an ALPA globe in vector form, but it is a 36-path auto-trace with per-facet shading (28 dark paths, 8 light) rather than silhouette-plus-knockouts. Masking it flattens the mark to a solid disc. Combining its paths under fill-rule="evenodd" to cut holes was tried on 2026-07-29 and rendered unusable — globe grid lost, disc clipped. A clean mono export has to come from design; it cannot be derived mechanically from that file.
Raised 2026-07-29 (AB#2272). The bottom tab bar is still a mock pending the menu-API changes that will drive it (ShowOnTabBar / ShowOnTabBarSort), so these assets are consumed from the cold-start seed today. Figma: navBar-bottom 4436:5027. Related: DQ-17 (icon strategy), D33 (font-family as MEC theme token).
Context. /kcm/policies has one action, SEARCH POLICIES, and it renders as small caps in grey on a grey fill. Every other primary action in the app is the blue endcap button — KCM's own crew-role filter uses it for APPLY, Flight Finder uses it for SEARCH FLIGHTS.
The effect is that the only thing you can do on the screen looks like the one thing you cannot. Nothing is functionally wrong: it is enabled and it works.
Ask: should this adopt the primary button treatment, or is a quieter action intended here for a reason we have not captured?
Resolved 2026-08-13 — mooted. /kcm/policies was purged: the screen had
no backing data source and no counterpart in the legacy XAML app. Airline-policy search lives on
/jumpseat/airline-policies (title-bar search state, Figma 4713:25303).
Raised 2026-08-02 from a UI/UX pass over the tester-facing areas (AB#2286). Not blocking UAT.
Context. The form pairs an AIRLINE select (defaulting to "All airlines") with an AIRPORT free-text field placeheld "Airport code or city". Flight Finder's airport fields on /jumpseat/search have typeahead over the same airport data.
Two fields sitting together behaving differently invites a member to expect suggestions in the second and get none. It may be deliberate — a policy search is coarser than a flight search — or it may be a typeahead that was never wired.
Ask: is the asymmetry intended? If not, the airport field should get the same typeahead as Flight Finder rather than a new pattern.
Resolved 2026-08-13 — mooted. /kcm/policies was purged: the screen had
no backing data source and no counterpart in the legacy XAML app. Airline-policy search lives on
/jumpseat/airline-policies (title-bar search state, Figma 4713:25303).
Raised 2026-08-02 from a UI/UX pass over the tester-facing areas (AB#2286). Not blocking UAT.
Context. /jumpseat/airline-policies is a long alphabetical directory with a "Search airlines…" field. It shows no count, and typing something that matches nothing leaves the area blank rather than saying so.
KCM Airlines is the same shape and handles both: it states a no-match explicitly, quoting what was typed. The two lists teaching different lessons about the same interaction is the part worth fixing, more than either behaviour on its own.
Ask: should this adopt KCM Airlines' no-match treatment, and is a result count wanted on either or both? A count is the cheapest way to tell "the filter worked and there are three" from "the filter did nothing".
Raised 2026-08-02 from a UI/UX pass over the tester-facing areas (AB#2286). Related: the empty-state inventory, which records eleven surfaces with no empty state at all. Not blocking UAT.