← Back to Component Index

Component Decisions Record

The decisions taken on the component library — naming, ViewModel hierarchy, and design scope — with the rationale behind each. This is the authoritative reference; the Naming Alignment Report holds the analysis that led to the naming set.

Date: 2026-06-04 Last updated: 2026-08-14 12:29 ET Audited: 2026-07-09 21:15 ET Decided by: J. Castro + review Status: D1–D49 adopted & applied through 2026-07-06 · D32: neutral naming · D33: font-family MEC token · D34: Text/On-Brand MEC-themeable · D41–D45: Flight Finder sync (filter/sort duplicate resolved, search secondary buttons, banner, advanced-search expander, duty period confirmation) · D46–D49: Figma gap sync (button pill rename resolved, Toggle Switch new atom, flight card-saved & recent searches node confirmed, button link resolved not-catalogued)
✓ Adopted and applied.

All component spec docs, property mappings, the index, and the backend report have been renamed to this set. The two implementation questions are now decided too: D3 (Button absorbs Icon Tile via an optional Icon) and D6 (greenfield ComponentViewModel hierarchy) — both applied across the docs.

1. Adopted Naming Set

Adopted NamePrevious NameViewModelCategoryDecision
CardCardCardViewModelContent surfaceKept
CardHeroHero CardCardHeroViewModelContent surfaceRenamed
CardTextText CardCardTextViewModelContent surfaceRenamed
CardSmallSmall CardCardSmallViewModelList item / nav rowRenamed
Button (optional Icon)Button Card
Icon Button Card
ButtonViewModelAction controlMerged
Button GroupMulti Button CardButtonGroupViewModelControl containerRenamed
CarouselSlider CardCarouselViewModelLayout containerRenamed
ListStack CardListViewModelLayout containerRenamed

2026-06-10 Re-sync — new adopted names

Added from the 2026-06-10 Figma re-sync (see decisions D12–D14 below). Full dispositions for all 34 Figma masters are in Component Changes.

Adopted NameFigma masterTier
Segmented Controltab groupNav chrome (D9 amended)
Bottom NavigationnavBar-bottomNav chrome
Top NavigationtopNavNav chrome
Nav Controlsnav controlsNav chrome
Navigation Drawerhamburger menuNav chrome
NavItem (Primary/Top/Drawer)NavItem-primary/-top, navItem hamburgerAtom
Form FieldMaster Form FieldPrimitive
Progress Barprogress bar & labelsPrimitive
Section Titlesection titlePrimitive
BadgestatusAtom
Info Blockprofile info blockAtom
EndpointendpointAtom (flight)
Logoalpa logoAtom (brand asset)
BackbackAtom (control)
Page TemplatetemplatePage template
Interior Templatemain view-interiorPage template
Feed Templatemain-feed slotPage template
Flight Cardflight cardDomain control (D11)
Flight Segmentflight segment cardDomain control (D11)
Flight Detailflight detail flight infoDomain control (D11)
Duty Periodcard-duty period-altDomain control (D11)

Folded into variants, not new components: card-notification (Card layout variant), btn-pulldown / btn-sort&filter / horizontal button with icon / button-sm (Button variants), size collapse (card-md/card-lg → Card variant). Screens (out of library, D14): profile, menu-hamburger. Tablet (*-tablet): deferred-responsive.

2. Decisions & Rationale

D1 — Slider Card → Carousel
Decision: Rename to Carousel (CarouselViewModel).
Why: In standard UI vocabulary "slider" means a draggable range/value input. Ours is a horizontally paged collection of cards — universally a "Carousel." Removes a genuine ambiguity for both design and engineering.
Also: Reclassified as a layout container, not a card.
D2 — Stack Card → List
Decision: Rename to List (ListViewModel).
Why: Developers reach for "list"; "stack" specifically implies a non-scrolling fixed arrangement (SwiftUI VStack), which would mislead for a scrolling vertical collection. SMEs don't use either term, so the choice is made on engineering clarity. "List" (UITableView / SwiftUI List / web lists) is the dominant convention.
Also: Reclassified as a layout container, not a card.
D3 — The card-btn button family
Decision (revised): Button is a single action control with an optional leading Icon (ImageSource?) — absent = text-only, present = icon + text. The former Icon Tile is folded in as the icon presentation. Button Group remains the container that lays out multiple Buttons (renames Multi Button Card).
Why: An icon is data, not a separate type. The codebase already proves this — MenuItemViewModel is one ViewModel where the icon is an optional Glyph/ImageSource, with no separate "IconMenuItem" type. Our Button and Icon Tile specs were the same horizontal layout differing only by icon presence, so they are one component. Button Group stays a container (sits with Carousel and List).

✓ Resolved (2026-06-05): Applied — Icon Tile merged into Button with an optional Icon property. The separate Icon Tile component and its docs were removed. Component count: 9 → 8.

D4 — Card family: type-first naming (Card{Qualifier})
Decision (revised 2026-06-10): The content-card family is named type-first — the Card type leads, the qualifier follows: CardSmall, CardHero, CardText (base = Card). ViewModels likewise: CardSmallViewModel, CardHeroViewModel, CardTextViewModel. Supersedes the earlier qualifier-first names (Small Card / Hero Card / Text Card).
Why: Groups the whole card family under one Card* prefix (sorts/reads together) and matches Figma's card-* names. Consistent with D5 — containers (Carousel / List / Button Group) are not cards, so they carry no Card prefix.
Scope: Scaffold content cards only. Size stays a variant (D12) — CardSmall is the nav-tile content component, distinct from base Card with Variant = Small. Domain controls keep domain names (Pilot Card, Flight Card — named for meaning, not the card type).

✓ Revised 2026-06-10. Vindicates the existing Blazor code (CardSmall.razor / CardSmallViewModel were already type-first). Doc-wide rename (card-hero/card-hero/, card-text/card-text/, card-small/card-small/ + ViewModel refs) is a follow-up pass.

D5 — Drop the "Card" suffix on containers
Decision: Containers do not carry the "Card" suffix — Carousel, List, Button Group.
Why: A container holds cards; it is not itself a card. Reserving "Card" for single content surfaces keeps the content/layout distinction clean.
D6 — Greenfield ViewModel hierarchy (ComponentViewModel root)
Decision: This is a greenfield component library — the ViewModels do not inherit the legacy app's base classes. The library has its own root, ComponentViewModel, from which all components derive. Content cards derive from CardViewModel; the Button shares the same card-like chrome through a SurfaceViewModel base (see D10); containers derive from ComponentViewModel directly. Containers' Items hold ComponentViewModel, so any component is feed-placeable.
Why: A container is not a card, so it must not derive from CardViewModel (which previously caused a self-referential type — a Carousel that is a card while holding cards). A neutral renderable base names the truth and keeps the content/layout split clean at the type level. Building greenfield avoids dragging legacy ObservableObject/page-VM assumptions into the library base.
ComponentViewModel — greenfield library root (abstract) · IsVisible / IsEnabled / IsLoading
  ├── SurfaceViewModel : ComponentViewModel (abstract) — card-like chrome · BackgroundToken / CornerRadius / Padding / tap
  │   ├── CardViewModel : SurfaceViewModel — content-surface base
  │   │   ├── DocumentCardViewModel : CardViewModel (abstract) → CardHeroViewModel (rename executed 2026-07-15 — see D59)
  │   │   ├── CardTextViewModel : CardViewModel
  │   │   └── CardSmallViewModel : CardViewModel
  │   └── ButtonViewModel : SurfaceViewModel — action control · Label / Command / optional Icon
  ├── ContainerSurfaceViewModel : ComponentViewModel (abstract) — paintable wrapper · BackgroundToken / CornerRadius / Padding · no tap · D26
  │   ├── ButtonGroupViewModel : ContainerSurfaceViewModel — ObservableCollection<ButtonViewModel>
  │   ├── GridContainerViewModel : ContainerSurfaceViewModel — 3-col fixed · Items: ObservableCollection<ButtonCardViewModel> · D25
  │   ├── CarouselViewModel : ContainerSurfaceViewModel — Items: ObservableCollection<ComponentViewModel>
  │   └── ListViewModel : ContainerSurfaceViewModel — Items: ObservableCollection<ComponentViewModel>
Scaffold only: this tree is the reusable, domain-free scaffold. Domain-specific controls (e.g. Pilot Card) are not subclasses here — they compose a scaffold ViewModel through a mapper/factory (see D11). Inherit-only bases (ComponentViewModel, SurfaceViewModel, DocumentCardViewModel) are abstract — "inherit from me, don't instantiate."

✓ Applied (2026-06-05): All component spec docs, property mappings, ViewModel code blocks, and collection element types updated to this hierarchy. BaseViewModel references replaced with ComponentViewModel; container Items and Button Group collections retyped.

D7 — Favorite heart not rendered (design scope)
Decision: The per-element favorite heart visual (i-heart) is not rendered on the components. The favorite feature is unchanged — IsFavorite / FavoriteIcon persists as client state (local Settings); only the on-element heart indicator is dropped.
Why: Design decision — the favorite is managed elsewhere in the UI, so a per-card heart indicator is no longer required on the elements themselves.
Figma caveat: The Figma source still contains the favorite / i-heart element across components. Layer trees and the "Favorite icon" dimensions are kept faithful to Figma for provenance, with an exclusion note. When importing future Figma revisions, drop the heart visual — it is intentionally excluded, not missed.

✓ Applied (2026-06-05): Heart removed from all rendered diagrams and previews (index, card-small, backend report, property-mapping ASCII). IsFavorite retained as state in the mappings. Exclusion notes added to the Design Tokens Reference and the affected component pages.

D8 — Text Card variants: Eyebrow → Label (mobile context)
Decision: The three Text Card variants are named by their leading element (simplest → richest): Description · Title · Label, mapping to TextCardVariant { Description, Title, Label }. The small label above the title — initially called Eyebrow — is renamed Label.
Why: "Eyebrow" is editorial/web jargon and reads as confusing. Label is the mobile-context term — it matches Material 3's label typography role and the MAUI Label control — and it matches the existing Label property in our spec (IsLabelVisible), so the variant name and the code stop diverging. ("Overline" is deprecated Material 2 and absent on iOS.)
Provenance kept: "eyebrow" remains as the Figma layer name and the --font-eyebrow-* token id (other/eyebrow). The mobile / web-dev / editorial comparison is recorded in the Naming Alignment Report (§5 addendum).

✓ Applied (2026-06-05): Renamed across the Text Card spec, index preview labels, and property mapping (variant note, references, TextCardVariant enum). Convention itself (lead-element naming) is unchanged.

D9 — Naming axis: Universal (provisional)
Decision: For now, standardize on universally-recognized component names (web/mobile-neutral). Avoid platform-specific terms — mobile (Segmented Control, Page Control, Cell) and web (Jumbotron, Overline). The current set already satisfies this.
Why: The platform direction (native MAUI/XAML vs Blazor/Razor) is unsettled — Blazor Hybrid work has restarted in a separate branch (2026-06-05). Universal names read correctly under either stack and require ~zero change now. Lowest-regret choice — refactor only if the direction settles to a strict web or mobile axis. The Label element choice (D8) is universal-compatible and stands.
Refactor surface (only if a strict axis is later chosen):
  • Mobile axis would rename: Hero Card → Feature Card · Button Group → Segmented Button/Control · Carousel → Page Control
  • Web axis would reconsider: Text Card Label element → Overline (MUI)
  • Stable under either: Card · Button · List · CardText · CardSmall
Full web/mobile/editorial comparison is in the Naming Alignment Report (§6).

Amended 2026-06-10: Segmented Control is adopted as a scoped exception to this rule for the in-screen single-select segment (Figma tab group) — it is the clearest name, matches the design system's own Segmented Control token, and disambiguates from Bottom Navigation (the app's real tab bar). Page Control remains avoided.

D10 — Shared SurfaceViewModel base for Card and Button
Decision: Extract a SurfaceViewModel : ComponentViewModel base that carries the card-like chrome shared by tappable tiles — BackgroundToken, CornerRadius, Padding, and the tap target. Both CardViewModel and ButtonViewModel now derive from it. The Button is thereby aligned with the cards in the tree without becoming a card.
Why: In this design system a Button is card-btn — a card-shaped surface — and the former Icon Tile was folded into it (D3), so Button and Card genuinely share a surface. But they do not share content: CardViewModel carries Header / ContentText / Link, which a Button has no use for. Parenting ButtonViewModel : CardViewModel would make Button inherit dead content properties and break is-a (an action control is not a content card). Factoring the shared chrome into a neutral SurfaceViewModel names exactly what is common — and gives the containers a coherent "holds surfaces" story.
Scope note: SurfaceViewModel holds presentation chrome only — BackgroundToken: string, CornerRadius, Padding, TapCommand. CardViewModel adds content (Header / ContentText / Link); ButtonViewModel adds Label / Command / optional Icon. Containers derive from ContainerSurfaceViewModel (D26) — they arrange surfaces, they are not surfaces.

✓ Applied (2026-06-05): Inserted SurfaceViewModel into the D6 hierarchy; reparented CardViewModel and ButtonViewModel across the Card/Button specs, property mappings, ViewModel code blocks, the index hierarchy tree, and the Component Architecture page.

