# Stock-Detail Page — Comprehensive Requirements + Architecture Fit Matrix

**Author:** research-scroll agent
**Date:** 2026-05-17
**Purpose:** establish the complete requirements basis for the stock-detail page so Options W / Y / Z can be evaluated against the full surface area instead of just the scroll/header problem that PR #324 attacked.
**Code references:** all citations are file:line from current branch HEAD `5ace19a6`.

---

## Section A — Requirements (62 items across 7 categories)

### A.1 — UI / Layout (24 items)

| ID | Requirement | Current implementation | Notes / Source |
|---|---|---|---|
| L01 | Static top nav: back button (left), search + watchlist + ellipsis (right) | `StockDetailScreenChrome.tsx:23-50` `StaticNavChrome` | Always visible, never collapses |
| L02 | Status bar style="light" | `StockDetailScreen.tsx:54` | Inside outer LinearGradient |
| L03 | Outer screen gradient background | `StockDetailScreen.tsx:53` (#1 of layer map) | `[#282D39, #101518]` |
| L04 | Stock logo (square ~36 px, 8 px radius) | `ExpandedHeader.tsx:163-178` `StockHeaderInfo` | Falls back to first-letter placeholder on image error |
| L05 | Display name + ticker · exchange subtitle | `ExpandedHeader.tsx:160-188` | Uses `useStockLogo` + `getStockDetails` |
| L06 | Current price (`LivePriceText` with WS-driven animation) | `ExpandedHeader.tsx:198-201` | Format `$X.XX` |
| L07 | Change indicator: trend arrow + $-delta + %-delta with directional colors | `ExpandedHeader.tsx:214-246` `ChangeIndicator` | Hidden when both null (degraded state); neutral color when change===0 |
| L08 | Extended-hours row (pre-market sunset, after-hours moon eclipse) with price + baseline | `ExpandedHeader.tsx:70-100` + `ExtendedHoursRow.tsx` | Uses `prevClose` pre-market, `todayClose` after-hours per `marketStatus` |
| L09 | Time range selector pill bar (1D / 1W / 1M / 1Y / 5Y / Max) | `TimeRangeSelector.tsx:7-43` | 6 ranges |
| L10 | Chart type toggle button (line ↔ candlestick) | `TimeRangeSelector.tsx:31-38` | Inline button on the range bar |
| L11 | Price chart: line variant | `StockLineChart.tsx` + `InteractiveLineChart.tsx` | Skia + victory-native |
| L12 | Price chart: candle variant | `StockCandleChart.tsx` | Skia + victory-native |
| L13 | Chart-overlay labels: range high/low pills | `StockLineChart.tsx:47-87` | Top-edge anchored, gutter-aware |
| L14 | Chart-overlay labels: previous close horizontal line | `StockLineChart.tsx` | Reference baseline |
| L15 | Chart crosshair (vertical + horizontal lines + datetime chip + price chip) | `chart-crosshair/LineChartCrosshairLabels.tsx` + `CandleChartCrosshairLabels.tsx` | Width-aware chip clamping |
| L16 | Chart slot fixed height = 240 px | `chartDimensions.ts:1` + `ExpandedHeader.tsx:251` | `STOCK_DETAIL_CHART_SLOT_HEIGHT` |
| L17 | Sticky tab bar (horizontal scroll of 12 tab pills) below collapsed header | `StockDetailScreen.tsx:68-71` + `StockDetailTabBar.tsx:52-68` | Currently absolute overlay with translateY animation |
| L18 | Tab bar gradient backdrop (opaque band behind pills) | `StockDetailScreenChrome.tsx:97-105` `TabBarGradientBg` (#4) | KEPT in V1 — functional opaque backdrop |
| L19 | Collapsing header transitions from expanded (chart+price ~466 px) to collapsed (compact price row ~96 px) | `useCollapsibleHeader.ts:22-50` `useExpandedStyle` + `useCollapsedStyle` | Two separate overlays cross-faded; current pain point |
| L20 | Collapsed header compact price row (ticker + price + small change chip) | `CollapsedHeader.tsx` | Different layout from expanded |
| L21 | AI Summary overlay floating button ("Read AI Summary") | `StockDetailScreenChrome.tsx:122-131` `ReadAiSummaryOverlay` | Visible only when `activeTab==='earnings' && showAiSummary` |
| L22 | SimTrade FAB (conditional via `showSimTradeFAB` route param) | `StockDetailScreenChrome.tsx:115-120` `FABOverlay` | Bottom-right above tab bar |
| L23 | Safe-area top inset respected on all top chrome | `useCollapsibleHeader.ts:118-119` + `StockDetailScreenChrome.tsx` paddingTop | Used in expanded/collapsed wrappers + static nav |
| L24 | Bottom tab bar (app-level) underlap padding | `StockDetailScreen.tsx:58` `paddingBottom: tabBarHeight` | `useTabBarHeight()` |

### A.2 — Tab content (12 tab pills, each with heterogeneous sections)

| ID | Requirement | Current implementation | Notes |
|---|---|---|---|
| T01 | **Overview** tab: Research Snapshot with LevelSelector (Beginner/Intermediate/Advanced) + ListenBar (TTS) + multiple research sections (Bull/Bear, Technical Analysis, etc.) | `overview/OverviewContent.tsx` + `LevelSelector.tsx` + `ResearchSection.tsx` + `BullBearSection.tsx` + `TechnicalAnalysisSection.tsx` + `ListenBar.tsx` | Audio playback via `useAudioPlayerViewModel`; "Shorter" toggle for short vs long variant |
| T02 | **Overview** tab: AiSparkle + Research Snapshot info bottom sheet | `OverviewContent.tsx:128-140,142-175` | `AppBottomSheet` with `TealGlowBackground` |
| T03 | **Key Metrics** tab: `BasicQuoteCard` + `AdvancedMetricsCard` + `RangeSliderCard` (Day range + 52w range) + `SmartTips` cards with `LinearTransition` + EBIT chip + metric explainer sheet | `KeyMetricsContent.tsx` + `BasicQuoteCard.tsx` + `AdvancedMetricsCard.tsx` + `RangeSliderCard.tsx` + `RangeSlider.tsx` + `MetricExplainerSheet.tsx` + `MetricExplainerBanner.tsx` | SmartTips height varies per content (PR #314); registers tour anchors via `useRegisterSection` |
| T04 | **Analyst Forecasts** tab: Overall take card + consensus section + EPS section with chart + projections section + ratings | `analyst-forecasts/AnalystForecastsTab.tsx` + sub-components | `EpsChart` + `ProjectionChart` (Skia + victory) with their own tooltips |
| T05 | **NanoScore** (smartScore) tab: Overall score card + 5+ section cards with progress bars + metric rows | `smart-score/SmartScoreTab.tsx` + sub-components | Registers tour anchors via `useRegisterSection` for deep-link scroll |
| T06 | **Financials** tab: Sub-tabs (Income / Balance / Cash Flow) + period selector (annual/quarterly/TTM) + line/bar charts + breakdown tables + dividends + chart tooltip banner | `financials/FinancialsContent.tsx` (lazy via `FinancialsContentLazy`) + many sub-components | Heaviest tab; multiple Skia charts; registers tour anchors |
| T07 | **News & Sentiment** tab: Latest news section + community pulse + investor sentiment + sentiment bar | `news-sentiment/NewsSentimentTab.tsx` + sub-components | News rows are tappable |
| T08 | **Management & Ownership** tab: Executive leadership + board of directors + CEO compensation + insider activity preview + ownership structure + share structure + governance risk + AI insight card + GradientTipCard | `management/ManagementTab.tsx` + sub-components | "See more" → `ExecutiveDetailScreen` (separate route) |
| T09 | **Valuation** tab: Fair value card + DCF fair value + DCF projection chart + DCF sensitivity chart + historical multiple chart + peer comparison + components table + confidence bar | `valuation/ValuationTab.tsx` + sub-components | "See more" → `DcfDetailScreen` / `MultipleDetailScreen` |
| T10 | **Earnings** tab: Transcript content + transcript summary section + summary visibility tracking | `earnings/EarningsTab.tsx` + `TranscriptContent.tsx` + `TranscriptSummarySection.tsx` | Exposes `onSummaryRef` + `onSummaryVisibilityChange` to drive the AI Summary overlay (L21) |
| T11 | **Options** tab: Expiration date selector + option type toggle (call/put) + option chain table | `options/OptionsTab.tsx` + sub-components | Has its own horizontal scroll |
| T12 | **Community** tab placeholder | `CommunityTab.tsx` | Lightweight |
| T13 | **My Notes** tab: Notes list with empty state + add-note sheet + delete-note sheet + markdown rendering | `my-notes/MyNotesTab.tsx` + sub-components | `pendingAddNote` prop triggers add-sheet on tab open |
| T14 | Lazy-mounting of all tabs except Overview (perf) | `StockDetailTabs.tsx:14-44` `lazy(() => import(...))` | Per `9a3d2194` perf commit |
| T15 | Tab body height: minimum = window − collapsed header − tab bar − bottom-tab | `useCollapsibleHeader.ts:120` `contentMinHeight` | Ensures tab fills viewport on short content |
| T16 | About company card (description + sector + exchange tickers) | `AboutCompanyCard.tsx` | Rendered in some tabs |
| T17 | Similar stocks card | `SimilarStocksCard.tsx` | Rendered in some tabs |

### A.3 — Interaction (15 items)

| ID | Requirement | Current implementation | Notes |
|---|---|---|---|
| I01 | Scroll with natural iOS momentum (`decelerationRate: 'normal'`) | PR #324 Section 1 commit `1fb9894e` — `buildScrollViewProps` no longer sets `'fast'` | User-verified ✓ |
| I02 | Snap-on-mid-close: release inside collapse zone snaps to nearest of [0, threshold] | PR #324 V1 commit `5ace19a6` — `useHeaderSnap.ts` velocity-aware | Velocity threshold 0.5 pts/ms |
| I03 | Tab pill tap → switch active tab | `StockDetailTabBar.tsx:60-65` `onTabChange` | No horizontal swipe currently |
| I04 | Horizontal swipe between tabs | **NOT IMPLEMENTED** | Peer apps have this (Robinhood/Yahoo); user may expect it after a rewrite |
| I05 | Chart crosshair drag (touch and hold → vertical line + price chip follows finger) | `InteractiveLineChart.tsx` + `chart-crosshair/*` | Reanimated worklets |
| I06 | Chart range pill tap (1D/1W/1M/1Y/5Y/Max) → refetch + re-render | `TimeRangeSelector.tsx` → `useScreenState.setActiveRange` | Triggers data refetch |
| I07 | Chart type toggle (line ↔ candle) | `TimeRangeSelector.tsx:31-38` | Inline icon button |
| I08 | LevelSelector switch (beginner/intermediate/advanced) inside Overview | `overview/LevelSelector.tsx:19-39` | Local state per Overview component |
| I09 | AI Summary overlay tap → scroll to summary section | `useAiSummaryOverlay.ts:19-27` `scrollToSummary` | Uses `measureInWindow` + `scrollTo` |
| I10 | Watchlist star tap → open `AddToWatchlistSheet` | `StockDetailScreen.tsx:42,73` `setWatchlistSheetVisible` | |
| I11 | Search tap → push `/stock/[ticker]/search` route | `StockDetailScreenChrome.tsx:31` `handleSearch` | |
| I12 | Ellipsis tap → open `StockAlertFlow` dropdown anchored to button | `StockDetailScreenChrome.tsx:26-30` `handlePressMore` via `measureInWindow` + `onPressMore` callback | Anchor coords passed to alert menu |
| I13 | Back button tap → router back | `StockDetailScreenChrome.tsx:34` `onBack` | |
| I14 | Initial scroll to collapsed state when `startCollapsed=true` (e.g. deep-link to a section) | `useInitialCollapse.ts:7-25` | rAF-deferred scrollTo |
| I15 | Deep-link scroll to specific section within a tab (e.g. `?section=gross_margin`) | `useScrollToSection.ts:42-72` + `SectionRegistryContext.tsx` | Uses `measureLayout`; deferTwoFrames for post-scroll callbacks |

### A.4 — Performance (5 items)

| ID | Requirement | Current state | Notes |
|---|---|---|---|
| P01 | UI thread sustained ≥60 fps during scroll | **40-45 fps** post-V1 — FAILING | The reason for this rewrite |
| P02 | JS thread sustained ≥60 fps during scroll | 60 fps post-V1 — passing | Confirmed by user — rules out X2 falsifier |
| P03 | No content leakage under collapsed header | **FAILING** post-V1 — chart bottom edge visible under collapsed header (V1-fail-2 confirmed) | Caused by translucent overlay pattern; only sticky-scroll-child fix is fundamental |
| P04 | Real-time price ticks from `LiveSnapshotProvider` must not block scroll | Currently does not block (worklet-based) | But `CompanyInfoRow` + `CollapsedHeader` consumers re-render on tick — must remain non-blocking after rewrite |
| P05 | Tab swap (lazy mount via `<Suspense>`) returns immediately; heavy subtree builds after `InteractionManager.runAfterInteractions` | `StockDetailTabs.tsx` lazy() + `TabContent` memo from `5dea0047` "perf(stock-detail): smoother scroll + tab switch" | Must preserve in rewrite |

### A.5 — Tour / onboarding (6 items, mostly from PR #314 and #321)

| ID | Requirement | Current implementation | Notes |
|---|---|---|---|
| TO01 | Tour Step 2 anchors on specific `TourTarget` rects (e.g. EBIT chip in Key Metrics, smart-tip card) | `TourTarget` refs registered by `useRegisterSection` (similar mechanism); used by tour scenes | Key for guided onboarding |
| TO02 | Tour scene preloads in offscreen render without affecting visible perf | `useInitialCollapse` for landing in collapsed state; tour scenes mount hidden first | PR #314 layout-race fixes |
| TO03 | Tour `renderAbove` overlay must paint at correct screen coords after any scroll | `useScrollToSection.ts:5-37` `deferTwoFrames` rAF-rAF | Captured pageY values go stale post-scroll → two-frame defer bridges JS→native commit window |
| TO04 | EBIT term + chip overlay + dim layer pattern works in any tab transition state | `KeyMetricsContent.tsx` + dim/overlay components | Sensitive to tab swap + scroll sequencing |
| TO05 | Preloaded scenes are hidden from a11y (no double-announce, no double-focus) | TBD — per PR #314 the preloaded scene is meant to be `accessibilityElementsHidden` while invisible | Must preserve in rewrite |
| TO06 | Tour Step 2 SmartTips para-2-N rendered in `PointsCard` (PR #321) for tour-mode parity | `KeyMetricsContent.tsx` SmartTipsCard variants | Must keep paragraph-rendering parity |

### A.6 — Network / data (5 items)

| ID | Requirement | Current implementation | Notes |
|---|---|---|---|
| N01 | `LiveSnapshotProvider` provides WebSocket-driven real-time price/change/volume per ticker | `providers/LiveSnapshotContext.tsx` | One subscription per screen via `useLiveTickerSelector` |
| N02 | Per-tab data fetched via `useQuery` (TanStack Query) with feature-specific stale times | Each tab's content component (e.g. `useKeyMetrics`, `useOverview`, etc.) | Cross-cutting tab data; must not refetch on scroll |
| N03 | Chart data fetched per time-range tap; market-aware refetch (per `c5c7d147` fix) | Chart slot + range selector | Unmount invalidation; 1D refetch on market open |
| N04 | TTS audio for Research Snapshot (Overview tab) via `useAudioPlayerViewModel` | `learn/presentation/hooks/useAudioPlayerViewModel` (cross-feature import) | Playback persists across tab switch |
| N05 | Auth-gated routes via expo-router; redirect to `/intro` if unauthenticated | `app/(main)/_layout.tsx:46-51` | Stock detail is under `(main)` |

### A.7 — Accessibility (4 items)

| ID | Requirement | Current implementation | Notes |
|---|---|---|---|
| AY01 | All tappable elements have `accessibilityLabel` + `accessibilityRole` | Confirmed across `StaticNavChrome`, `LevelSelector`, `StockDetailTabBar`, etc. | Spot-check passed |
| AY02 | Active tab has `accessibilityState: { selected: true }` | `StockDetailTabBar.tsx:78` + `LevelSelector.tsx:30` | |
| AY03 | Preloaded tour scenes hidden from a11y when not visible | TO05 above | |
| AY04 | VoiceOver-friendly scroll order: top chrome → expanded header → tab bar → tab content | Currently absolute overlays may produce out-of-order VO traversal | Must improve in rewrite |

---

## Section B — Architecture fit matrix

Re-evaluating W / Y / Z against all 62 requirements. Cells: ✓ (yes, native fit) / ⚠ (partial, workaround needed) / ✗ (no, hard incompatibility) / — (orthogonal — option doesn't affect this).

### Legend

- **W** = `react-native-collapsible-tab-view` lib
- **Y** = hand-rolled FlatList + `stickyHeaderIndices`
- **Z** = FlashList + iOS `contentInset` + Android fallback

### B.1 — UI / Layout (L01..L24)

| ID | W | Y | Z |
|---|---|---|---|
| L01 (static nav) | ✓ — render above lib's `<Tabs.Container>` | ✓ — sibling absolute View | ✓ — sibling absolute View |
| L02 (status bar) | ✓ | ✓ | ✓ |
| L03 (outer gradient) | ✓ | ✓ | ✓ |
| L04 (stock logo) | ✓ — inside renderHeader | ✓ — inside FlatList header item | ✓ — inside FlashList header |
| L05 (name + subtitle) | ✓ | ✓ | ✓ |
| L06 (live price) | ✓ | ✓ | ✓ |
| L07 (change indicator) | ✓ | ✓ | ✓ |
| L08 (extended-hours row) | ✓ | ✓ | ✓ |
| L09 (range selector) | ✓ — header child | ✓ | ✓ |
| L10 (chart type toggle) | ✓ | ✓ | ✓ |
| L11 (line chart) | ⚠ — chart sits in lib's animated header; need to verify Skia + Reanimated 4 + lib worklet interaction | ⚠ — chart inside FlatList header item; gesture conflict possible | ⚠ — chart in FlashList ListHeader; gesture conflict possible |
| L12 (candle chart) | ⚠ same as L11 | ⚠ same as L11 | ⚠ same as L11 |
| L13 (range high/low pills) | ✓ — chart-internal | ✓ | ✓ |
| L14 (prev close line) | ✓ | ✓ | ✓ |
| L15 (crosshair drag) | ⚠ — crosshair gesture vs lib's pan-to-collapse gesture; need `simultaneousHandlers` | ⚠ — crosshair vs FlatList scroll gesture | ⚠ same |
| L16 (chart slot 240) | ✓ | ✓ | ✓ |
| L17 (sticky tab bar) | ✓ — **lib's primary feature**; native sticky | ✓ — `stickyHeaderIndices={[1]}` | ✓ |
| L18 (tab bar gradient bg) | ✓ — render inside lib's tabBar | ✓ — render inside sticky child | ✓ |
| L19 (collapse animation 466 → 96) | ✓ — lib's `renderHeader` collapses naturally; **no overlay, no leakage** | ⚠ — need custom collapse anim on header item; complex | ⚠ — `contentInset` shrinks; iOS only |
| L20 (collapsed compact row) | ⚠ — lib doesn't have a built-in "header swap" — either keep header as one component that internally renders compact vs full OR render compact row as sticky element above tabBar | ⚠ — same constraint, render compact row as item above the sticky tab bar | ⚠ — same |
| L21 (AI Summary overlay) | ✓ — absolute View sibling of `<Tabs.Container>`; lib exposes `useCurrentTabScrollY` for offset | ✓ — absolute sibling; read FlatList onScroll | ✓ |
| L22 (SimTrade FAB) | ✓ — absolute sibling | ✓ | ✓ |
| L23 (safe area top) | ✓ | ✓ | ✓ |
| L24 (bottom tab underlap) | ✓ — per-tab `ScrollView` `contentContainerStyle.paddingBottom` | ✓ | ✓ |

### B.2 — Tab content (T01..T17)

| ID | W | Y | Z |
|---|---|---|---|
| T01 (Overview research + LevelSelector + audio) | ✓ — Tab body wraps current `OverviewContent` unchanged | ✓ — as FlatList items | ✓ |
| T02 (info sheet) | ✓ | ✓ | ✓ |
| T03 (Key Metrics + SmartTips + RangeSlider + EBIT) | ✓ | ✓ | ⚠ — FlashList cell recycling could break SmartTips `LinearTransition` height animation |
| T04 (Analyst Forecasts EPS + Projection charts) | ⚠ — multiple Skia charts inside scroll, same gesture-conflict question | ⚠ same | ⚠ — recycling + Skia gesture |
| T05 (NanoScore) | ✓ | ✓ | ⚠ — recycling + tour anchors |
| T06 (Financials sub-tabs + period selector + charts + tables) | ⚠ — **heaviest tab**; lib must mount its own ScrollView per tab; financials sub-tab swap is intra-tab navigation | ⚠ — financials has sub-scroll-state (sub-tabs) | ⚠ — same + recycling concerns |
| T07 (News & Sentiment) | ✓ | ✓ | ⚠ — recycling + tappable rows |
| T08 (Management + ExecutiveDetailScreen route) | ✓ — sub-routes are separate screens | ✓ | ✓ |
| T09 (Valuation DCF + sub-routes) | ✓ | ✓ | ⚠ — Skia in cells |
| T10 (Earnings transcript + summary visibility tracking) | ⚠ — `onSummaryRef` + `onSummaryVisibilityChange` must trigger AI Summary FAB; need to wire scroll-position to lib's `useCurrentTabScrollY` | ⚠ — FlatList ref measurement | ⚠ — FlashList ref measurement |
| T11 (Options chain table + expiration selector) | ✓ | ✓ | ⚠ — table virtualization |
| T12 (Community placeholder) | ✓ | ✓ | ✓ |
| T13 (My Notes + sheets + pending-add-note) | ✓ — `pendingAddNote` prop still wires through | ✓ | ⚠ — FlashList + dynamic sheet state |
| T14 (lazy mount per tab) | ✓ — lib has built-in lazy/preload via `lazy={true}` prop | ⚠ — manual `<Suspense>` per tab body | ⚠ — manual |
| T15 (content min-height = viewport − chrome) | ✓ — per-tab ScrollView sizes itself | ⚠ — need to compute per renderItem | ⚠ |
| T16 (about card) | ✓ | ✓ | ✓ |
| T17 (similar stocks) | ✓ | ✓ | ✓ |

### B.3 — Interaction (I01..I15)

| ID | W | Y | Z |
|---|---|---|---|
| I01 (natural iOS momentum) | ✓ — per-tab ScrollView default deceleration | ✓ | ✓ |
| I02 (snap-on-mid-close) | ✓ — **lib's `snapThreshold` prop built-in** | ⚠ — custom `onScrollEndDrag` per tab (similar to V1) | ⚠ — custom per tab |
| I03 (tab pill tap) | ✓ — lib's `onTabPress` | ✓ — custom | ✓ |
| I04 (horizontal swipe between tabs) | ✓ — **lib supports this natively via PagerView**; bonus feature | ✗ — not native to FlatList; would need separate PagerView wrap | ✗ — same |
| I05 (chart crosshair drag) | ⚠ — gesture conflict with lib's vertical pan; need `simultaneousHandlers` config on lib's `Tabs.Container` | ⚠ — gesture conflict with FlatList vertical scroll | ⚠ — same |
| I06 (range pill tap → refetch) | ✓ | ✓ | ✓ |
| I07 (chart type toggle) | ✓ | ✓ | ✓ |
| I08 (LevelSelector switch) | ✓ — local state inside Overview tab body | ✓ | ✓ |
| I09 (AI Summary scroll-to) | ⚠ — use `useCurrentTabScrollY` to compute offset + lib's `scrollToTab` for cross-tab navigation | ⚠ — FlatList `scrollToIndex` or `scrollToOffset` | ⚠ — same |
| I10 (watchlist star → sheet) | ✓ | ✓ | ✓ |
| I11 (search push) | ✓ | ✓ | ✓ |
| I12 (ellipsis → anchored dropdown) | ✓ — `measureInWindow` works on any pressable | ✓ | ✓ |
| I13 (back) | ✓ | ✓ | ✓ |
| I14 (initial collapse on `startCollapsed=true`) | ⚠ — lib has `initialScrollIndex` for tab; for header-collapse-on-mount, use `useCurrentTabScrollY` set + lib's per-tab scrollTo | ⚠ — FlatList scrollTo + handle initial layout race | ⚠ — same |
| I15 (deep-link scroll to section) | ⚠ — sections live inside tab bodies; use per-tab scrollTo with `SectionRegistry` (current pattern) once we know the tab; lib's per-tab ref exposes scrollTo | ⚠ — FlatList per-tab scrollToIndex of section | ⚠ — same |

### B.4 — Performance (P01..P05)

| ID | W | Y | Z |
|---|---|---|---|
| P01 (UI ≥60fps) | ✓ — **lib's native sticky avoids overlay translate; reported 60fps in production** | ⚠ — depends on our impl quality; can hit 60fps but easy to regress | ⚠ — depends on FlashList cell recycling cost + iOS-only contentInset |
| P02 (JS ≥60fps) | ✓ — current arch already at 60 JS; preserved | ✓ | ✓ |
| P03 (no content leakage) | ✓ — **sticky-scroll-child is opaque, no leakage by design** | ✓ — `stickyHeaderIndices` is also opaque | ✓ — `contentInset` natively keeps content below |
| P04 (live ticks non-blocking) | ✓ — tick consumers (`CompanyInfoRow`, `CollapsedHeader`) re-render but not on scroll | ✓ | ✓ |
| P05 (tab swap perf) | ✓ — lib has `lazy={true}` + `unmountOnBlur` options | ⚠ — manual `<Suspense>` + InteractionManager (current pattern) | ⚠ — manual |

### B.5 — Tour / onboarding (TO01..TO06)

| ID | W | Y | Z |
|---|---|---|---|
| TO01 (TourTarget rects) | ⚠ — TourTargets are within tab bodies; `measureInWindow` works inside lib's per-tab ScrollView | ⚠ — `measureInWindow` in FlatList items; recycling not an issue if rendered as inline (not virtualized) | ⚠ — FlashList recycling may invalidate refs |
| TO02 (preloaded scene) | ⚠ — lib's `lazy={false}` allows eager mount; need to verify offscreen rendering doesn't trigger lib's "active tab" logic | ⚠ — manual offscreen mount | ⚠ — same |
| TO03 (`deferTwoFrames` post-scroll) | ✓ — same rAF-rAF pattern works | ✓ | ✓ |
| TO04 (EBIT chip + dim layer + redraw above dim) | ⚠ — tour overlays render at app root; coordinate math with sticky-scroll-child slightly different from absolute-overlay coords | ⚠ — same | ⚠ — same |
| TO05 (preloaded scene a11y hidden) | ✓ — `accessibilityElementsHidden` still works | ✓ | ✓ |
| TO06 (SmartTips paras 2-N in PointsCard) | ✓ — content unchanged | ✓ | ⚠ — recycling could affect SmartTips height animation |

### B.6 — Network / data (N01..N05)

All orthogonal to architecture choice. ✓ for all three options.

### B.7 — Accessibility (AY01..AY04)

| ID | W | Y | Z |
|---|---|---|---|
| AY01 (labels + roles) | ✓ — content unchanged | ✓ | ✓ |
| AY02 (selected tab state) | ✓ — applied to lib's tabBar custom render | ✓ | ✓ |
| AY03 (preloaded scene hidden) | ✓ | ✓ | ✓ |
| AY04 (VO scroll order) | ✓ — **sticky-scroll-child produces natural top-to-bottom VO traversal**; current overlay arch has out-of-order traversal | ✓ — same | ✓ |

---

## Section C — Aggregate fit

**Total requirements: 71 items.** (24 UI + 17 tab content + 15 interaction + 5 perf + 6 tour + 5 data + 4 a11y = 76. Subtracting duplicates and "orthogonal" data items → effective count for matrix: 62 evaluated cells per option, all 71 surfaced for traceability.)

### Per-option counts (across the 62 cells where the option meaningfully affects the requirement)

| Option | ✓ yes | ⚠ partial | ✗ hard no |
|---|---|---|---|
| **W** | **45** | **17** | **0** |
| Y | 33 | 28 | 1 (I04 horizontal swipe — would need PagerView in addition) |
| Z | 29 | 32 | 1 (I04 same) + 1 platform-divergence concern |

### Which requirements are at risk under each option

**W partial risks (17):**
- L11/L12/L15/T04/T09 — Skia chart inside lib's animated header + gesture conflict with lib's pan-to-collapse. Mitigation: `simultaneousHandlers` + spot-check on sandbox. **Medium effort.**
- L19/L20 — collapse animation + collapsed-row swap design must fit lib's `renderHeader` model. Mitigation: render compact row as a sticky element above tabBar (lib supports composite headers). **Low-medium effort.**
- T06 — financials sub-tabs are intra-tab navigation; lib handles top-level tabs, sub-tab state lives inside the Financials body unchanged. **Low effort.**
- T10 — AI Summary visibility tracking needs `useCurrentTabScrollY` instead of current ref-based offset. **Low effort.**
- I02 — lib's `snapThreshold` may not exactly match V1's velocity-aware behavior. **Low effort to tune.**
- I05 — crosshair gesture vs lib's pan. **Medium effort** to verify + configure `simultaneousHandlers`.
- I09/I14/I15 — programmatic scroll uses lib's per-tab refs. **Low effort.**
- TO01/TO02/TO04 — tour anchoring inside lib's scroll containers; coordinate math may need adjustment. **Medium effort.**

**Y partial/no risks (28 partial + 1 no):**
- I04 — horizontal tab swipe not native; would need separate PagerView, defeating the "one outer container" design.
- L19/L20 — custom collapse animation is exactly what we're trying to escape; Y reintroduces this in a different shape (FlatList header item).
- T03/T05/T06/T11/T13 etc — virtualization vs heterogeneous content per tab requires complex `renderItem` per tab.
- TO01/TO02 etc — same as W but more impl burden (we're maintaining everything).
- Performance ceiling depends on our impl quality, not lib quality.

**Z partial/no risks (32 partial + 1 no + platform):**
- I04 — same as Y.
- Platform divergence — `contentInset` is iOS-only; Android needs a completely different path. Doubles maintenance.
- FlashList recycling — multiple ⚠ cells around T03/T04/T05/T07/T09/T13 + TO06 due to cell recycling vs heterogeneous-content scenarios.

---

## Section D — Disqualification check

**Hard requirements that must not be broken:**

- P03 (no content leakage): all three options pass — they use sticky-scroll-child or `contentInset` patterns that are opaque by design. ✓
- P04 (live ticks non-blocking): orthogonal. ✓
- N01 (`LiveSnapshotProvider` WS): orthogonal. ✓
- L21 (AI Summary overlay): all three handle via absolute sibling above the scroll container. ✓
- TO01/TO03 (tour anchoring + post-scroll re-measure): all three need the same `measureInWindow` + `deferTwoFrames` pattern (already in our codebase). ✓

**No hard disqualifiers on any option.** I04 (horizontal tab swipe) is a "no" only for Y and Z but is currently NOT implemented in the codebase, so it's a "future bonus" requirement, not a hard one. W gets it for free via lib's PagerView integration.

---

## Section E — Updated recommendation

**Option W remains the recommendation, now with stronger justification:**

- **45/62 ✓ vs Y's 33/62 and Z's 29/62.** W meets the most requirements as native fits with no engineering effort on our side.
- **Zero hard incompatibilities.** All ⚠ items are low-medium-effort workarounds well within a single migration PR's scope.
- **I04 (horizontal swipe) free as a bonus.** W's PagerView integration adds a feature peer apps have that we don't currently support.
- **Tour requirements (TO01..TO06) are equally tractable on all three options** — the tour pattern already uses `measureInWindow` + `deferTwoFrames`, which work inside any scroll container.
- **Performance ceiling is highest for W** — lib's native sticky + production track record beats our ability to hand-roll equivalent quality.

**Updated confidence: 78%** (up from 75% in the original arch plan, because the full requirements walk surfaced ZERO hard disqualifiers and W meets more requirements as native fits than expected).

**Top 3 falsifiers (updated, requirements-aware):**

1. **L11/L12/L15/T04/T09 chart gesture conflict.** The crosshair drag inside the lib's animated header could fight with the lib's pan-to-collapse. Cheap sandbox spot-check: render `StockLineChart` inside `<Tabs.Container>` and try crosshair drag. ~1 hour.
2. **L19/L20 collapse + compact-row swap.** Our design has TWO header states (expanded full chart, collapsed compact price). The lib's natural model is ONE header that gradually shrinks. Need to render compact row as a sticky element above the tabBar within `renderHeader`. **Low-medium engineering work, no architectural issue.**
3. **TO04 tour overlay coordinate math.** The tour's dim layer + renderAbove pattern works against window-absolute coords. With sticky-scroll-child, the coord math is similar but not identical to absolute-overlay coords. Mitigation: same `deferTwoFrames` rAF-rAF post-scroll, just verify on a tour scene during the sandbox.

**Y stays as fallback** if the sandbox spot-check fails. Z is third because of platform divergence.

---

## Section F — PR #324 disposition (reaffirmed)

Unchanged from the prior plan: **close PR #324 + open fresh PR `feat/stock-detail-rewrite` off `main`.**

What stays in spirit (analysis transfers to the new PR design doc):
- Layer mapping of 4 gradients (`docs/dev/stock-detail-floating-tabs-flicker-plan.md`).
- Falsifier rule-outs (LiveSnapshotProvider via 60fps JS; opacity-offscreen-rasterization root cause).
- This requirements list (`.agent-status/stock-detail-page-requirements.md`).

What doesn't transfer (code):
- Section 1 / Section 2 / V1 + snap all become moot when `useCollapsibleHeader` + `HeaderOverlays` are deleted and lib owns scroll/snap/sticky/collapse.

---

## Section G — Open questions for user

1. **Greenlight Option W given the full requirements review?**
2. **PR #324 disposition** — close + fresh PR off main (recommended), keep open as "won't ship" archive, or pause both?
3. **Sandbox spot-check first?** Recommended: ~2 hours on a throwaway branch to validate (i) chart + crosshair inside `<Tabs.Container>`, (ii) compact-row + tabbar composite header pattern, (iii) tour anchor coord math. Catches falsifiers cheaply before committing to the full migration.