D11 — Domain controls compose the scaffold (they don't subclass it)
Decision: A domain-specific control (e.g. Pilot Card) is not a scaffold ViewModel and does not derive from one. It is a small unit that maps a domain source onto a generic scaffold ViewModel via a RawRepresentationFactory<T>-style mapper. PilotCard = (UserInfo + contract DocumentItem) → a configured CardViewModel. The scaffold (ComponentViewModel → SurfaceViewModel → CardViewModel / ButtonViewModel / …) stays domain-free. Inheritance is reserved for shared presentation state; domain behavior / data is composed.
Why: The previous PilotCardViewModel : CardViewModel injected IAuthentication + DataManager, ran an async contract lookup, and applied a business rule (omit rank) — all inside a scaffold subtype, with async work in a constructor. That is the fragile base class trap and it pulls domain dependencies into the library base. Microsoft's DI guidelines call for declaring dependencies by constructor injection and warn against direct instantiation of dependent classes within services — which is exactly what the subclass did. Composition through an injected service/factory is the modern-.NET model (a DI container composes graphs by constructor injection — that is composition).
Factory contract (important): the mapper is a pure projection — data in, scaffold VM out. It must not hold an IServiceProvider and resolve dependencies at runtime; the DI guidelines list "injecting a factory that resolves dependencies at runtime" as a service-locator anti-pattern to avoid. The async gathering (auth + contract lookup) lives in a normal DI-injected domain service; the factory only maps. If a VM ever needs the container to build it with runtime args, the supported mechanism is ActivatorUtilities.CreateInstance (objects created outside the container). Register the factory/service; never register the CardViewModel.
// Scaffold — unchanged, domain-free
public class CardViewModel : SurfaceViewModel { /* HeaderText, ContentText, LinkText, Link, … */ }

// Domain layer — pure mapper (data in → VM out)
public sealed class PilotCardFactory : RawRepresentationFactory<CardViewModel>
{
    public CardViewModel Create(UserInfo user, DocumentItem? contract) => new()
    {
        HeaderText    = user?.MEC ?? "ALPA",
        ContentText  = $"Welcome {user?.FirstName} {user?.LastName}",  // rank omitted (tier ≠ rank)
        LinkText     = contract?.Title ?? "View Contract",
        Link         = contract?.FileID ?? string.Empty,
        IsLinkVisible = contract is not null,
    };
}
// async auth + contract lookup lives in a DI-injected domain service, not the factory/VM

✓ Applied (2026-06-05): PilotCardViewModel removed from the D6 scaffold tree; Pilot Card recast as a domain control in its data-source doc and listed in the new Domain Controls layer. Migrated (2026-06-05): all four current data bindings now follow this pattern — Pilot Card (Card), Document Hero (Hero Card), Menu Text (Text Card), Did You Know List (List); each scaffold ViewModel stays domain-free, with mapping in a factory and async gathering in a domain service. Catalog: Domain Controls.

D18 — Variant-as-type + typed themeable surface (2026-06-10 re-sync)
Decision: Size variants collapse into a concrete Variant enum (e.g. CardVariant { Small, Medium, Large }) that the SDUI layer sets to pick which variant renders. The ViewModel carries a typed style surface — a ComponentStyle value object of design-token types (ThemeColor, typed scale units) — not CSS strings.
Why: "No magic strings" (project rule). Type the structure; let the remote MEC theme supply the values. MEC theme = configuration → Options pattern (IOptionsMonitor<MecThemeOptions>, live-reload). An IThemeResolver maps (component type, Variant, Style, active theme) → the class="…" string only at the view edge; the domain layer never holds CSS. Extends D6/D10 — typed chrome becomes token defaults the theme overrides.

✓ Adopted 2026-06-10 (Figma re-sync). Originally numbered D12 in the June 10 capture — renumbered D18 when the 2026-06-17 ingest reserved D12–D17 for component-naming decisions. See capture §5d · Component Changes.

D19 — Component vendor strategy
Decision: Build design-system primitives from scratch (own DOM + CSS variables). A third-party control library is allowed only for complex generic controls (date/time pickers, combobox, dialog, virtualization) and must be wrapped behind our own component + typed ViewModel. Single theming vector: CSS custom properties from typed tokens.
Why: The "restyle, don't rebuild" MEC-theming requirement dictates DOM ownership — own the markup for anything a MEC themes. A vendor type must never touch a ViewModel or markup contract, so vendors stay swappable. MudBlazor is optional/wrapped (prototype only); do not adopt MudTheme as the theming system.

✓ Adopted 2026-06-10. Originally numbered D13 — renumbered D19 (see D18 note). See capture §5e.

D20 — Component library is a MAUI-free RCL (Presentation Extraction AB#2087)
Decision: The design system is a library, not app-shell — a Razor Class Library targeting plain net10.0 with no MAUI dependency, consumed by both the MAUI Hybrid BlazorWebView host and the ASP.NET Core RemoteHost/SDUI. Nav chrome, page templates, primitives, atoms, and their ViewModels live in it; screens stay app-side.
Why: One library, MAUI nowhere in the shared layer — reusable across hosts, testable without the MAUI workload. This is the Presentation Extraction (Feature AB#2087) that UI Refresh is gated on. Typed tokens are library-defined (not Microsoft.Maui.Graphics); platform concerns are injected interfaces implemented in the host (D11 boundary).

✓ Adopted 2026-06-10. Implementation gated on AB#2087. Originally numbered D14 — renumbered D20 (see D18 note). See capture §5f.

3. Implementation Follow-ups (for the build phase)

  1. Variant collapse (from D3) — RESOLVED 2026-06-05: Button is now one component with an optional Icon property (absent → text-only, present → icon button). Icon Tile was removed. Grounded in the existing MenuItemViewModel idiom where the icon is optional data.
  2. ViewModel base class — RESOLVED 2026-06-05 (see D6): Greenfield ComponentViewModel root adopted. Containers derive from it directly; the Button and the content cards share a SurfaceViewModel chrome base (D10); content cards derive from CardViewModel : SurfaceViewModel; Items hold ComponentViewModel. Applied across all docs.
  3. Figma sync: Figma component names remain informal (listy card generic, card-btn, slider). Recommend asking the designer to align Figma names to this adopted set so design and code share one vocabulary.
  4. Naming axis going forward: New components should be named by function, with size expressed as a variant prop — not as a separate component (avoid repeating the card-lg/card-md/card-sm split in code).
  5. Card family rename (2026-06-10, D4 revised): type-first is canonical — CardSmall / CardHero / CardText (the Blazor CardSmall code was already correct). The docs need the rename: dirs hero-card/card-hero/, text-card/card-text/, small-card/card-small/, and ViewModel refs HeroCardViewModelCardHeroViewModel, TextCardViewModelCardTextViewModel, SmallCardViewModelCardSmallViewModel across the spec docs. Follow-up pass. (Arrows corrected 2026-07-15 — this note originally listed the post-rename name on both sides of each arrow, a transcription slip. The follow-up pass itself finally completed 2026-07-15: D59.)
  6. Figma sync done (2026-06-10): the revised Figma was mapped to the adopted set (new names above; full dispositions in Component Changes). Supersedes the earlier "ask the designer to align names" recommendation for this revision.

4. What Was Applied

5. 2026-06-17 Revision — ALPAmobileWtablet

The 2026-06-17 Figma export (ALPAmobileWtablet_20260617, exported 2026-06-16T22:36) introduces a components-tablet section, a restructured flight-card family, and naming changes on the top nav. Decisions D12–D15 resolve the 🚩 flags captured in figma-component-specs.json during the inventory pass. Source: binary extraction from canvas.fig; designer annotations quoted verbatim.

✓ All decisions adopted — D12–D15 resolved 2026-06-22.

D12 (tablet tier), D13 (-og suffix), D14 (card-button = new ButtonCard scaffold), and D15 (flight card family + flight card-og = flight segment card) are all confirmed by binary evidence. No screenshot required. Pending work: tokens pass, sizing deep-dive (null dimensions), screen-mapping, domain-controls roll-up.

D12 — Tablet components: deferred-responsive tier (not scaffold atoms)
Decision: Adopted. Tablet-specific masters (navBar-bottom-tablet, topNav-tablet, flight card-TABLET, template-tablet, template-tablet - home) are a deferred-responsive tier — catalogued separately in figma-component-specs.json under "_revision":"2026-06-17", but not added to the scaffold ViewModel hierarchy or the main component grid in index.html until the responsive layout system is defined.
Why: The designer's annotation is explicit: "A considered tablet treatment, not the phone scaled up: dedicated type mode, purpose-built navs, and increased margins/padding. Tablet components live in the components-tablet section." These are structurally independent components, not size variants of the same ViewModel. However, implementation is gated on the presentation-extraction epic (AB#2087) — adding them to the scaffold hierarchy prematurely would force changes at implementation time. Capturing them in the JSON inventory with a revision tag preserves the Figma truth without committing the hierarchy.
Typography: Tablet mode scales the Typography collection — a modest bump (Body 18→20 px, Display L 32→36 px, etc.), not a zoom. The type mode is a Figma-side collection switch; the token delta is recorded in design-tokens.html under "Tablet Typography Mode."
Code guidance: When implemented, tablet components map to the same ViewModel types as their phone counterparts but are configured via a tablet layout context (not subclasses). Do not create TabletNavBarViewModel; reuse NavBarViewModel with a layout/size discriminator.

Status: Adopted 2026-06-22. Tablet entries added to figma-component-specs.json. design-tokens.html to be updated with tablet typography delta in the tokens pass (step 3). No changes to architecture.html, index.html, or scaffold ViewModel tree until AB#2087 is delivered.

D13 — -og suffix: Figma informal → normalize at code boundary
Decision: Adopted. The Figma designer used the -og suffix (informal for "original") to distinguish the base/phone version from new tablet and variant-family siblings. This informal suffix is not adopted in code names or ViewModel identifiers. The mapping is:
Figma name (2026-06-17)Spec / code nameRationale
topNav-og (4153:3892)topNav / TopNavViewModelThe phone top nav is the canonical version; no suffix needed. Old topNav entry (4864:32242) now labeled topNav/old in Figma — it is retired.
flight card-og (5452:679)Verify vs flight segment card (4555:7483) — see D15Pending screenshot confirmation before retiring the old entry.
Figma provenance: figma-component-specs.json preserves Figma layer names as-is (including -og). The normalization applies only to the code-facing names in specs, property mappings, and ViewModel declarations. Per D9, component names should be universally recognized — "og" is internet slang and not universally recognized in a UI naming context.
topNav transition: The old topNav entry in figma-component-specs.json is flagged 🚩 and carries a "_flag" note. When the deep-dive confirms topNav-og matches the old 393×98 dimensions, the old entry is retired and the topNav-og entry is promoted to the canonical phone top nav. All spec docs referencing topNav remain valid — no rename propagation needed since code names are already normalized.

Status: Adopted 2026-06-22. figma-component-specs.json updated. No spec HTML renames required — code names were already normalized. Confirm topNav-og dimensions in sizing deep-dive.

D14 — card-button is a new scaffold component: CTA Card (ButtonCardViewModel)
Decision: Adopted 2026-06-22. card-button (5452:434) is a distinct new scaffold component — not a rename or variant of card-btn. The two components are structurally different:
  • card-btn (4737:13038, 116×106 px) — standalone icon-tile navigation button. The existing Button scaffold component (D3). Short single-word labels: "Committees", "Reps", "Hotels". Used inside Button Group.
  • card-button (5452:434) — content card with an embedded CTA button. Master has 2 variants (Property 1: Default / Variant2). All instances carry a description paragraph plus a short action label: "Secure your flight with Jumpseat. / Jump Seat", "View committee work, volunteer resources and MEC information. / Committees", "Advocate for aviation safety and contribute to the PAC. / Take Action". Phone-scoped (i-phone context).
Why a scaffold component, not a domain control: Instances span four distinct feature areas (MEC membership, Jumpseat, Committees, PAC). The pattern — description text + primary CTA button — is domain-free and reusable. Per D6/D11, domain-free reusable patterns belong in the scaffold, not the domain-controls catalogue. The designer promoted it to a master component with named variants, confirming it is intended as a reusable building block.
ViewModel: ButtonCardViewModel : CardViewModel. Inherits Header / ContentText from CardViewModel; adds CallToAction : ButtonViewModel for the embedded action. The two variants (Default / Variant2) likely represent button-style differences (primary vs secondary CTA) — confirm in sizing deep-dive.
Scaffold table addition: Button Card (ButtonCardViewModel, category: Content surface with action). Added to the catalogue in this record; HTML grid in index.html and architecture.html updated in the doc roll-up pass.
Token note: The Buttons/Primary Button / Secondary Button / Tertiary Button token paths found in the same revision are standalone button-style tokens. They apply to the embedded CallToAction button inside ButtonCard, but are not themselves the definition of card-button. Update design-tokens.html with these paths in the tokens pass.

Status: Adopted 2026-06-22. Binary evidence (string extraction from canvas.fig): master definition confirmed at 0x1b39d0; 4 instance occurrences with body-text + CTA-label pattern; card-btn master independently confirmed at 0x4d4922 with navigation-tile instances. 🚩 flag on card-button in figma-component-specs.json to be cleared and size filled in sizing deep-dive.

D15 — Flight card family: one domain control; flight card-og = flight segment card
Decision: Adopted 2026-06-22. Two separate conclusions:
  • flight card-og is the 2026-06-17 revision of flight segment card. Dimensions and styling match exactly — both 353×111, fill #ffffff, stroke #e3e3e3, strokeWeight 1. The designer renamed and regrouped the component as part of the flight-card family in this revision; the -og suffix marks it as the base/phone version (D13). The new entry adds a swipe variant to the existing Default + expanded states — an addition, not a redesign. Old flight segment card entry (4555:7483) is retired; flight card-og (5452:679) is the canonical phone master going forward. The existing child tree extracted from flight segment card (trip row, date/time row, leg detail, layover, Close Details button) carries over as the authoritative structure.
  • The full flight card family maps to one Flight Card domain control. The Figma masters represent Figma's variant system; in the domain controls catalogue this is a single Flight Card entry with content/state variants.
Variant mapping (final):
Figma masterMaps toNotes
flight card-og (5452:679, 353×111)Flight Card — base / phoneReplaces flight segment card (4555:7483). States: Default (collapsed) · Expanded · Swipe. Carries trip row, date/time, multi-leg detail, "Close Details" underline button.
flight card-TABLET (5500:44)Flight Card — tablet tier (D12)States: Default · Swipe · Expanded. Variant4/Variant5 — map by appearance in sizing deep-dive.
flight card-recent searches (4441:398)Flight Card — Recent Searches variantJSFF feature. Swipe-to-reveal. State: Default · Swipe.
flight card-saved flightFlight Card — Saved Flight variantJSFF feature. State: Default · Swipe.
flight card-saved searchesFlight Card — Saved Searches variantStates: Default (collapsed) · Expanded · Swipe.
Overlay convention (new pattern): The 2026-06-17 file formalizes the overlay- naming convention: frames prefixed overlay- (overlay-flight finder filter, overlay-kcm filter, overlay-notification filters) are modal sheets layered over a base screen. Build as overlays, not separate screens. Screen-architecture note only; no scaffold additions required.
Notification segmentation (new pattern): The notifications screen uses a two-tab segmented control (Flight Finder tab vs Comms tab). card-notification serves both — Comms is a style/content variant, not a separate component. Captured in screen-mapping.html.

Status: Adopted 2026-06-22. Binary cross-check confirmed: flight card-og and flight segment card share identical size/fill/stroke values (extracted from figma-component-specs.json). 🚩 flag on flight segment card in figma-component-specs.json updated to retired status. Domain controls catalogue (domain-controls.html) to be updated with the Flight Card domain control entry and variant table in the doc roll-up pass.

D16 — FlightSegmentCardViewModel: new 10th scaffold component for Flight Card
Decision: Adopted 2026-06-22. The Flight Card domain control composes a new scaffold ViewModel — FlightSegmentCardViewModel : CardViewModel — rather than force-fitting data into the base CardViewModel.
Rationale:
  • The flight card XAML template binds 8 independent typed fields: Origin, Destination, DepartureTime, ArrivalTime, Duration, FlightNumber, AircraftType, StatusBadge. Each renders in a discrete visual slot (city codes, time row, leg detail) — they are never concatenated in the view.
  • Base CardViewModel provides only 3 properties: Header, ContentText, Link. Mapping 8 distinct fields into these 3 buckets would require the factory to concatenate at least 5 pieces of domain data into ContentText. The DQ-9 threshold was ≥ 3–4 concatenations; this exceeds it by a clear margin.
  • StatusBadge carries both text and a status color — not representable in a plain string without in-band markup or secondary properties, making CardViewModel structurally inadequate.
  • Option B (typed scaffold subclass) was preferred over Option A (force-fit) because: (a) type safety — the factory can assign each field directly with no parsing; (b) XAML binding clarity — each {Binding Origin} is unambiguous; (c) PCL MAUI HeightRequest is fixed-per-variant (189/129 px for ButtonCard, 164/1361 px for flight card) — typed data ensures the correct variant is selected.
Scaffold ViewModel properties:
PropertyTypeSource
OriginstringDeparture IATA code (e.g. "ORD")
DestinationstringArrival IATA code (e.g. "SFO")
DepartureTimestringFormatted time (e.g. "6:13 AM")
ArrivalTimestringFormatted time (e.g. "8:43 AM")
DurationstringFormatted duration (e.g. "4h 30m")
FlightNumberstringAirline + flight number (e.g. "UAL 423")
AircraftTypestringAircraft model (e.g. "Boeing 737")
StatusBadgestringStatus text (e.g. "ON TIME", "DELAYED") — color mapping handled by factory / XAML trigger

Inherits Header, ContentText, Link from CardViewModel. These are available but are not the primary rendering surface for flight data.

Impact:
  • Scaffold library: 9 components → 10 components.
  • FlightCardFactory type updated to RawRepresentationFactory<FlightSegmentCardViewModel>.
  • Spec page pending: flight-segment-card/flight-segment-card-component.html to be added in the next doc roll-up pass.
  • Implementation gated on Epic AB#2087 (Presentation Extraction).

Status: Adopted 2026-06-22. Amended by D17 (2026-06-22): FlightSegmentCardViewModel is demoted from the scaffold hierarchy — the Flight Card family moves to Track B (typed domain components). The ViewModel properties table above remains accurate as a description of the display data required; it is now the domain of a typed ContentView rather than a ComponentViewModel subclass.

D17 — Two-track component pattern: scaffold for generic, typed domain components for complex cases
Decision: Adopted 2026-06-22. The component library operates on two tracks. Track A is the ComponentViewModel scaffold hierarchy (domain-agnostic, renderable in any generic container). Track B is typed domain components — ContentView subclasses that bind directly to a domain model type, bypassing the scaffold. A Track C (post-AB#2087) migrates Track B components to native Blazor Razor components.
Rationale:
  • The scaffold's value is maximum when a component is domain-agnostic and renderable in an unknown context — the same HeroCardViewModel can be placed in a Carousel, a List, or a standalone slot because it carries only generic display strings. This value is zero for a component that is always and only rendering one specific domain type.
  • Flight cards require 8+ typed fields whose semantics are inherently flight-domain (Origin, StatusBadge with color, Gate, etc.). Abstracting these into string properties produces a leaky scaffold, weakens type safety, and creates mapping overhead with no reuse benefit. There is no scenario where a flight card appears inside a Carousel of mixed content types.
  • The DQ-9 field-count threshold (≥ 3–4 concatenations → new scaffold) correctly identified structural misfit; the correct resolution is not a deeper scaffold subclass but a step off the scaffold entirely.
  • The DataTemplate pattern (MAUI) and @parameter binding (Blazor) are idiomatic, well-understood mechanisms for exactly this pattern — they don't require a shared ViewModel base class.
Two-track rule:
TrackWhen to useMAUI implementationBlazor implementation (post-AB#2087)
A — ScaffoldComponent is domain-agnostic AND may appear in a generic container (Carousel / List) alongside other typesComponentViewModel subclass + RawRepresentationFactory<T>Shared Razor component receiving a typed ViewModel via @parameter
B — Typed domain componentComponent is always bound to one specific domain type AND has ≥ 5 domain-specific fieldsContentView subclass + BindableProperty per display slot + DataTemplate registration keyed to domain typeRazor component receiving the domain model directly via @parameter Flight Flight
Track B components (confirmed 2026-06-22):
Figma componentDomain typeMAUI controlSpec
flight card-og family (D15)Flight / LegFlightCardView : ContentViewspec (amended)
card-duty period-altDutyPeriod / DutyPeriodExpandedDutyPeriodCardView : ContentViewspec pending
flight detail flight infoFlight / LegFlightLegInfoView : ContentViewspec pending
Impact on scaffold:
  • FlightSegmentCardViewModel (D16) is demoted: removed from the ComponentViewModel hierarchy. The Flight Card family moves entirely to Track B. Scaffold count: 10 → 9.
  • FlightCardFactory : RawRepresentationFactory<FlightSegmentCardViewModel> is replaced by FlightCardView.xaml.cs — a typed ContentView whose BindableProperty fields correspond to the 8 display slots identified in D16.
  • DQ-10 (flight detail flight info) → resolved: Track B typed component, not a new scaffold subclass.
  • DQ-11 (duty period embedded flight rows) → resolved: internal row template within DutyPeriodCardView, no separate scaffold or Track B component needed.
  • Implementation of all Track B components remains gated on Epic AB#2087 (Presentation Extraction).

Status: Adopted 2026-06-22. Architecture.html, index.html, domain-controls.html, design-questions.html updated. D16 amended.

6a. 2026-06-24 — Grid Container Scaffold

✓ D25 adopted — Grid Container scaffold added 2026-06-24.

New Track A scaffold component specced from MEC screen Figma data (node gpO3masyyNNHxjvRtdcRo7 · 20785:1418). v1 scope: 3-column, card-btn only, phone only. Implementation gated on Epic AB#2087.

D25 — GridContainerViewModel: new Track A scaffold for MEC button grid pattern
Decision: Adopted 2026-06-24. The repeated 3-column card-btn grid pattern found in the My MEC screen (MEC Actions, Quick Links) is formalised as a new Track A scaffold component: GridContainerViewModel : ContainerSurfaceViewModel (base updated from ComponentViewModel to ContainerSurfaceViewModel by D26). Code name: GridContainer. Spec: grid-container-component.html.
Rationale:
  • The MEC screen contains two independent 2-row × 3-col grids of card-btn items. This is a distinct layout pattern not covered by ButtonGroupViewModel (single horizontal row) or CarouselViewModel (horizontal scroll).
  • v1 scope strictly matches the Figma: ButtonCardViewModel items only, Columns=3 fixed, CellHeight=84, Gap=8, phone only. MAUI: CollectionView + GridItemsLayout(span:3).
  • Collection order = display order — server pre-sorts, no client-side sort field in v1.
  • Mixed content types, flexible column count, tablet layout, and server-driven sort order are explicitly deferred with defined extension paths (see spec § 6).
  • Cell background is resolved via DynamicResource Surface/Brand — the dark-navy MEC colour is a theme token, not a grid property.

Status: Adopted 2026-06-24. Spec: grid-container-component.html. Implementation gated on Epic AB#2087. Figma source: MEC screen node 20785:1418, "row" frame pattern — no published component_set for this container; it is ALPA-defined.

6b. 2026-06-23 Revision — ALPAMobile2026-06-23

The 2026-06-23 Figma export (ALPAMobile2026-06-23, exported 2026-06-23T05:25:43Z) captures the current published component library state. All library components were last updated 2026-06-03. Sixteen new components were discovered that were not inventoried in the 2026-06-17 sync. Source: Figma MCP library search against R6hPjBdjSIn6946qTJasUC; local binary at ~/ALPAMobile2026-06-23/canvas.fig (fig-kiwij format, 27 MB).

✓ D18–D24 adopted — 2026-06-23 revision complete.

All 19 🚩 flags from the 2026-06-23 inventory pass resolved. 16 new components added to figma-component-specs.json. Pending work: dimension audit (new components have null sizes — get_metadata blocked by thumbnail-only MCP page), tokens pass update for 2026-06-23 source note, screen-mapping update for new components, domain-controls roll-up for D22 entries.

D18 — flight card: new distinct component_set master, separate from flight card-og
Decision: Adopted provisional 2026-06-23. flight card (componentKey 8e152db607…) is a new component_set master distinct from flight card-og. Both coexist in the published library. Provisional classification: scaffold/composite — a redesigned flight information card that may supersede flight card-og over time. Code name candidate: FlightCard (without the -og suffix). Pending designer confirmation of the intended relationship between the two masters.
Rationale: The presence of both flight card and flight card-og as separate component_sets in the library is unambiguous (different componentKeys). The -og suffix (per D13 and D15) marks the original/legacy variant. The new flight card without suffix is likely the current design direction. No dimensions available from this sync — dimension audit deferred until MCP page access is restored.
Track B / D17 note: If flight card binds the same Flight/Leg domain type as flight card-og, it remains Track B (typed domain component). Does not change the scaffold count.

Status: Adopted provisional 2026-06-23. Pending designer confirmation of flight card vs flight card-og relationship. figma-component-specs.json updated.

D19 — Top-nav chrome retirement: topNav / topNav-og replaced by nav controls + NavItem-primary
Decision: Adopted 2026-06-23. Both topNav and topNav-og are removed from the published library in the 2026-06-23 revision (neither appears in the MCP library search). The open 🚩 flags on both entries are resolved: topNav is retired; topNav-og is retired. Two new nav-chrome components replace the atomic behaviors: nav controls (componentKey 7b56460e…, tier nav-chrome, code name NavControls) and NavItem-primary (componentKey 2d0a7265…, tier nav-chrome, code name NavItemPrimary). Note: topNav-tablet and navBar-bottom / navBar-bottom-tablet are also not surfaced as library components — they remain in the file canvas but are not published; no retirement action needed, they retain their existing notes.
Rationale: The D15/DQ-6 resolution (2026-06-22) identified topNav-og as the canonical top-nav master. The 2026-06-23 library sweep confirms both topNav variants are no longer published, replaced by smaller atomic components. This is consistent with the designer's stated intent to build purpose-built nav components (D12).

Status: Adopted 2026-06-23. figma-component-specs.json updated: topNav and topNav-og _flag cleared → _note. nav controls and NavItem-primary added with provisional tier.

D20 — Toolbar - Top - iPhone / Toolbar - Top - Sheet: vendor reference, excluded from ALPA scaffold
Decision: Adopted provisional 2026-06-23. Both Toolbar - Top - iPhone (componentKey 1831b6d0…) and Toolbar - Top - Sheet (componentKey 21388da3…) are Apple iOS design-kit components (their Figma description explicitly links to feedbackassistant.apple.com/new?form=developertools.fba, which is Apple's design resource feedback channel). Classification: vendor/reference — catalogued in figma-component-specs.json for provenance but not added to the ALPA scaffold hierarchy, ViewModel tree, or domain-controls catalog. Pending designer confirmation: if these were imported for design reference only, no action needed; if the designer intends to use them as the ALPA top-nav chrome, a separate decision is required.
Rationale: The Apple design kit components are standard iOS HIG chrome, not custom ALPA-designed components. Their presence in the library likely reflects the designer using them as layout references alongside the ALPA-specific nav controls and NavItem-primary components.

Status: Adopted provisional 2026-06-23. Pending designer confirmation. figma-component-specs.json entries added with vendor/reference note.

D21 — JSFF flight card consolidation: 3 individual entries → flight card-saved & recent searches component_set
Decision: Adopted 2026-06-23. The three individual placeholder entries (flight card-recent searches, flight card-saved flight, flight card-saved searches) are superseded by the new flight card-saved & recent searches component_set (componentKey 9dfaa1ba…). The three old entries are flagged for retirement — they had null dimensions and no node IDs in the previous sync. The consolidated set is the authoritative Figma master for the JSFF saved/recent search domain control. Code name candidate: FlightCardSavedAndRecentSearches. The variant breakdown (Saved Flight vs Saved Search vs Recent Search) is handled as states within the component_set, not separate scaffold masters.
Rationale: Component_sets in Figma bundle variants under a single master. The presence of a single flight card-saved & recent searches set confirms the designer consolidated what were previously modelled as three separate components. This matches the established pattern (D15: flight card-og is a component_set with Default/Swipe/Expanded states).

Status: Adopted 2026-06-23. figma-component-specs.json: 3 old entries flagged; new consolidated entry added. Retire old entries after designer confirms.

D22 — New domain controls: flight finder filters, notification card flight status, search secondary buttons
Decision: Adopted provisional 2026-06-23. Three new component_sets are provisionally classified as domain-control tier (they bind flight-domain or notification-domain data onto scaffold surfaces):
Figma namecomponentKeyProvisional code nameRationale
flight finder filtersc3045b66…FlightFinderFiltersFilter controls bound to JSFF flight-search state; domain-specific fields
notification card flight status7b28221c…NotificationCardFlightStatusFlight-status notification variant; extends card-notification scaffold?
search secondary buttons93779985…TBDSecondary action buttons on search/JSFF screen — may be scaffold/atom if domain-agnostic
Pending: Confirm whether notification card flight status composes card-notification scaffold (Track A chain) or is a standalone Track B typed component. Confirm whether search secondary buttons carries domain data.

Status: Adopted provisional 2026-06-23. figma-component-specs.json entries added. Dimension audit and domain-controls stubs pending.

D23 — New scaffold atoms: form field, toggle, hamburger, logo, radio, favorites icon, nav items
Decision: Adopted 2026-06-23. The following new components are classified as scaffold/atom tier — domain-agnostic, smallest reusable units:
Figma namecomponentKeyCode nameType
Master Form Fielda8a39ae1…FormFieldcomponent_set
toggle-settings2f0062fc…ToggleSettingscomponent_set
hamburger menu2b49fcdf…HamburgerMenucomponent
alpa logoeb48b025…AlpaLogocomponent
radio button5865b23b…RadioButtoncomponent_set
i-favoritese61f830f…IFavoritescomponent
NavItem-primary2d0a7265…NavItemPrimarycomponent (nav-chrome)
nav controls7b56460e…NavControlscomponent (nav-chrome)
Note: NavItem-primary and nav controls are classified as nav-chrome sub-tier (not general scaffold/atom) since they are navigation-specific. Implementation gated on AB#2087.

Status: Adopted 2026-06-23. figma-component-specs.json entries added. Dimension audit pending (null sizes).

D24 — Primary button: new scaffold/atom, relationship to button-sm TBD
Decision: Adopted provisional 2026-06-23. Primary button (componentKey d5499f97…) is classified as scaffold/atom tier, code name PrimaryButton. The existing button-sm entry (id 4128:7717, 151×36, fill #007bc2) is not retired — it is in the Figma file canvas but was not published to the library in the 2026-06-23 sweep. The two components may overlap in role. Pending designer confirmation: does Primary button replace button-sm, or are they parallel (e.g., button-sm is a small variant and Primary button is the full-size CTA)?
Rationale: button-sm was in the original spec as a 151×36 blue action button. Primary button is a new component_set likely covering multiple size/state variants. The naming pattern (Primary button) aligns with the intent of a standard scaffold button atom.

Status: Adopted provisional 2026-06-23. figma-component-specs.json: Primary button entry added. button-sm entry unchanged pending confirmation. Dimension audit pending.

7. What Was Applied — 2026-06-17 Revision

✓ D26 adopted — ContainerSurfaceViewModel introduced 2026-06-24. SurfaceViewModel gap resolved.

All four container ViewModels reparented from ComponentViewModel to new abstract ContainerSurfaceViewModel. SurfaceViewModel.Background: Color updated to BackgroundToken: string — both surface bases now use the same dual-target token pattern. See architecture.html §5.

D26 — ContainerSurfaceViewModel: new abstract intermediate for paintable containers; dual-target surface model applied to both surface bases
Decision: Adopted 2026-06-24. Introduce ContainerSurfaceViewModel : ComponentViewModel (abstract) as the new base for all four container ViewModels. The four containers (ButtonGroupViewModel, GridContainerViewModel, CarouselViewModel, ListViewModel) are reparented from ComponentViewModel to ContainerSurfaceViewModel. This gives containers a paintable wrapper surface — background color, corner radius, and padding — so MEC design variations can apply brand color and layout customizations at the container level, independent of the cell surfaces.
Properties added by ContainerSurfaceViewModel:
  • BackgroundToken: string — default "Transparent". A semantic token name from the Surface/* namespace (e.g. "Surface/Brand", "Surface/Default"). Not a Color.
  • CornerRadius: double — default 0. Concrete value; does not participate in MEC theme swapping.
  • Padding: Thickness — default Thickness.Zero. Concrete value; does not participate in MEC theme swapping.
Why BackgroundToken is a string, not a Color:
  • XAML / MAUI path: MAUI's MEC theme-swap mechanism works via ResourceDictionary.MergedDictionaries — the MEC theme dictionary overrides token values at runtime. Only bindings that dereference a token key at render time (via IThemeResolver) observe the merge. A ViewModel holding a resolved Color freezes the value at construction time; the runtime theme swap has no effect on it.
  • CSS / HTML path (Blazor Hybrid): CSS theming works through custom property cascades (var(--surface-brand)). If the ViewModel holds a concrete Color value, the renderer must inject it as an inline style (style="background-color:#05273E"), which overrides every CSS custom property due to specificity. Per-MEC brand selectors (:root[data-mec="UAL"] { --surface-brand: … }) cannot override an inline hex value.
  • Resolution per path: XAML resolves BackgroundToken via IThemeResolver.Resolve(token)Color at bind time. CSS resolves it via token.ToCssVar()var(--surface-brand) at render time. A shared TokenNameHelper utility (post-AB#2087) will provide both conversions.
SurfaceViewModel — resolved at D26: SurfaceViewModel.Background: Color updated to BackgroundToken: string. Both surface bases now hold the same token-name pattern — factories and domain controls pass a token from the Surface/* namespace, not a resolved color. The 6 leaf classes (CardViewModel, HeroCardViewModel, TextCardViewModel, SmallCardViewModel, ButtonCardViewModel, ButtonViewModel) inherit BackgroundToken without change. Full model in architecture.html §5.
Distinction from SurfaceViewModel: SurfaceViewModel is for tappable surfaces (cards, buttons) and includes a TapCommand. ContainerSurfaceViewModel is for layout wrappers that hold collections — they are never directly tappable. The tap handler lives on the items inside, not the container. This distinction is intentional and reflects the Figma model: MEC section headers are screen-level composition, not library components.

Status: Adopted 2026-06-24. Implementation gated on Epic AB#2087. Architecture doc updated: architecture.html §1 (hierarchy), §2 (reference table), §5 (dual-target surface model), §6 (overlaps). Grid Container spec updated: grid-container-component.html.

9. What Was Applied — 2026-06-24 Revision

8. What Was Applied — 2026-06-23 Revision

12. What Was Applied — 2026-06-26 Figma Sync (Neutral Naming · Font Tokens · On-Brand Theming)

11. What Was Applied — 2026-06-26 Favorite Heart Reversal

10. What Was Applied — 2026-06-24 Dynamic Feed Revision

D27 — PageViewModel: page root outside the ComponentViewModel hierarchy

Decision: Adopted 2026-06-24. Introduce PageViewModel : ObservableObject as the top-level page model. It is not a ComponentViewModel subclass — it is not renderable by ComponentView.razor and cannot appear as an item inside a container. It is the root that Home.razor receives from PageFactory.

Properties: PageId: string?, Title: string?, Components: ObservableCollection<ComponentViewModel>. Components holds the mapped container ViewModels (ContainerSurfaceViewModel subclasses), which the existing ComponentView.razor dispatcher already handles.

Why outside the hierarchy: A page is not a component — it has no background token, no tap command, no IsVisible. Making it a ComponentViewModel subclass would allow it to appear inside containers, which is never correct. Keeping it separate preserves the invariant that every ComponentViewModel is a renderable leaf or container node.

Factory: PageFactory.Create(PageDto)PageViewModel. Delegates to ContainerFactory for each ContainerDto. Implementation gated on Epic AB#2087 and the backend endpoint delivery.

D28 — Dynamic feed type discriminators: container type and item type string sets

Decision: Adopted 2026-06-24. These are the canonical values for ContainerDto.ContainerType and ItemDto.ItemType. The backend emits them; the client factory maps them to ViewModels. Unknown strings are skipped with a warning — never throw.

Container types

Figma nameDiscriminator stringViewModelNotes
slider"Carousel"CarouselViewModelHorizontal scroll; carries Title + view-all
MEC grid pattern"Grid"GridContainerViewModeln-column layout; see D25, D29
multi-button-card"ButtonGroup"ButtonGroupViewModelSingle horizontal row of Buttons
stack-card"Stack"StackContainerViewModelSurface-neutral vertical stack; see D30

Item types

Figma nameDiscriminator stringViewModelNotes
welcome thing"PilotCard"CardViewModelPilot welcome card with chevron link
card-hero"HeroCard"CardHeroViewModelLarge image card with eyebrow + title
card-small"CardSmall"CardSmallViewModelCompact card
card-text"CardText"CardTextViewModelText-only card (no image)
card-button (D14)"ButtonCard"ButtonCardViewModelBody text + CTA label; used in Grid rows
card-btn (D3)"Button"ButtonViewModelIcon-tile nav button; used in ButtonGroup
emergency variant"EmergencyButton"ButtonViewModelIsEmergency = true

Forward-compatibility: factories skip unknown type strings (log warning, no throw) so the backend can add new types before the client ships support.

Legacy aliases: ItemComponentFactory retains prototype strings ("SingleButtonLarge", "SliderImages", etc.) during transition. New endpoint uses canonical names only.

Full contract: dynamic-feed/dynamic-feed-api-contract.html §4–§5.

D29 — GridContainerViewModel.Items widened to ObservableCollection<ComponentViewModel> (resolves D25 deferral)

Decision: Adopted 2026-06-24. The D25 deferred extension — widening GridContainerViewModel.Items from ObservableCollection<ButtonCardViewModel> to ObservableCollection<ComponentViewModel> — is now resolved.

Reason: The server-driven feed contract (D28) allows any item type inside a "Grid" container, not just ButtonCard. A typed ObservableCollection<ButtonCardViewModel> would require the factory to silently drop non-ButtonCard items or throw. Widening to ComponentViewModel is the correct extension path documented in D25 §6.

Rendering impact: The XAML CollectionView (or Blazor iterator) switches from a single fixed DataTemplate to a DataTemplateSelector (XAML) or <ComponentView Vm="item" /> iterator (Blazor). The Blazor path already works — ComponentView.razor dispatches on type. The XAML path requires a ComponentDataTemplateSelector (post-AB#2087).

v1 scope unchanged: the MEC screen grid remains 3-column ButtonCard items. The widening is an API / ViewModel contract change — the Figma design scope for v1 does not change.

D30 — StackContainerViewModel: surface-neutral vertical stack container (resolves D28 placeholder)

Decision: Adopted 2026-06-25. Introduce StackContainerViewModel : ContainerSurfaceViewModel as the ViewModel for the "Stack" container type. Replaces the temporary ListViewModel placeholder established when D28 locked the discriminator strings.

Shape: Items: ObservableCollection<ComponentViewModel> only. No layout properties (orientation, spacing, alignment) — these are surface concerns. The bound rendering surface (Stack.razor for Blazor; a VerticalStackLayout-backed template for XAML post-AB#2087) owns layout entirely.

Why not widen ListViewModel: ListViewModel (D10/D11) is a Track B domain control — it models a typed date · headline · subtitle row for domain-specific use cases (Did You Know list, notification rows). It belongs in Track B (domain controls), not Track A (server-driven scaffolds). Widening it to carry ObservableCollection<ComponentViewModel> would cross the D17 Track A/B boundary and give a domain control a generic container role. A new Track A type is the correct extension.

Precedent: Same pattern as GridContainerViewModel (D25) — a new ContainerSurfaceViewModel subclass rather than widening an existing type.

Source files changed: ComponentViewModels.cs (new class) · Stack.razor (new component) · ComponentView.razor (dispatch case added).

D35 — Spacing/* tokens included in theme endpoint payload at base defaults; MEC-variable server-side
Decision: Adopted 2026-06-26. Spacing tokens (Spacing/xtiny through Spacing/xxlarge and Spacing/page-margins) are included in every ThemeResponse payload, set to ALPA base values for all MECs by default. A MEC can override them server-side without an app store release.
Why: The alternative — keeping spacing as a frontend-only constant — would require an app deployment any time a MEC wanted denser or looser spacing. Delivering spacing through the theme endpoint costs nothing extra on the client (it's already iterating the token map) and keeps future layout flexibility in the server. Same reasoning applied to Font/* (D33) and all other theme tokens.
MEC-variable today: No MEC currently overrides spacing. All MECs receive identical ALPA base values. The mechanism is in place for future customization.
Token set added: Spacing/xtiny (4 px) · Spacing/tiny (8 px) · Spacing/xxsmall (12 px) · Spacing/xsmall (16 px) · Spacing/small (20 px) · Spacing/medium (24 px) · Spacing/large (28 px) · Spacing/xxlarge (40 px) · Spacing/page-margins (16 px). Nine tokens; total payload rises to 39.
Closes: OT-5 in theme-endpoint-contract.html. DQ-12 (Figma spacing annotations) remains open for confirming token names against the Figma library file, but the architectural decision to include spacing in the payload is settled.

✓ Applied (2026-06-26): theme-endpoint-contract.html token table + example payloads updated. design-tokens.html namespace table updated. OT-5 closed.

D34 — Text/On-Brand is MEC-themeable; must use DynamicResource
Decision: Adopted 2026-06-26. Text/On-Brand is not a static white — it is an MEC theme override slot. Base value: #ffffff. UAL override: #bed6fb (light periwinkle blue, per MEC Figma node 20657:502). All templates that render text on brand-colored surfaces must bind via DynamicResource to the Text/On-Brand token, not to a hardcoded hex.
Why: UAL's brand background is a medium-to-dark blue (not the near-black ALPA navy), so pure white text passes WCAG but the design team has chosen a softer contrast. Hardcoding white would break UAL's spec and prevent future per-MEC overrides.
Scope: Applies to: primary button labels, NavBar-bottom labels and icons, any text sitting on a Surface/Brand or per-MEC brand fill. Does not apply to Surface/Default (white) backgrounds — those use Text/Primary.
Theme endpoint: The theme response delivers text-on-brand as part of the per-MEC color block. The client applies it as a CSS custom property or XAML merged dictionary. See theme-endpoint-contract.html.

Applied (2026-06-26): Decision recorded. theme-endpoint-contract.html update pending (Task 6).

D33 — Font-family as MEC theme token: Font/Heading and Font/Body slots
Decision: Adopted 2026-06-26. Font-family is a per-MEC theme override. Two token slots are added to the theme endpoint: Font/Heading and Font/Body.
Base (ALPA): Futura PT Book (weight 400) for body; Futura PT Bold (weight 700) for headings. No other weights are available — the procurement covers exactly two files: FuturaPTBook.otf and FuturaPTBold.otf. Anything labeled "heavy" or "medium" in the Figma variable names refers to optical-weight within these two files, not separate font files. To achieve visually heavier text at the same font size, use Bold (700). Do not specify weight values other than 400 or 700 for Futura PT.
Per-MEC:
  • UAL: League Spartan — both Heading and Body slots (web font, publicly available)
  • DAL: Bebas Neue for Heading slot; League Spartan for Body slot (per Figma node 21247:2907)
  • FDX: TBD — open as DQ-14 in design-questions.html
Why: Each MEC airline has its own brand typography. The base Futura PT purchase is ALPA-only; airline-specific fonts are injected at theme load. Keeping font-family in the theme token slot means the component library never needs to know which airline it is rendering for.
Weight constraint: Per-MEC fonts may provide their own weight files. Where only a single weight file exists, bold simulation (font-synthesis) is not acceptable — ask the design team to use a font that has a Bold variant. DQ-14 covers this for FDX.

Applied (2026-06-26): Decision recorded. DQ-14 filed in design-questions.html. theme-endpoint-contract.html Font/Heading + Font/Body slots pending (Task 6).

D32 — Neutral naming principle: Figma platform-specific names normalize to ALPA-neutral names
Decision: Adopted 2026-06-26. When Figma uses Apple HIG component names, iOS-specific suffixes, or typographic artefacts, normalize to a platform-neutral ALPA name in all specs, docs, and code. The Figma name is noted parenthetically for traceability but the ALPA name is authoritative.
Mapping table:
Figma name (raw)ALPA-neutral nameReason
Toolbar - Bottom - iPhoneNavBarBottomApple HIG label; platform-neutral name used everywhere
Toolbar - Top - iPhoneTopNavApple HIG label; platform-neutral name used everywhere
name-ogname (suffix dropped)-og = "original" — Figma artefact; D13 established drop rule
manu-hamburgerHamburgerMenuTypo in Figma ("manu" = "menu"); correct to standard term
btn-flight finder interior navInteriorTabNavFigma name encodes context; neutral reusable name preferred
Principle: Names should describe what a component is, not where Figma originally placed it or what Apple calls it. Platform-neutral names survive a future Android or web target without renaming.
Naming authority: This file is authoritative. figma-component-specs.json holds the Figma name as figmaName and the ALPA name as the object key.

Applied (2026-06-26): Decision recorded. Normalization will be applied during the next figma-component-specs.json Phase 3 pass.

D31 — Favorite heart rendered on all favoritable controls (amends D7)
Decision: Adopted 2026-06-26. The per-element favorite heart visual (i-heart, 18×18) IS rendered on all favoritable components. Amends and supersedes D7 (2026-06-05 — "favorite heart not rendered").
Favoritable controls: CardSmall (card-sm) · CardText / TextCard (card-md) · Card / CardHero (card-lg) · List (listy card generic) · ButtonCard (card-btn). The i-heart in Button and ButtonGroup is the button's own icon image, not a favorites toggle — those are unaffected by this decision.
Why: Design team direction (2026-06-26) — the Figma design always showed the heart on these controls. D7 was a temporary scope reduction that has been reversed. The Figma i-heart layer is intentional and faithful to the design.
IsFavorite state: Remains client-side (Settings.FavoriteItemIdsJSON)... Superseded 2026-07-08: the backend Favorites API (Change 3) delivered — IsFavorite is now sourced from the live API (IFavoritesQueries), not local storage.
Future imports: Do NOT drop i-heart on future Figma imports — it is intentional. The D7 import note ("drop the heart visual") is rescinded.

✓ Applied (2026-06-26): Heart indicator reinstated in all affected component specs and property-mapping docs (card-small, small-card, card-text, text-card, list, domain-controls, screen-mapping, home-screen-data-gap, home-page-audit, membership-screen-data-gap, backend-api-mapping-report, index). design-tokens.html import note updated. design-questions.html status updated. Supersedes D7.

✓ Implemented (2026-07-08, WI 2182): Interactive heart-toggle wired (auth-gated, ShowFavorite/IsFavorite/OnFavoriteToggle parameters) in CardSmall.razor, HeroCard.razor, Card.razor, and ButtonCard.razor. Not yet wired: List.razor — flagged as a remaining gap, not silently dropped. CardText/TextCard also not yet wired (no consuming screen needed it this pass).

D36 — Sort authority: server pre-sorts; IsSortable=false locks position; user may reorder otherwise
Decision: Adopted 2026-06-26. The server delivers PageDto.Containers and ContainerDto.Items pre-sorted in admin-defined order. That order is authoritative. Device users may reorder containers and items within their local cache. IsSortable=false on a ContainerDto or ItemDto locks that element's position — the user cannot move it. Default is true.
Comms control: The comms/content team sets IsSortable=false per item when pinning is required. This gives the comms team guaranteed placement without blocking user personalisation of other items.
Full contract: dynamic-feed-api-contract.html §3.2 (ContainerDto) and §3.3 (ItemDto).
D37 — Token fields (BackgroundToken, PaddingToken, CornerRadiusToken) are theme-derived, not editor-set
Decision: Adopted 2026-06-26. BackgroundToken, PaddingToken, and CornerRadiusToken in ContainerDto are populated server-side from a component-type → theme mapping. Content editors do not set them directly. They are global theme decisions. Fields are retained explicitly in the DTO for backwards compatibility — if a future admin UI exposes limited controls, the surface is already there.
Why: Tokens represent MEC brand decisions, not per-item editorial choices. Allowing editors to set raw token keys would undermine the theme system and require client-side validation. Server-side mapping keeps the theme authoritative.
D38 — CornerRadius: double?CornerRadiusToken: string?; BorderRadius/* namespace added to theme endpoint
Decision: Adopted 2026-06-26. ContainerDto.CornerRadius (a raw double? pixel value) is renamed to CornerRadiusToken (a string? semantic key, e.g. "BorderRadius/M"). This makes it consistent with BackgroundToken and PaddingToken. The client resolves the key via the theme endpoint.
BorderRadius/* namespace: 6 tokens added to the theme endpoint: BorderRadius/None (0px), BorderRadius/S (4px), BorderRadius/M (8px, default fallback), BorderRadius/L (12px), BorderRadius/XL (16px), BorderRadius/Full (50%). Base values apply to all MECs today; MEC-overridable server-side.
D39 — WidgetDtoItemDto; WidgetComponentFactoryItemComponentFactory
Decision: Adopted 2026-06-26. The leaf DTO class is renamed from WidgetDto to ItemDto for consistency: the containing property is ContainerDto.Items (not Widgets), and the factory is ItemComponentFactory. The JSON field name "items" is unchanged. The Blazor alignment doc references the pre-rename name as it describes the existing codebase being refactored.
D40 — Domain controls are standalone implementations composing shared atoms + tokens; no shared base substrate; component library is opt-in
Decision: Adopted 2026-06-29. Divergent domain controls are implemented as standalone controls rather than inheriting a shared base/substrate. This generalizes the D16/D17 rationale (FlightCardView is its own ContentView, not force-fit into CardViewModel): once a control's shape/fields diverge past the DQ-9 reuse threshold, it gets its own implementation.
Shared layer = atoms + tokens, not a base class: standalone controls compose shared atoms (StatusBadge, EmptyState) and design tokens (color · spacing · typography) for visual consistency. What is rejected is a shared base class/wrapper that couples unrelated controls — the atoms/tokens are a shared vocabulary, not a substrate. (Prevents drift, e.g. the per-page --kcm/--cass badge-color reuse the scaffold accumulated.)
Library is opt-in: the component library is for recurring, data-driven controls. One-off static / informational content (e.g. login copy, advocacy blurbs) may be plain markup and is exempt — it is not forced through the library.
Saved-cards application (resolves the reconciliation open question): Saved Search Card is a standalone control (a saved query); Saved Flights reuses Flight Segment Card by composition where the Figma frame matches (else standalone); both compose the shared action-column atom. There is no "Saved Item Card" base control.
D41 — flight finder filter and sort: duplicate node confirmed — 4450:11374 canonical, 4705:8702 flagged as stale copy
Decision: Adopted 2026-07-02. The live Figma file (owEYzHf7FrHRvWC2u82UOl) carries flight finder filter and sort as a COMPONENT_SET at two node IDs: 4450:11374 and 4705:8702. A full structural + text diff shows the two are byte-identical — same 2 states (Property 1=filter 393×499, Property 1=sort 393×480), same child instances, same text content (Airline / Aircraft / Flight Status / Aircraft filter rows; Arrival Time / Departure Time / Connection Time / Number of Connections / Total Trip Time sort options). This rules out "different variant states" or an "in-progress split" — it is a genuine duplicate. 4450:11374 is adopted as canonical; 4705:8702 is flagged for designer cleanup and is not catalogued as a separate library entry.
How canonicality was determined: Figma node-ID session numbers trend chronologically within a file. 4450:11374's two children were born in two different sessions (filter = 4370:8880, older; sort = 4450:11375, added later) — consistent with a master that was built up incrementally over time, the normal growth pattern. 4705:8702's two children (4705:8703, 4705:8715) were both born together in one later session — consistent with a single duplicate/paste operation performed after 4450:11374 already existed in full. This was originally an evidence-based call pending design-team confirmation.
Human confirmation (2026-07-06): Jose Castro reviewed both nodes directly in Figma and confirmed 4705:8702 is a duplicate of 4450:11374. The evidence-based call above is now verified, not inferred — no further designer sign-off needed on the duplicate-node question itself.
Likely provenance: This component is probably the live-file successor to the previously binary-only overlay-flight finder filter reference (screen-mapping.html, node 0x44de70, from the now-dropped .fig binary extraction) — the Filter state's Apply/Clear CTAs and check/radio form controls match that earlier overlay note exactly.
Composition: domain-control tier. Composes flight finder filters (D22, existing domain-control — 3 instances in the Filter state, one per field) + radio button (5 instances in the Sort state) + tab group (Sort/Filter pill toggle) + nav controls (close X) + Primary button (Apply) + Underline Button (clear). Code name: FlightFinderFilterSort.
Domain binding confirmed (2026-07-06): already fully implemented — JumpseatFlightFinderResultsFiltersPageViewModel.cs. Every filter/sort field the Figma capture lists (Airline / Aircraft / Flight Status filters; Arrival Time / Departure Time / Connection Time / Number of Connections / Total Trip Time sort) exists as a bound property there, driven by an in-memory FilterSettings object and broadcast via FilterSettingsHaveChangedMessage. Filter choices (airline/aircraft lists) come from the existing flight-search response (flightSearchResponse.Airlines, JumpseatFlightFinderResultsPageViewModel.cs), not a separate endpoint. No backend gap — confirmed by a doc-audit assessment (2026-07-06) that checked this and 4 other newly-synced components (D42, D44, D47, D48) for unresolved backend work; only D43/Change 5 needs backend action.

Status: Adopted 2026-07-02 · Confirmed 2026-07-06 · domain binding confirmed 2026-07-06. figma-component-specs.json: flight finder filter and sort entry added (canonical id 4450:11374, _duplicateNode: 4705:8702). Spec: flight-finder-filter-sort-component.html. No open items.

D42 — search secondary buttons: resolves D22's open TBD — scaffold/atom tier, code name SearchSecondaryButton
Decision: Adopted 2026-07-02. D22 (2026-06-23) provisionally classified search secondary buttons as domain-control tier with code name TBD, pending confirmation of whether it "carries domain data." Live capture (componentKey region, node 4381:10609) shows both Property 1 variants carry identical generic content — an icon-generic placeholder glyph + a "Recent" text label, no flight/domain-specific fields. Resolved: scaffold/atom tier, code name SearchSecondaryButton — a reusable secondary-action chip (icon + label). "Recent" is example content; the same chip is expected to also render "Favorites" and similar quick-access affordances on the search/JSFF screen.
Dimensions: Default 103×38, Variant2 79×63 (component_set overall 143×121). Fill #5d6471, stroke #05273e.
Rationale: D22's own note allowed for this outcome ("may be scaffold/atom if domain-agnostic"). No domain-specific field was found in either variant, so the domain-control classification is not supported by the evidence.

Status: Adopted 2026-07-02. figma-component-specs.json: search secondary buttons entry added. design-tokens.html 2026-06-23 New Components table row updated (tier + code name + dimensions). domain-controls.html: placeholder stub removed from the domain-controls catalog (reclassified out). Spec: search-secondary-button-component.html.

D43 — banner: domain-control tier, code name PromoBanner — maps onto the existing PageBanner API
Decision: Adopted provisional 2026-07-02, domain source confirmed 2026-07-06. banner (component_set, node 5512:12925) is a new master not present in the 2026-06-17 or 2026-06-23 sweeps. It is a dismissible promotional/announcement banner: sample content is a UPA27 union survey promo ("Survey Now Open" / "Closes June 10 at 11:59PM PT"), a logo image, and a close (X) control. The two Property 1 states are a layout re-order (close-left vs close-right), not different content or a size variant. Classified domain-control tier — the title/subtitle/image are dynamic content, analogous to notification card flight status (D22).
Dimensions: Default 319.4×56, Variant2 309.4×56 (component_set overall 346.4×172). Fill #05273e (navy) on both states — a solid fill, not a background photo.
Domain source confirmed (2026-07-06): not a new Dynamic Feed ItemDto and not a dedicated announcements/surveys service — the two candidates originally floated. This is the richer version of the existing, already-shipped PageBanner model (ALPAMobile.Domain/Data/Models/PageBanner.cs), served by GET /api/pagebanner/list and resolved per page in BannerPageViewModel via PageBanner.GetBannerForPage(PageName, banners) — a generic per-page banner slot already used by ~20+ pages (Home, Jumpseat, Known Crewmember, Member Resources, MEC/LEC, Flight Search, Settings, Notification Center, Advocacy). Today's implementation renders it as a single flat image (FeaturedImageLink) with the title/subtitle baked into the image pixels — confirmed live in the app, no separate text fields exist yet. This also resolves the D40 "static content, exempt from the library" question raised below: it is not one-off content — it's a recurring, already-multi-page pattern, so it correctly stays a catalogued domain control. Required field additions (Title, Subtitle, LogoImageLink) and the open dismiss-persistence question are tracked as Change 5 in the Backend API Mapping Report.

Status: Adopted provisional 2026-07-02 · domain source confirmed 2026-07-06 · dismiss behavior resolved 2026-07-06 (client-side only — per-session by default, per-device local storage if permanent dismiss is needed; no backend involvement). figma-component-specs.json: banner entry added. Spec: promo-banner-component.html. Remaining open item: the PageBanner API field additions (Change 5) — additive, not yet requested from backend.

D44 — expander-flight finder-advanced search: confirms the Expander's open content gap; new domain-control layer FlightFinderAdvancedSearch
Decision: Adopted 2026-07-02. The existing Expander primitive spec (captured 2026-06-26 from the dated snapshot file) carried an explicit open gap: "Expanded content not shown in Figma screen node ... not confirmed from the current screen capture." Live capture of this component_set (node 4361:5537) confirms the expanded content: Avoid Connection field, Connect Via field, Min Connection Time range slider, Length of Travel range slider (both reuse the existing slider master, 4317:5239, already in figma-component-specs.json), three checklist rows (Search by Arrival Date / Include Cargo Carriers / Show Codeshare), and a Secondary button labelled "Save Search to Favorites" (collapsed-then-expand copy shortens to "Save Search" in the open state).
Layering: The generic Expander primitive (trigger + slot, unchanged tier) stays domain-free. The specific "Advanced Search" instance — with the confirmed flight-search field set — is modelled as a new domain-control, FlightFinderAdvancedSearch, that composes Expander + Master Form Field + slider + checklist + Secondary button. This follows the same domain-control-composes-scaffold pattern as flight finder filters (D22) — same domain (JSFF flight-search filter state).
Domain binding confirmed (2026-07-06): already fully implemented — JumpseatFlightFinderSearchPageViewModel.cs. Every field matches 1:1: AvoidConnectionCities, ConnectVia, MinConnectionTime, MaxLengthOfTravel, IsSearchByArrivalDate, IncludeCargo, ShowCodeshare — all map directly into the existing FlightSearchRequest sent to the real flight-search API. No backend gap (same doc-audit assessment as D41).
Dimensions: Default (collapsed) 345×71, Open (expanded) 345×693 (component_set overall 385×828). Fill #efefef on both states.

Status: Adopted 2026-07-02 · domain binding confirmed 2026-07-06. figma-component-specs.json: expander-flight finder-advanced search entry added. expander/expander-component.html updated — open-content warning resolved, Usage table updated, links to the new domain control. Spec: flight-finder-advanced-search-component.html. No open items.

D45 — card-duty period confirmed retired (absent from live file); card-duty period-alt confirmed present (filed under FTDT, no change)
Decision: Adopted 2026-07-02. A full-file search of the live Figma file (owEYzHf7FrHRvWC2u82UOl, "app" page) for any node named card-duty period or card-duty period-alt returns 16 matches, all card-duty period-alt — the master component_set (4617:13876, in "components ftdt" section 4543:5876, 3 states: Default 345×237 / Variant2-expanded 345×1073 / delete-demo 345×237) and its instances across the FTDT dashboard, Duty Periods list, archived Duty Periods, and the tablet variant. No occurrence of the bare card-duty period name exists anywhere in the live file.
Retirement confirmed, not new: This is not a new finding — the original adopted-naming table (§1, 2026-06-10 re-sync) already mapped the adopted name Duty Period only to Figma master card-duty period-alt, and domain-controls.html's Duty Period placeholder already cites only node 4617:13876 (3 states matching this capture exactly). card-duty period (non-alt) was never adopted as a separate component — this decision formally closes the "confirm retired or renamed" flag per the sync procedure's Removed-master rule, rather than introducing new scope.

Status: Adopted 2026-07-02. No figma-component-specs.json changes — card-duty period-alt's existing entries (from the dated-snapshot file, ids 5515:52/5515:55) are unaffected; this decision only confirms live-file disposition. No other doc changes required — existing domain-controls.html and naming-decisions-record.html §1 references were already correct.

D46 — button pill: live-file capture confirms the already-adopted Button (Pill) master; corrects a word-order transcription in the 2026-06-17 dispositions table
Decision: Adopted 2026-07-06. The gap audit flagged a live-file node 4245:5754 named button pill (COMPONENT_SET, 167×158 overall — Property 1=default 127×31 white/navy text, Property 1=selected 109×31 blue #007bc2/white text) as not matching the 2026-06-17 revision's dispositions-table entry, which was recorded as pill button (reversed word order). Comparing the two: this is the same component, not a rename. The dispositions table's word order was a transcription slip — the live Figma layer name has always been button pill, confirmed by (a) the three button pill instance children already present under the existing tab group entry's slot in figma-component-specs.json, unchanged since the original capture, and (b) the hand-authored spec page button-pill/button-pill-component.html (written 2026-06-26), which already used the correct word order and correct Figma provenance.
What's new here: the existing spec page cited only older, dated-snapshot instance node ids (5048:13572/5048:13573, file psH738AqHDxuMyFm897f9r). This sync adds the live-file master capture (4245:5754, file owEYzHf7FrHRvWC2u82UOl) as a second, more authoritative provenance record — the actual COMPONENT_SET with both variant states, rather than two separate instance ids.
No new tier/naming decision: code name remains Button (Pill) (ButtonPillViewModel). No dimension conflict — both states match the shape described in the existing spec page (segmented pill, active/inactive).

Status: Adopted 2026-07-06. figma-component-specs.json: new top-level button pill entry added with live node id and both states. button-pill/button-pill-component.html: live node id added alongside the existing dated-snapshot reference. screen-mapping.html registry: node updated from null to 4245:5754.

D47 — Toggle - Switch: new scaffold/atom, distinct from toggle-settings — the bare on/off switch toggle-settings composes
Decision: Adopted 2026-07-06. The gap audit flagged live-file node 4128:7733, Toggle - Switch (COMPONENT_SET, 104×120 overall — State=Off 64×28 fill #d2d4d6, State=On 64×28 fill #007bc2; both states are a Frame (21×10, AX label) + Knob (39×24, white)) as a node distinct from the already-catalogued toggle-settings (D23, node 4875:11208, code name ToggleSettings). Structural comparison confirms these are genuinely different components in a compose relationship, not the same node under two names: toggle-settings' own two states (Property 1=Default 299×28, Property 1=Variant2 244×44) each contain a Toggle - Switch INSTANCE sized 64×28 with the identical Frame(21×10)+Knob(39×24) structure as this master's On state, plus a sibling "Lorem ipsum dolor sit amet" label text node. In other words: toggle-settings = label text + an instance of this Toggle - Switch atom; this atom is the bare switch control itself.
Classification: scaffold/atom tier — the smallest reusable on/off control, no further composition. Code name: ToggleSwitch. Distinct from ToggleSettings (which composes it with a label and remains a separate, already-adopted catalog entry — its own tier/dimensions are unchanged by this decision).

Status: Adopted 2026-07-06. figma-component-specs.json: new top-level Toggle - Switch entry added. New spec page: toggle-switch/toggle-switch-component.html. screen-mapping.html registry: new entry added (node 4128:7733); existing toggle-settings entry (still node null) is unchanged — its own node-id gap remains open, out of scope for this sync. index.html: added to the Atoms spec grid.

D48 — flight card-saved & recent searches: live node confirmed for the already-adopted D21 consolidation; two sibling usage frames documented, not separate masters
Decision: Adopted 2026-07-06. D21 (2026-06-23) adopted flight card-saved & recent searches (componentKey 9dfaa1ba…) as the consolidated component_set superseding three placeholder JSFF entries, but recorded no node id (figma-component-specs.json never got a top-level entry; screen-mapping.html's registry carried "node": null). The gap audit's live-file node 4450:9884 closes this: COMPONENT_SET, 393×465 overall — Property 1=Default 353×61 (collapsed/compact card: trip row + Underline Button 320×38 fill #efefef + paramaters [sic] rows) and Property 1=Variant2 353×340 (expanded parameters view, Underline Button unfilled).
Two sibling usage frames, not separate masters: the audit also flagged two adjacent live-file nodes as "already-known separate frames" — flight card-saved searches (FRAME, node 4450:10395, 353×115) and flight card-recent searches (FRAME, node 4450:11208, 353×115). Direct structural capture shows both are identically structured composites: a swipe panel + an INSTANCE of this D21 master's Default state (353×61) + an expanded detail flight-leg panel (234×1066, matching flight detail flight info content). The only difference between the two frames is the swipe-reveal action icon — i-trash (delete, red #c02126) on the "saved searches" frame vs i-favorites (save, #464b55) on the "recent searches" frame. Both are FRAME type, not COMPONENT/COMPONENT_SET — i.e. they are not published library masters; they are per-context usages of the one master, exactly as D21's original rationale predicted ("the variant breakdown is handled as states within the component_set, not separate scaffold masters").
Stale entries confirmed superseded: the three 2026-06-17 placeholder entries in figma-component-specs.json (flight card-recent searches, old id 4441:398; flight card-saved flight, id null; flight card-saved searches, id null) are confirmed retired by this capture — none of their old ids match the current live structure, and the live file's actual shape (one master + two usage frames) fully accounts for the "saved flight / saved searches / recent searches" distinction without needing three separate scaffold masters.

Status: Adopted 2026-07-06, confirms D21. Tier unchanged (domain-control). Code name unchanged: FlightCardSavedAndRecentSearches. figma-component-specs.json: new top-level entry added with confirmed node id, dimensions, and a _usageFrames provenance note for the two sibling frames. screen-mapping.html registry: node updated from null to 4450:9884. domain-controls.html: placeholder stub added. design-tokens.html: dimensions updated from TBD to confirmed. architecture.html §6: cross-reference added. Stale-language audit (2026-07-06): flight-segment-card-component.html's Figma Variant Map table still showed the three superseded entries as active/TBD rows, missed by the original D48 roll-up — now updated to mark them superseded and add the consolidated master's own row.

D49 — button link ("Forgot Member Number"): one-off screen content, not catalogued — per D40
Decision: Adopted 2026-07-06. The gap audit flagged live-file node 5251:15088, button link — a FRAME (not a COMPONENT/COMPONENT_SET — no published library master exists at this node), 218×24, containing the text "FORGOT MEMBER NUMBER" (203×24, #007bc2) and a small chevron/caret vector icon (9.4×8.1, #007bc2). This sits on the Log In screen next to the "Member Number or ALPA Email" credential field (see login-screen-data-gap.html), as a sibling to the already-documented, likewise-uncatalogued "Forgot Password" link.
Resolution: not catalogued — one-off, screen-specific static content, exempt from the library per D40 ("one-off static/informational content... may be plain markup"). Two supporting facts: (1) it is a plain FRAME, never published as a reusable Figma component/component_set; (2) its sample content is real, specific screen copy ("FORGOT MEMBER NUMBER"), not a lorem-ipsum placeholder — the same treatment already given to "Forgot Password" on the same screen. This is the mirror-image case of D43 (banner), where D40 was raised as a candidate exemption but the evidence (recurring, ~20+ page usage) pointed the other way, toward cataloguing. Here the evidence (unpublished frame, one specific screen, parallels an existing uncatalogued precedent) points toward exemption.
No new tokens: both styles used are already tokenized — the text uses the Mobile Button Text style (design-tokens.html --font-mobile-button) and the Aqua Blue fill style resolves to #007bc2, already tokenized as Text/Link / --text-links. No design-tokens.html changes required.

Status: Adopted 2026-07-06. figma-component-specs.json: entry added for provenance, tier screen-out, explicitly marked not-catalogued (no spec page created). login-screen-data-gap.html: cross-reference added alongside the existing "Forgot Password" note. No domain-controls.html / index.html / architecture.html changes (nothing to catalogue).

D50 — card-button: syncs figma-component-specs.json / screen-mapping.html to the already-adopted live-file node from DQ-5 (2026-06-22); adds a data point to the existing "CTA button height: TBD" gap
Decision: Adopted 2026-07-13. A 2026-07-13 intake scouting pass flagged live-file node 4104:5451 (component_set, file psH738AqHDxuMyFm897f9r) as an uncatalogued second card-button master alongside the existing D14 entry (5452:434, dated-snapshot id from the 2026-06-17 .fig capture, size: null). Cross-check found this is not a new component4104:5451 is the exact node button-card-component.html and design-tokens.html already cite as the source for card-button's confirmed dimensions, resolved back on 2026-06-22 as DQ-5 ("Live Figma metadata (node 4104:5451) confirms two variants — Default 353×189px, Variant2 353×129px"). This is the same dated-snapshot-vs-live-file split as D46's button pill case: the .fig binary capture assigned card-button the id 5452:434; the live MCP capture assigns the same component 4104:5451. figma-component-specs.json's top-level entry and screen-mapping.html's registry were simply never updated when DQ-5 resolved elsewhere — an internal-consistency gap, not an open design question about identity.
Data point for the existing open gap: button-card-component.html already carries an open CTA button height: TBD row. get_screenshot on 4104:5451 shows both variants' live example content (an "Accident Hotline" icon+title+body card with a plain bottom divider) with no distinct embedded button/action-label element visible — differing from D14's original description of instances following a "description paragraph + short action label" pattern (e.g. "Secure your flight with Jumpseat. / Jump Seat"). This doesn't reopen the node/size identity (independently confirmed by 3 sources: DQ-5, design-tokens.html, button-card-component.html) but is worth the design team's attention when the existing CTA-button-height TBD is resolved — the master's own example content may simply be generic placeholder that real instances retarget, or the CTA button may only render conditionally.

Status: Adopted 2026-07-13. figma-component-specs.json: D14's card-button entry size filled in (353×189 Default / 353×129 Variant2, from the already-confirmed live node); _flag converted to _note citing D50 and the live node id. screen-mapping.html registry node updated from the stale 5452:434 to 4104:5451. button-card-component.html and architecture.html: stale 5452:434 references normalized to cite 4104:5451 as the live/authoritative node, with 5452:434 kept as the original dated-snapshot provenance. design-tokens.html already correct — no change needed. Remaining open item (pre-existing, not new): button-card-component.html's own CTA button height: TBD row — now with the added observation that the live example content shows no visible CTA element.

15. What Was Applied — 2026-07-13 Intake Sync

Figma Phase 1 capture pass triggered by a full intake sweep (Figma/Teamwork/Comms). Live file psH738AqHDxuMyFm897f9r. Of 5 items an initial scouting pass flagged as new/uncatalogued, 4 were false positives — the scouting pass compared only against this file's capped _resync.dispositions summary table (D32/2026-06-17) instead of the full JSON body, which already carried D33–D49 (resolved through 2026-07-06). banner=D43, expander-flight finder-advanced search=D44, search secondary buttons=D42, and button link=D49 were already resolved; the flight-finder-filter-and-sort duplicate was already resolved (D41); the button-pill naming question was already resolved (D46); all 5 foundation pages plus Icons were already captured in foundations-reconciliation.html (2026-06-29/06-30); all 3 MEC per-airline pages were already re-checked (2026-07-06). The fifth item, card-button, was likewise a false alarm on "new master" but surfaced a real doc-sync gap (D50).

D51 — EmptyState atom implemented: corrects the 2026-06-29 spec-only draft's Icon/CTA/target-project shape against real usage (WI-2255)
Decision: Adopted 2026-07-14. D40 (2026-06-29) named EmptyState as one of two shared atoms (alongside StatusBadge) that standalone domain controls compose. A spec-only draft was written the same day, before any real consumer was audited. Implementation (WI-2255, Component Library Reconciliation follow-up) found and fixed three shape mismatches between that draft and the 4 real hand-rolled consumers it was meant to dedupe (FavoritesPage.razor ×2, JumpseatSavedFlightsPage.razor, JumpseatSavedSearchesPage.razor, DocumentsListPage.razor ×2):
  • Icon is optional, not required — 2 of the 6 real usages (both DocumentsListPage.razor instances) render no icon at all. The original draft's Icon: string (required) would have forced a placeholder on those.
  • CTA is a generic RenderFragment? ChildContent slot, not a fixed CtaLabel/OnCta button pair. None of the 4 real consumers use a CTA today — a fixed button API would have been unvalidated speculation. The generic slot matches the already-shipped Expander.razor content-slot precedent (same codebase, same pattern, already proven).
  • Target home is ALPAMobile/Components/Library/ (the actual live Blazor component location, globally imported via _Imports.razor's @using ALPADocs.Components.ViewModels) — not ALPAMobile.HybridUi. (Clarified 2026-07-15: HybridUi is the pre-development prototype codename for the shared component-library RCL, used throughout the planning-era spec docs' "Target home" lines — not an authoring error as this entry originally implied. No project by that name was ever created; the RCL materializing under that plan is ALPAMobile.Presentation per D20/WI-2254.) This atom follows the current dual-write convention pending WI-2253's broader namespace-consolidation decision. (Consolidation landed: since D61 (2026-07-15, WI-2254) the single home is ALPAMobile.PresentationComponents/Library/ for razors, ViewModels/ for the ViewModels — and the dual-write convention is retired.)
Tier correction: the original draft's badge read "primitive," contradicting D40's own text ("compose shared atomsStatusBadge, EmptyState"). Corrected to scaffold/atom to match D40.
Not a new naming/tier decision — D40 already named and tiered this component. This decision records the implementation-shape corrections only: a prior spec-only doc's assumptions didn't survive contact with the real codebase (same category of fix as the docs-only Component Library Reconciliation pass's card-button node-sync finding, tracked separately on docs/AB1821-intake-sync-01).

Status: Adopted 2026-07-14. EmptyStateViewModel added to both ALPAMobile/Components/ViewModels/ComponentViewModels.cs (live) and ALPAMobile.Presentation/ViewModels/EmptyStateViewModel.cs (staged copy). EmptyState.razor added to ALPAMobile/Components/Library/, registered in ComponentView.razor's dispatcher. .alpa-fav-empty* CSS renamed to .alpa-empty-state* in alpa-components.css (4 consuming pages rewired, 0 remaining references). Spec: empty-state-component.html corrected in place.

D52 — StatusBadge atom implemented for the 6 light-surface consumers; dark-surface fdetail-badge flagged, not merged (WI-2255)
Decision: Adopted 2026-07-14. D40 (2026-06-29) named StatusBadge as one of two shared atoms. A spec-only draft was written the same day, before real consumers were audited, and included a Size parameter (Sm/Md) meant to let one Kind-driven component cover both the light-surface alpa-policy-badge* pages and the dark-surface alpa-fdetail-badge* usage on JumpseatDetailsPage.razor.
Real-code check found a semantic conflict the draft didn't anticipate: Kind=Kcm resolves to a different colour depending on surface — light-surface Kcm is a green tint (#e8f5e9/#2a8a4c, "eligibility marker" meaning), but the dark-surface "KCM Eligible" badge on the flight-details hero is solid blue (var(--alpa-blue)/white). The same enum value would need to mean two different colours depending on a second parameter (Size) — this breaks the component's own stated principle ("a badge's colour is chosen by its meaning") and the D40 rule it exists to enforce ("never reuse another domain's badge class for its colour"). Making Size silently override Kind's colour would reproduce the exact drift this atom is supposed to eliminate.
Scope of this decision: implement StatusBadge for the 6 real light-surface consumers only (JumpseatAirlinePoliciesPage, AdvocacyPage, MecEventsPage, MecHotelsPage, KcmAirlinesPage, KcmPoliciesSearchPage — 8 badge usages total). The dark-surface alpa-fdetail-badge* usage (JumpseatDetailsPage.razor, 2 badges: "KCM Eligible" + a plain date chip) is left untouched, not merged in. Size is not implemented.
Not resolved, flagged: whether the dark-surface Kcm badge is genuinely the same semantic concept as the light-surface one (and needs its own theming-aware colour resolution once MEC theming lands) or is a distinct concept that happens to share a label — needs design confirmation before consolidating. Per the Component Library Reconciliation Procedure's rule to surface risks for a human decision, not resolve them unilaterally.
Also added: MarginTop bool parameter, replacing the scaffold's alpa-policy-badge--mt spacing modifier (used by 2 of the 6 real consumers, MEC Events and MEC Hotels).

Status: Adopted 2026-07-14. StatusBadgeViewModel + StatusBadgeKind enum added to both ALPAMobile/Components/ViewModels/ComponentViewModels.cs (live) and ALPAMobile.Presentation/ViewModels/StatusBadgeViewModel.cs (staged copy). StatusBadge.razor added to ALPAMobile/Components/Library/, registered in ComponentView.razor's dispatcher. .alpa-policy-badge* CSS renamed to .alpa-status-badge* in alpa-components.css (6 pages rewired, 0 remaining references). .alpa-fdetail-badge* untouched. Spec: status-badge-component.html corrected in place.

D53 — ActionColumn atom built for the first time; not a fixed heart/bell/X trio (WI-2257)
Decision: Adopted 2026-07-14. Both D40 and the Saved Search Card spec reference "the shared action-column atom" (heart/bell/X) that Saved Search Card and Saved Flights compose — but it was never built. Building it as the WI-2256 prerequisite.
Real-code check found 3 different action pairs, not a fixed 3-icon set: checking the 3 hand-rolled consumers (JumpseatRecentSearchesPage.razor, JumpseatSavedSearchesPage.razor, JumpseatSavedFlightsPage.razor) found each has exactly 2 action buttons, but the pair differs per page — Recent Searches: heart-toggle "save" (♡/♥) + "search again" (↻); Saved Searches: static-heart "remove" (♥, no toggle) + "search again" (↻); Saved Flights: bell-toggle "alert" (🔔/🔕) + "delete" (✕). No fixed heart+bell+X trio exists anywhere. The atom is modeled as a generic Items collection instead — each consumer supplies its own icon, label, and click behavior via an ActionKey string, rather than the component prescribing named slots.
CSS consolidation: the scaffold had two near-identical action-row blocks (alpa-saved-card-action-btn* and a byte-identical duplicate under alpa-saved-flight-card-actions) plus a colour/size coupling (--delete was both red and smaller) that doesn't generalize — a future compact-but-blue or full-size-red action wasn't expressible. Renamed to alpa-action-column* with Variant (Neutral/Accent/Highlight, colour) and Compact (size) as independent, orthogonal parameters.
Scope of this decision: the atom itself only — ActionColumnViewModel/ActionColumnItemViewModel, ActionColumn.razor, CSS. The 3 consumer pages are not rewired here; that happens in the WI-2256 follow-up (Saved Search Card / Saved Flights), so the click-behavior wiring lands together with the domain-control that owns it rather than being split across two commits.

Status: Adopted 2026-07-14. ActionColumnViewModel + ActionColumnItemViewModel + ActionColumnItemVariant enum added to both ALPAMobile/Components/ViewModels/ComponentViewModels.cs (live) and ALPAMobile.Presentation/ViewModels/ActionColumnViewModel.cs (staged copy). ActionColumn.razor added to ALPAMobile/Components/Library/, registered in ComponentView.razor's dispatcher (display-only — OnItemClicked isn't wired through that path). .alpa-saved-card-action-btn* / .alpa-saved-flight-card-actions CSS renamed to .alpa-action-column*. Spec: action-column-component.html. Amended by D54Icon's type changed from a raw glyph string to a typed asset enum.

D54 — ActionColumn Icon: raw glyph/emoji → typed SVG mask-icon asset (amends D53, WI-2257)
Decision: Adopted 2026-07-14. Live-validating the WI-2256 rewiring surfaced that Saved Flights' alert toggle rendered 🔔/🔕 — a raw Unicode emoji, not a themeable glyph. Unlike the row's other Unicode symbol characters (♡/♥/↻/✕, which inherit CSS color), an emoji carries its own fixed multicolour rendering and cannot follow ActionColumnItemVariant — it read as a stock emoji dropped into a flat-icon design system, not a matching icon.
Root cause: the scaffold's action-row icons were never on the app's real icon system. Everywhere else — TopNav.razor's alpa-top-nav-icon--*, the favorite heart (heart.svg / heart-selected.svg) on Hero/CardSmall/Button/Card — icons are exported Figma vectors applied via CSS mask-image + currentColor, so a single asset recolors per state/variant. The exported set already has exact matches confirmed against asset-inventory.html: heart.svg/heart-selected.svg (save), notifications.svg (alert — same asset TopNav uses), search-history.svg (documented there as "Recent-search affordance" — confirmed by its own aria-label="Search History", a magnifying glass + circular-arrow glyph), trash.svg (delete — the same i-trash referenced in the original saved-search-card spec's swipe-affordance note).
New ActionColumnIconAsset enum: Heart, HeartSelected, Notifications, SearchHistory, Trash — replaces ActionColumnItemViewModel.Icon's string type. This narrows D53's "fully generic" framing slightly (Icon is now a closed set, not arbitrary text) but that's a closer match to how every other icon in the app works — a governed set of exported vectors, not free-text glyphs — and keeps ActionKey/Title/Variant/Compact exactly as generic as D53 specified.
Known gap: Notifications has no separate "off" asset in the exported set (unlike heart's two-asset on/off pair) — the off state is colour-only (Neutral variant, same glyph). Documented interim limitation, same pattern as StatusBadge's D52 fdetail-badge caveat.
Mask-size technique: initial pass used mask-size: contain, which renders inconsistently across icons because the exports have slightly different aspect ratios (heart/bell/trash 22×22, search-history 21×23) and are edge-tight — each scales to fill its own viewBox at a different apparent size. Switched to the exact fixed-pixel-size technique alpa-top-nav-icon already uses (mask: var(--icon) center / 18px 18px no-repeat, 12px for the --compact modifier) — confirmed live, all four icons now read as one consistent size.

Status: Adopted 2026-07-14. ActionColumnIconAsset enum added to both ViewModel copies; ActionColumnItemViewModel.Icon retyped from string to the enum. ActionColumn.razor renders a masked <span> per item instead of glyph text. CSS: .alpa-action-column-icon + 5 --{asset} modifier classes added, using the alpa-top-nav-icon fixed-mask-size technique. All 3 Jumpseat pages (Recent Searches, Saved Searches, Saved Flights) updated to the new enum values. Live-validated on iOS simulator — all icons render correctly sized and themed, toggle/remove/delete behavior unchanged.

D55 — Saved Search Card + Saved Flight Card implemented; Saved Flights standalone, not Flight Segment Card composition (WI-2256)
Decision: Adopted 2026-07-14. Implements the Saved Search Card spec (spec-only since 2026-06-29). Two domain controls, not one: SavedSearchCardViewModel (Recent Searches + Saved Searches — a saved query, D40) and SavedFlightCardViewModel (Saved Flights — a saved flight). Both compose the shared Action Column atom instead of exposing named OnToggleSave/OnReSearch parameters as the original spec drafted — each page builds its own 2 ActionColumnItemViewModel entries and a single OnActionClicked callback switches on ActionKey, matching D53's generic-Items design rather than hard-coding per-page action semantics into the card itself.
Saved Flights deviates from D40's literal text. D40 (and this spec's original "Related" section) call for Saved Flights to reuse Flight Segment Card by composition. Verified against the real code: FlightCardView is a native XAML ContentView (Track B) — ALPAMobile/Components/ContentViews/FlightCardView.xaml — with no Blazor port anywhere in the codebase. Composition is not mechanically possible today. SavedFlightCardViewModel is a standalone Blazor implementation instead, matching the existing hand-rolled alpa-saved-flight-card shape exactly. Documented deviation — reconcile once Flight Segment Card gets its own Blazor port (separate, unscoped work); this spec's "Related — Saved Flights" section is corrected in place to describe what is actually implemented rather than the original composition plan.
CSS dedupe: .alpa-saved-flight-card-actions (byte-identical to .alpa-action-column, flagged as deferred cleanup in D53) removed now that the page composes <ActionColumn> directly — no wrapper class needed.
Not registered in ComponentView.razor's dispatcher: unlike the shared atoms (EmptyState, StatusBadge, ActionColumn), these two are page-composed domain controls with no ItemComponentFactory-driven dynamic-feed path — all 3 Jumpseat lists are local hardcoded mock records (no SavedSearch/SavedFlight domain type exists), same pattern D40 and D42/D43 established for other domain controls. Matches how PromoBannerViewModel/SearchSecondaryButtonViewModel — which are dispatcher-registered — are driven by real ItemComponentFactory data, a path these controls don't have yet.

Status: Adopted 2026-07-14. SavedSearchCardViewModel + SavedFlightCardViewModel added to both ALPAMobile/Components/ViewModels/ComponentViewModels.cs (live) and ALPAMobile.Presentation/ViewModels/ (staged copies). SavedSearchCard.razor + SavedFlightCard.razor added to ALPAMobile/Components/Library/. JumpseatRecentSearchesPage.razor, JumpseatSavedSearchesPage.razor, JumpseatSavedFlightsPage.razor rewired off hand-rolled markup onto the new components. Live-validated on iOS simulator — toggle-save, remove, search-again, toggle-alert, and delete all confirmed working on-device. Spec: saved-search-card-component.html, corrected in place.

D56 — ALPAMobile.Presentation ViewModel shape reconciled to the live copy; staged copy's typed tokens were unreconciled drift, not an upgrade (WI-2253)
Decision: Adopted 2026-07-14. A fresh, byte-accurate class-by-class comparison of ALPADocs.Components.ViewModels (legacy, live — every .razor renderer, ItemComponentFactory.cs, FavoritesViewModel.cs, and the unit tests bind to this copy) against ALPADocs.Presentation.ViewModels (staged, target RCL — confirmed zero .razor/production consumers) found the two had diverged well beyond what WI-2253's original finding described. Reconciled the staged copy to match legacy in every case below — legacy is authoritative; the staged copy's differences were incomplete/stale drift, not deliberate improvements to carry forward.
Correction to an assumption in this WI's own original framing: the staged copy's SurfaceViewModel/ContainerSurfaceViewModel had retyped BackgroundToken/Padding from string?/double to ThemeColor/SpacingToken enums. This reads like a "no magic strings" upgrade, but D26 (adopted 2026-06-24) already resolved BackgroundToken as a semantic token-name string (e.g. "Surface/Brand") specifically so XAML (IThemeResolver.Resolve(token)) and CSS (token.ToCssVar()var(--surface-brand)) can both resolve the same value from an open, server-driven, per-MEC token set — a closed C# enum can't represent that. Same pattern as D38's CornerRadiusToken. The staged enums are reverted to legacy's string/double shape, not adopted.
CardViewModel is concrete with ContentText, not abstract: the staged copy had made CardViewModel abstract and dropped ContentText entirely (pushing Title/Image onto the base instead). ItemComponentFactory.cs directly instantiates plain CardViewModel today — it cannot be abstract. Legacy's shape (concrete, HeaderText/ContentText/LinkText/Link, no Title/Image on the base) restored. IsLinkVisible reverted from staged's narrower Link-only check to legacy's Link OR LinkText — this exact property was already the subject of a docs-consistency fix earlier in this session's intake pass (pilot-card IsLinkVisible ASCII-diagram correction), reinforcing legacy's logic as the deliberately-audited one.
DocumentCardViewModel restored: staged had collapsed this abstract intermediate (HeroCardViewModel : DocumentCardViewModel : CardViewModel) by redeclaring Eyebrow/Description/AccentColor directly on HeroCardViewModel. It's explicitly named in the documented scaffold tree (D6), not incidental — recreated as its own file, HeroCardViewModel reverted to an empty terminal subclass.
Per-subclass Title/Image, not base-class: legacy pushes Title/Image down into DocumentCardViewModel, CardTextViewModel, and CardSmallViewModel individually rather than hoisting them onto CardViewModel. Staged's CardTextViewModel and CardSmallViewModel were relying on the (now-removed) base-class Title/Image — added directly to each, matching legacy. CardSmallViewModel.IconMaskUrl was missing entirely from staged; added — FavoritesViewModel.cs sets it and UnitTest/FavoritesViewModelTests.cs asserts it, a real functional gap, not a style choice.
Unbacked additions dropped: CarouselViewModel.Header (staged's rename of Title — no backing decision; ItemComponentFactory.cs and the D21-era decision text both use Title live today) reverted to Title. ButtonGroupViewModel.Title and ToggleSwitchViewModel.IsEnabled (staged-only additions with no corresponding legacy property, no consumer, no decision) removed. ButtonViewModel's local IsEnabled redeclaration and new ICommand? TapCommand re-exposure removed — both become redundant once the shared ComponentViewModel.IsEnabled exists and SurfaceViewModel.TapCommand is inherited normally, matching legacy exactly.
Ported (legacy-only, never staged): GridContainerViewModel (D25), StackContainerViewModel (D30), ActionButtonViewModel, WelcomeBannerViewModel — none had any staged counterpart at all. Added as new files, verbatim from legacy.
Base class: ComponentViewModel gained Id : string? (server-assigned reconciliation key — cache diffing, favorites, reorder persistence) and IsEnabled : bool, both missing from staged.
Scope boundary: this pass touches only ALPAMobile.Presentation/ViewModels/* — no legacy-copy changes (legacy is authoritative and live), no .razor file moves, no RCL-project conversion. A follow-up investigation found ALPAMobile.Presentation isn't actually configured as a Razor Class Library yet (no RCL SDK, no Microsoft.AspNetCore.Components package refs, zero .razor files exist there today) — that prerequisite conversion, plus the actual renderer migration, is deferred as separate work (WI-2254, description updated with this finding).

Status: Adopted 2026-07-14. All edits confined to ALPAMobile.Presentation/ViewModels/*. dotnet build clean on both ALPAMobile.Presentation.csproj and ALPAMobile.csproj (net10.0-ios); dotnet test UnitTest/UnitTest.csproj — 468/468 passing (the one test file touching either namespace, FavoritesViewModelTests.cs, binds only to the untouched legacy copy). No simulator validation needed — the staged copy has no .razor consumers yet, so nothing renders from it today; it is now correctly shaped and ready for whenever WI-2254 wires it up.

D57 — content-endpoint-contract.html §7 open items resolved/narrowed against real code; WI-2262's D11/DocumentHeroFactory claim corrected
Decision: Adopted 2026-07-15. WI-2262 (content-endpoint → shared UI component adapter layer) asked to resolve content-endpoint-contract.html §7's three open items before the adapter contract can be finalized. Checked each against real shipped code rather than speculating:
Favorites relationship — fully resolved, already shipped. Favorite.ItemTypeId (int — FavoriteItemTypes.MenuItem=1, Document=2, NotificationCenterMessage=3) + Favorite.ItemId (string) is the confirmed discriminator + reference pair (ALPAMobile.Domain/Data/Models/Favorite.cs). FavoritesViewModel.HydrateAsync resolves it via IMenuQueries.GetMenuItemAsync / IDocumentsQueries.GetDocumentByFileIdAsync and hand-builds CardSmallViewModel/HeroCardViewModel — working in production today, just not extracted into a reusable factory (that inline logic is the WI-2262 starting point for the adapter layer, not a gap in the contract itself).
Feed binding for dynamic-feed content — resolved, out of client scope. The dynamic feed delivers ItemDto, a server-pre-flattened "flat bag, not a union type" (dynamic-feed contract §3.3) discriminated by ItemDto.ItemType — not a raw MenuItem/DocumentItem passthrough. ItemComponentFactory.Create(ItemDto) already handles every component type in §4's mapping table. Whatever turns a MenuItem/DocumentItem into an ItemDto happens server-side — not a client adapter-layer concern.
MobileContent Item/envelope shape — narrowed, genuinely still open. No unified envelope exists for a client that wants to render real MenuItem/DocumentItem content outside both Favorites and the dynamic feed (e.g. a new MEC Blazor page listing real documents by Scope/Category directly). This is WI-2262's real remaining scope: extract FavoritesViewModel.HydrateAsync's inline MenuItemCardSmallViewModel / DocumentItemHeroCardViewModel mapping into reusable, named factories in ALPAMobile.Presentation or .Application, so both Favorites and any future direct consumer call the same code.
Correction to WI-2262's own evidence: WI-2262 states the shipped HeroCardViewModel (subclasses DocumentCardViewModel) contradicts "the one adapter explicitly designed in the docs, DocumentHeroFactory ... design decision D11." DocumentHeroFactory is not named anywhere in this record (naming-decisions-record.html) — that specific attribution to a decision entry doesn't exist. It is documented elsewhere (architecture.html, domain-controls.html, card-hero/card-hero-property-mapping.html) — but under the stale pre-rename type name CardHeroViewModel, not the shipped HeroCardViewModel. That's a separate, larger, pre-existing gap: a 2026-06-10 "type-first" rename decision (CardSmall / CardHero / CardText) was recorded with an explicit "docs need the rename ... follow-up pass" note that was never carried out — roughly a dozen spec files (including a dedicated card-hero/ pair) still say CardHeroViewModel against code that ships HeroCardViewModel. Out of scope for this pass; tracked separately, not by WI-2262. D11's composition-not-subclass rule is scoped to domain controls (its own example: Pilot Card = (UserInfo + DocumentItem) → CardViewModel via a mapper) — it does not govern the scaffold's own internal tier hierarchy. HeroCardViewModel : DocumentCardViewModel : CardViewModel is scaffold architecture under D6 ("Content cards derive from CardViewModel"), independently reconfirmed in D56 as "explicitly named in the documented scaffold tree (D6), not incidental." HeroCardViewModel subclassing is correct as shipped — no reconciliation needed there. Domain controls that compose the scaffold (Pilot Card, Saved Search Card per D55) remain correctly composition-based; the two patterns were never in conflict.

Status: Adopted 2026-07-15. docs/component-specifications/content-endpoint-contract.html §7 updated in place with these findings and code citations; status line updated. The adapter/factory implementation this cleared the way for landed the same day — see D58. Amended by D59: this entry's references to the "shipped name HeroCardViewModel" were accurate when written; the D4-revision rename was executed later the same day, so the shipped type is now CardHeroViewModel.

D58 — content→component adapter factories built (MenuItemCardFactory · DocumentHeroFactory); head-only placement, no staged Presentation copy (WI-2262)
Decision: Adopted 2026-07-15. Implements the adapter layer D57 scoped: FavoritesViewModel.HydrateAsync's inline content→component mapping extracted into two reusable factories in ALPAMobile/Components/, following ItemComponentFactory's exact precedent (sealed class, DI singleton, pure mapper — no IServiceProvider, no async; the data lookup stays with the caller, per the RawRepresentationFactory rule and D11).
MenuItemCardFactory: MenuItem → CardSmallViewModel — carries the web-servable-image check (native-only ImageSource names never reach the WebView <img>) and the MenuItemIconMask fallback so a menu-backed tile never renders an empty image box. The bare MenuItem type name is aliased in the factory (using MenuItem = ALPADocs.Data.Models.MenuItem) — the MAUI global usings make it ambiguous with Microsoft.Maui.Controls.MenuItem in the head project.
DocumentHeroFactory: DocumentItem → HeroCardViewModel, plus a static OpenRoute(fileId) that owns the /document-open?fileId= policy — previously duplicated verbatim in FavoritesViewModel and DocumentsListPage.razor (the P1 never-a-raw-Path-href rule now has one home). Create's link parameter is caller-supplied because fileId keying differs per context: Favorites keys by Favorite.ItemId, direct consumers by DocumentItem.FileID.
Head-only placement — deliberately no staged ALPAMobile.Presentation copy. The live component-library ViewModels the factories construct are head-only types (the same reason ItemComponentFactory and FavoritesViewModel live in the head). The dual-write convention used for ViewModels (D51–D55) was not extended to these factories: unconsumed staged copies are exactly the drift surface D56 spent a pass cleaning up, and ItemComponentFactory — the closest precedent — has no staged copy either. The factories migrate to Presentation together with the renderers in WI-2254.
Consumers rewired: FavoritesViewModel (both mappings + route builder; two new ctor dependencies, DI-registered as singletons next to ItemComponentFactory) and DocumentsListPage.razor's ResolveHref (route builder only — its row mapping targets ListViewModel, not a hero card, and stays as-is). The native-XAML DocumentsListPageViewModel.PopulateDocumentList named in WI-2262's evidence is not rewired: it maps to UnreadListItemViewModel (a native list-row VM with an embedded TapCommand) — a different output tier with behavior baked in, not servable by a pure component-library mapper.

Status: Adopted 2026-07-15. dotnet test 474/474 passing (6 new factory-contract tests in ContentComponentFactoryTests.cs; the 16 existing FavoritesViewModelTests pass unchanged as the behavior-preservation net). Live-validated on iOS simulator against production data: favorited a real document from /home-preview → hydrated on /favorites as a full hero card with link /document-open?fileId={escaped GUID} → removed → empty state restored. The MenuItem path is unit-covered (7 tests); no menu-favorite affordance exists in the scaffold UI to drive it live. docs/detail/N-TIER-ARCHITECTURE.md gained a "Content-to-Component Mapping" section and its Phase 5 status was corrected from "pending" to "staged" (WI-2262 item 4). Amended by D59: the HeroCardViewModel type this entry references was renamed to CardHeroViewModel later the same day. Amended by D61: the "head-only placement" in this entry's title was the placement at adoption time; WI-2254 executed 2026-07-15 and the factories now live in ALPAMobile.Presentation/Components/.

D59 — D4-revision card-family rename finally executed in code (HeroCardViewModelCardHeroViewModel, HeroCard.razorCardHero.razor); leftover pre-rename doc directories deleted
Decision: Adopted 2026-07-15. The 2026-06-10 D4 revision made type-first card-family naming canonical (CardSmall / CardHero / CardText) and recorded a "follow-up pass" to carry the rename through — which never happened for the hero card. CardSmallViewModel/CardTextViewModel shipped type-first, but the hero shipped and stayed HeroCardViewModel/HeroCard.razor, forking the family and leaving ~13 spec docs (already written type-first per the decision) contradicting the code. Resolved by executing the recorded decision in code rather than back-dating the docs to match the drift: HeroCardViewModelCardHeroViewModel (10 files: ComponentViewModels.cs, the staged Presentation copy, ItemComponentFactory, DocumentHeroFactory, FavoritesViewModel, ComponentView.razor, ComponentSkeleton.razor, FavoritesPage.razor, tests) and HeroCard.razorCardHero.razor (tag usages updated). Wire-format strings were untouched — ItemDto.ItemType discriminators ("SingleButtonLarge"/"DocumentHero") are quoted literals outside the type rename.
Leftover directories deleted: the 2026-06-10 doc-dir renames were done as copies — hero-card/, small-card/, text-card/ lingered alongside the canonical card-hero/, card-small/, card-text/ (byte-identical modulo names for small/text; for hero, the old dir was the stale pre-revision snapshot while card-hero/ carried the 2026-06-10 typography revisions). All three deleted. index.html had grown contradictory dual entries for the hero spec — one marking card-hero/ "legacy, superseded" (backwards: it was the newer revision) and one presenting the stale hero-card/ snapshot as current — consolidated to a single Card Hero entry pointing at card-hero/.
Also flipped to the executed names in current-state docs: architecture.html (which additionally carried stale TextCardViewModel/SmallCardViewModel for the two classes that had already shipped type-first), page-compositions.html, scaffold-component-reconciliation.html, content-endpoint-contract.html, ui-refresh-1821/component-library-plan.html, detail/N-TIER-ARCHITECTURE.md, this record's D6 tree annotation, and the D4-revision follow-up note (whose rename arrows were also a transcription slip — both sides listed the post-rename name). Historical decision text (D26/D40/D56/D57/D58 bodies) left as written; D57/D58 carry amendment notes. naming-alignment-report.html (dated 2026-06-26 report) left as a dated snapshot.
Observed here, resolved same day in D60: the discriminator registry in this record (§13-era table) lists card-hero → wire type "HeroCard", but ItemComponentFactory matched only "SingleButtonLarge" or "DocumentHero". This entry originally flagged it as needing backend coordination — over-cautious: no backend implements the feed yet (AB#2133 undelivered), and the dynamic-feed contract's own §5 alias note already prescribed the fix (canonical values + legacy aliases in the factory). Executed as D60.

Status: Adopted 2026-07-15. dotnet test green after the rename (474/474 — the suite builds the head project including the razor tag rename). Completes the D4-revision follow-up recorded 2026-06-10 and closes the naming fork surfaced by the 2026-07-15 doc-consistency audit.

D60 — ItemComponentFactory + ItemDto aligned to the dynamic-feed contract's canonical shape (§3.3/§5); contract's Button-row self-contradiction fixed
Decision: Adopted 2026-07-15. Follow-up to D59's flagged wire-string discrepancy. Comparing the shipped code against the dynamic-feed contract §5 canonical discriminator registry (D28) found the gap was wider than one string: the factory handled only 4 of the 7 canonical item types. "HeroCard" had only legacy aliases ("SingleButtonLarge"/"DocumentHero"); "CardText" and "ButtonCard" had no case at all and threw NotSupportedException — the contract's own §6 example payload (which uses ButtonCard six times) would have crashed the shipped factory. No backend coordination was required: the feed endpoint doesn't exist yet (AB#2133), and the contract's §5 alias note already prescribed exactly this fix ("the new endpoint should use the canonical values ... the factory will continue to support the legacy aliases for transition").
ItemDto widened to the contract §3.3 shape: the shipped ALPAMobile/ApiModels/ItemDto.cs was the prototype wire shape (Title/Blurb/CtaText/CtaLink/Icon); the contract §3.3 defines a richer canonical shape. Added the 7 missing nullable fields (HeaderText, Description, Eyebrow, Label, Image, Link, LinkText) — backward-compatible additions; Blurb/Icon retained and documented as legacy → Description/Image transition fields.
Factory: canonical-first with legacy fallback: every arm now prefers the canonical field and falls back to the prototype field (Description ?? Blurb, Image ?? Icon, Link ?? CtaLink per-type), so both payload generations render identically. New arms: "HeroCard" (joins the hero alias set, now also maps Eyebrow/Link), "CardText"CardTextViewModel (Label · Title · Description · Link), "ButtonCard"ButtonCardViewModel (HeaderText · Description→ContentText slot · composed CallToAction ButtonViewModel, null when no CTA fields present).
Contract self-contradiction fixed: §5's Button/EmergencyButton rows listed Title/Link as the label/target fields, contradicting §3.3's own field comments (CtaText — "CTA button label (ButtonCard, Button)") and the shipped factory. Rows corrected to CtaText/CtaLink (+ Image for the icon, with legacy Icon fallback).
Deliberately left as transition shape: the factory's carousel arm treats "Feed"/"Slider*"/"Carousel" as an ItemType, while the contract models carousels as containers (ContainerDto.containerType) — a prototype-era flattening the mocks depend on. Restructuring to the three-level container model is real AB#2133-integration work, not a naming fix; covered by the §5 alias note until then.

Status: Adopted 2026-07-15. dotnet test 505/505 — every §5 canonical discriminator and every legacy alias is pinned in UnitTest/ItemComponentFactoryTests.cs, including canonical-vs-legacy field-precedence cases. Contract doc updated in place (§3.3 shipped-DTO note, §5 row fix + implementation note, status line).

D61 — WI-2254 executed: component library migrated to ALPAMobile.Presentation RCL; ViewModel dual-write retired (this is the library planning-era docs call the "scaffold PCL" and prototype-era docs call "ALPAMobile.HybridUi")
Decision: Adopted 2026-07-15. Executes D20 (Track C): ALPAMobile.Presentation converted from Microsoft.NET.Sdk to Microsoft.NET.Sdk.Razor (a Razor Class Library), with Microsoft.AspNetCore.Components.Web pinned at 10.0.0 — the exact version the head already resolves transitively through Microsoft.AspNetCore.Components.WebView.Maui, so no version drift. All 25 Library razors (including ComponentView.razor, the dispatcher) moved via git mv to ALPAMobile.Presentation/Components/Library/; the three content→component adapter factories (ItemComponentFactory, MenuItemCardFactory, DocumentHeroFactory), MenuItemIconMask, and FavoriteToggleContext moved to ALPAMobile.Presentation/Components/.
ViewModel cutover — namespace-stable, zero consumer churn: the staged per-type copies were re-namespaced ALPADocs.Presentation.ViewModelsALPADocs.Components.ViewModels and the head's live ComponentViewModels.cs bundle was deleted in the same change. Every consumer (28 pages, factories, tests, _Imports.razor) kept compiling untouched because every type kept its namespace — only the defining assembly moved. Member-level parity across all 34 types (32 classes + enums) was verified programmatically before the cutover. This resolves WI-2253's duplicate-namespace finding for the component set and retires the D51–D55 dual-write convention.
ItemDtoALPAMobile.Application/ApiModels/: the dynamic-feed wire contract now lives beside the Application query ports (namespace ALPADocs.ApiModels unchanged) — unblocking ItemComponentFactory's migration and establishing where WI-2270's PageDto/ContainerDto will land.
FavoriteToggleContext retargeted to the port: its constructor now takes IFavoritesQueries (Application) instead of the head's FavoritesQueriesRouter; the router implements the interface, so call sites pass unchanged.
Deliberately staying in the head: AlpaScreen.razor (composes head-only TopNav/BottomNav and injects ContentQueriesRouter — page chrome, not a library atom) and FavoritesViewModel (injects the head's *QueriesRouter Mock/Live services). Both follow when their dependencies gain ports.

Status: Adopted 2026-07-15. dotnet test 525/525 (including the N-Tier architecture suite — the RCL stays MAUI-free and Infrastructure-free). Live-validated on iOS simulator across 15 routes (/home, /home-preview, /favorites, jumpseat search/saved/saved-searches, /kcm, /mec, /mec/events, /documents, /notifications, /advocacy, /customize-nav): all render with no Blazor error bar; /home and Saved Searches visually spot-checked pixel-correct.

D62 — favorite identity moves onto the ViewModel (FavoriteItemTypeId); FavoriteKey composite ItemId convention adopted; DocumentCategory favorites (type 4) implemented client-first
Decision: Adopted 2026-07-16. ComponentViewModel gains a nullable FavoriteItemTypeId, stamped by the adapter that knows the content source: MenuItemCardFactory → MenuItem (1), DocumentHeroFactory → Document (2), the new CategoryCardFactory → DocumentCategory (4), and ItemComponentFactory passes ItemDto.FavoriteItemTypeId through from the feed. ComponentView now reads the ViewModel's declared type with per-component historical defaults instead of hardcoding a favorite type per visual component. Rationale: a CardSmall can render a menu item or a document category — the visual component type must not imply the content's favorite identity; the adapter layer owns content identity (the WI-2262 principle).
FavoriteKey — the wire ItemId convention: a new Domain helper (beside FavoriteItemTypes) owns the composite {typePrefix}-{rawId} convention matching the backend's production Favorites rows (menu-/doc-/msg-, new cat-). Composition happens at the favorites boundary (FavoriteToggleContext on toggle/membership checks; FavoritesViewModel parses on hydration); ViewModels and adapters carry raw ids. Parsing splits on the first dash only (document fileIds are GUIDs and contain dashes) and tolerates legacy un-prefixed values (they pass through unchanged and degrade to a failed lookup, never a crash). This closes the bare-id mismatch — the pre-convention client stored raw ids, which would not have matched backend/legacy-written composite rows at the AB#2237 mock→live flip.
DocumentCategory favorites (type 4), client-first per Task AB#2271: FavoriteItemTypes.DocumentCategory = 4 (backend handoff pending — the live API must accept the type before the flip). Raw id is {scope}|{categoryKey}, owned by CategoryCardFactory (which also owns the /documents?scope=&category= deep-link shape). FavoritesViewModel gains the category hydration arm — resolving the friendly name via CategoryItem.DisplayTitle from the GetDocuments tree, degrading to unresolved when the category is gone or the raw id is malformed — and the new FavoriteItemKind.Category renders the card-sm row on FavoritesPage alongside menu-item favorites. The home-preview quick-list category rows now carry Id + FavoriteItemTypeId and render hearts when authenticated.

Status: Adopted 2026-07-16. dotnet test 544/544 (19 new pins: FavoriteKey compose/parse incl. GUID raw ids and legacy tolerance, FavoriteToggleContext composite storage and backend-row matching, CategoryCardFactory mapping and raw-id round-trip, factory stamping, ItemDto passthrough, category hydration resolved/gone/malformed arms). Live-validated on iOS simulator against the mock-pinned favorites store: 6 category hearts render on /home-preview when authenticated, toggle → /favorites hydrates "Canada Limits" with its friendly name and /documents deep link → remove restores the empty state.

D63 — three-level dynamic-feed client model implemented (PageDto/ContainerDtoPageFactory/ContainerFactory); /home-preview leads (WI-2270)
Decision: Adopted 2026-07-17. The client implements the contract's three-level model (§3.1/§3.2): PageDto/ContainerDto land in ALPAMobile.Application/ApiModels/ beside ItemDto; the new ContainerFactory (ALPAMobile.Presentation/Components/) dispatches the §4/D28 discriminators ("Carousel" / "Grid" / "ButtonGroup" / "Stack") onto the D28–D30 ContainerSurfaceViewModel subclasses, delegating every item to ItemComponentFactory; unknown container types skip (TryCreate → null) per the §4 forward-compatibility rule — one level up from CreateAll's per-item degrade. PageFactory maps the root onto the new PageViewModel (ALPADocs.Components.ViewModels — the component-tree root a presenter binds to, not a page presenter).
Data seam: new IHomeFeedQueries Application port (GetPageAsync(pageId)PageDto?) behind HomeFeedQueriesRouter, mock-pinned (ScaffoldDataSource.HomeFeedUseLive = false — no live endpoint exists; AB#2133's route/auth are still open contract items). MockHomePreviewFeedService reshaped to the canonical PageDto — a Carousel container of HeroCard items (ALP MAG articles, DocumentHeroFactory.OpenRoute links, Document favorite identity) plus a Stack container of DocumentCategory quick-list rows (D62 identity intact) — and HomePreviewPage renders PageViewModel.Components.
Scope decisions (2026-07-17, with Jose): /home-preview leads/home and MockHomeFeedService stay on the legacy flat CreateAll path so other devs roll up on their own schedule; the flat path and the "Feed"/"Slider*" ItemType arms remain documented transition fallbacks. No container-aware skeletons — HomeSkeleton unchanged; loading polish is deferred to the real home page's design pass. ButtonGroup now exists as BOTH a ContainerType and the legacy ThreeUpButton ItemType producing the same ViewModel — canonicity ask recorded on AB#2270. Explicitly deferred: PageId-keyed caching + ?since differential (wire with the AB#2133 flip), user reordering UI (IsSortable respected render-side only), theme-token resolution (AB#2185–2187; tokens pass through as names).

Status: Adopted 2026-07-17. dotnet test 552/552 (8 new pins: all four ContainerType discriminators, Grid 3-column default + D29 mixed items, ButtonGroup button-only filter as tiles, Stack server-order authority, unknown-container skip at both container and page level, D62 favorite identity riding through containers, and the contract §6-shaped camelCase end-to-end via AlpaWireJson). Live-validated on iOS simulator: /home-preview renders through the container path (4-hero carousel with resolved header, 6 category rows, 10 hearts through the cascade), category-favorite toggle→hydrate→remove round trip intact, /home legacy path unaffected.

D64 — Flight Card gets a Blazor (Track A) implementation: FlightCard.razor + FlightCardViewModel + FlightCardFactory, inline expand supersedes the details page (AB#2299)
Decision: Adopted 2026-07-18. Amends D17 (which moved the flight card out of the scaffold to native Track B as FlightCardView : ContentView): the Blazor results/saved-flights pages now get their own Track A implementation of the flight card-og master (4713:19868, Default/swipe/expanded variants) — ALPAMobile.Presentation/Components/Library/FlightCard.razor with FlightCardViewModel (D16 display fields adapted to the live master) and the pure-mapper FlightCardFactory (FlightItineraryDto → VM; policy-route lookup injected as a delegate). Track B native remains untouched; the two tracks share the Figma master as source of truth.
Inline expand: the master's expanded variant (4713:19985) renders in place via the VIEW/CLOSE DETAILS pill — per-leg Departure/Arrival blocks (status chip derived from estimated-vs-scheduled, never invented; gate "NA" when unknown per master; equipment; KCM + Jumpseat Policy links; updated line), layover bands, per-leg totals. This retires the static /jumpseat/details page (audit FF-1) — results no longer navigate. Amended by D66 (2026-08-14): a dedicated Blazor detail screen now exists for tapped flight alerts. Results still expand inline exactly as decided here — what changed is that a push tap has somewhere better to land than a list.
Capture artifacts: the results-page frame 4713:25168 still mocks the older card anatomy (per-leg dotted rails + layover chips in the collapsed card) — the live component master supersedes it (flagged for designer cleanup, same class as D41's byte-identical duplicate). The expanded variant's top pill keeps the "View Details" label with a flipped caret; treated as a sample-content oversight — both pills read "Close Details" when expanded.
CSS: new alpa-fcard-* subsystem prefix; the retired .alpa-flight-card* and .alpa-fdetail-* blocks are removed (the KCM airport-detail page's borrowed .alpa-fdetail-leg-* rows became KCM-owned .alpa-kcmad-*).
D66 — A dedicated Blazor flight-detail screen for tapped flight alerts: the same FlightCard held open (ExpandLocked), addressed by schedule key (AB#2461, AB#2465)
Decision: Adopted 2026-08-14. Amends D64, which retired the static /jumpseat/details mock and concluded "results no longer navigate". That holds for the results list — cards still expand inline, unchanged. What D64 did not cover is the push entry point: a member who taps "DL5328 now departs from gate A17" was landing on a list of alerts and having to find the flight in it. New page JumpseatFlightDetailPage (/jumpseat/flight-detail) is where that tap goes.
Composes, does not re-render: the page renders the SAME FlightCard through a new ExpandLocked parameter — the expanded variant with no chevron at either end, so the detail cannot be collapsed by a stray tap on a screen whose entire purpose is that detail. A page-specific layout was rejected: it would be a third place the master's anatomy has to be kept true (after FlightCard and the native XAML JumpseatFlightFinderDetailsPage), and the two would drift on the first design change. The lock is a render decision and never writes Vm.IsExpanded, so a VM shared with a results row leaves that row's own expand state alone.
Two addressing modes: /jumpseat/flight-detail/{Index:int} — a position in the results snapshot, meaningful only within the snapshot that produced it (an expired snapshot lands on the empty state, never a wrong flight). /jumpseat/flight-detail/alert/{ScheduleKey} — a tapped alert, which names its flight and knows nothing of any search: the key is looked for in the snapshot first, then in Saved Flights, where a subscribed flight always is because subscribing to alerts saves it. Snapshot-resolved flights share the cached card VM with the results row (save/alert state cannot diverge); saved-list flights get a page-local VM and fetch the airline reference list so the Jumpseat Policy link resolves as it does in results.
Status refresh (native parity, plus one): the same FindFlightInfoAsync lookup the native JumpseatFlightFinderDetailsPageViewModel makes, on landing and on pull-to-refresh (new reusable alpa-pull-refresh.js; .NET is called once, on release past threshold). Added beyond native: a refresh on app resume, debounced 60s — a member who backgrounds the app at the gate and comes back is exactly who a stale gate or delay reading misleads. Refreshed itineraries are written back into the results snapshot so the row behind the page shows the same reading.
Updated line moves above the card: native shows it at the bottom. It answers "is what I am looking at current", which is the question the member opens the screen with, so it belongs before the data it qualifies. It carries the SERVICE's stamp, never the app's fetch time, and says so plainly when the service reports none — the same never-invent rule the card's status chips follow.
Honest failure states: FindFlightInfoAsync answers bare null for signed-out, offline, no-such-flight and a failed call alike, so every one of them read as "couldn't refresh" — a cancelled flight looked like an app failure. FlightStatusRefresh/FlightStatusCurrency (ALPAMobile.Presentation/Components) is a pure classifier that separates what device state can explain, through ports that already exist (IAuthentication, IConnectivityService) plus the itinerary's own arrival time. The residual pair — 503-exhausted vs genuinely-not-in-schedule — is filed as a bug against the shared service (AB#2463); the web-view leak found beside it is AB#2464.
Push payload: the alert already identifies its flight — correlationKey is the fs-{ScheduleKey} audience it was addressed to. The visible-iOS hub registration template never carried that field (only Android and silent-iOS did), so an iOS tap had nothing to route by; added, and asserted by test across all three templates. A payload without it still degrades to the alert list.
Still open: the results-card entry point. A card tap still expands inline per D64; whether it should also navigate here is a separate decision and deliberately not taken in this pass.

Status: Adopted 2026-08-14. Unit suite 944/944. Validated live on both platforms: a real notification tapped in the Android OS drawer opens the correct flight with the card held open and back landing on the Flight Finder alert tab; all three refresh paths (landing, pull, resume) captured on the wire against production; offline and failed-refresh status lines both walked with the network disabled and restored.

14. What Was Applied — 2026-07-06 Figma Gap Sync

Scoped sync of 4 items surfaced by a gap-audit pass against the current figma-component-specs.json baseline, captured from the live Figma file (owEYzHf7FrHRvWC2u82UOl, "ALPA mobile app") via direct REST nodes calls (no .fig binary export available this pass — Dev-Mode annotation pins could not be extracted; noted as a gap, not fabricated).

13. What Was Applied — 2026-07-02 Flight Finder Sync

Scoped sync of 4 newly-discovered Flight Finder components from the live Figma file (owEYzHf7FrHRvWC2u82UOl, "ALPA mobile app") — distinct from the dated-snapshot file (psH738AqHDxuMyFm897f9r) that figma-component-specs.json's _resync block records as baseline. Not a full re-diff of the file.

⚠ Code-side regression, unrelated to this Figma sync — flagged here for visibility alongside D43's open item. PR 1695 ("Composition Root Cleanup & SOLID Hardening") merged into imp/blazor-hybrid 2026-07-06 and removed the IFTDTCalculationEngine injection path from ALPAMobile/ViewModels/FTDTDutyPeriodBasePageViewModel.cs — the FTDTEngine accessor, the ctor parameter, and the engine.Calculate(dutyPeriod) call site are all gone; Calculate() now unconditionally calls the plain domain-model dutyPeriod.Calculate(). MauiProgram.cs no longer registers IFTDTCalculationEngine in DI. GatedFTDTCalculationEngine, RestRulesCalculationEngine, and LegacyFTDTCalculationEngine still exist as source files but are now fully orphaned — nothing instantiates or resolves them, so the §117.25 rest-rules calculation path is unreachable at runtime. Merged intentionally (stacking with other changes in PR 1664) to be reconciled during that branch's conflict resolution, not fixed here — this note exists so the FTDT engine wiring isn't dropped silently when that reconciliation happens. Resolved 2026-07-06: the engine stack was restored during the PR 1664 reconciliation and extraction passes — GatedFTDTCalculationEngine is registered as IFTDTCalculationEngine in HeadServiceCollectionExtensions.AddHeadServices (dispatching Legacy vs RestRules via live-reload feature flag), and the Presentation-layer FTDT ViewModels (ALPAMobile.Presentation/ViewModels/FTDT/) inject it; the §117.25 rest-rules path is reachable again and covered by unit tests.