diff --git a/docs/superpowers/specs/2026-04-14-aeroflot-flights-react-rewrite-design.md b/docs/superpowers/specs/2026-04-14-aeroflot-flights-react-rewrite-design.md
new file mode 100644
index 00000000..507f32a6
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-14-aeroflot-flights-react-rewrite-design.md
@@ -0,0 +1,1073 @@
+# Aeroflot Flights — Angular → React Rewrite Design
+
+**Status:** Draft for review
+**Date:** 2026-04-14
+**Owner:** gnezim
+**Scope:** Full rewrite of the Aeroflot Flights web application from Angular 12 to a dual-mode Modern.js app (standalone SSR site + Module Federation 2.0 remote component), delivered in phases via strangler-fig migration.
+
+---
+
+## 0. Context
+
+### 0.1 Current state
+
+An ASP.NET host (`Aeroflot.Flights.Web.csproj`, `Startup.cs`, `Program.cs`) serves an Angular 12 SPA from `ClientApp/`. The SPA provides four flows — **Online Board** (real-time arrivals/departures with SignalR live updates), **Schedule** (flight timetable search with multi-leg deep links), **Flights Map** (Leaflet-based route visualization, feature-flagged), and **Popular Requests** — across nine languages (ru, en, es, fr, it, ja, ko, zh, de) with URL-prefixed locales (`/{lang}/…`).
+
+Key characteristics of the existing codebase:
+
+- **No NgRx.** State lives in services exposing RxJS `BehaviorSubject`s.
+- **Heavy RxJS** in filter and data-source services.
+- **No Angular Universal.** Currently CSR-only.
+- **PrimeNG 10** for Calendar, Autocomplete, Accordion, Dropdown, DataTable, Tooltip, Dialog, Toast — with Aeroflot-specific SCSS theme overrides.
+- **`@ngx-translate` + `messageformat.js`** for i18n (ICU MessageFormat).
+- **Application Insights** for telemetry.
+- **SignalR `TrackerHub`** for live flight updates (`SubscribeDate(flightDate, departure, arrival)` → pushed `RefreshDate` events).
+- **`custom-webpack.config.js`** randomizes `chunkLoadingGlobal` — an early signal that the codebase was authored with micro-frontend embedding in mind.
+- **Custom URL schemes** for deep linking, notably the variable-length multi-leg schedule URL (`/schedule/SU0001-2025-01-15/SU0002-2025-01-15/…`) handled via a custom `UrlMatcher`.
+- **Tests:** ~175 Vitest-equivalent `.spec.ts` files (modest coverage), 5 Cypress E2E scenarios (currently not passing), no Storybook stories authored.
+
+### 0.2 Customer requirements (summary)
+
+1. **Stack:** Modern.js (SSR), Module Federation 2.0 with `mf-manifest.json`, React 18+ with Concurrent Mode and ``, no side effects in component bodies, `React.lazy()` for dynamic imports. Bundler: any MF 2.0-capable (Rspack chosen).
+2. **Data:** REST API, JSON; rendered data must stay consistent with API.
+3. **Performance:** 100 RPS.
+4. **Reliability:** Geo-distributed VMs.
+5. **Security:** Component must be isolated — no threat to other site components.
+6. **SEO & availability:** 24/7/365, ≤6h MTTR; SEO optimization, JSON-LD + OpenGraph; analytics via Яндекс.Метрика, CTM, Вариокуб, Ключ-Астром (Dynatrace).
+7. **Cross-platform:** Embeddable in multiple channel apps (Web, PWA); fluid responsive layout.
+8. **Logging & monitoring:** Collect frontend logs in a customer-specified format (TBD), ship to the customer's log system; system-event monitoring to a metrics aggregator.
+9. **Module structure:** Must match the customer's standard remote-frontend module template (template not yet available — **SPEC ASSUMPTION: default to Modern.js MF remote layout, reconcile later**).
+10. **Design:** Must match customer design system and mockups; must embed other remote components when available. **Pixel-perfect parity with the current Angular app is the near-term design target**, with a UI adapter boundary for future reskinning.
+
+### 0.3 Non-goals
+
+- No new UX or features beyond parity with the current Angular app.
+- No backend / REST API changes.
+- No data-model changes.
+- No authentication; the app remains public.
+- No Angular ↔ React hybrid runtime. The migration is strangler-fig at the URL-routing level, not at the component level.
+
+---
+
+## 1. High-level architecture + repo layout
+
+### 1.1 Runtime topology
+
+A single Modern.js project produces **two deployable artifacts** from the same `src/`:
+
+1. **Standalone SSR site** — the public entry point (`flights.aeroflot.ru` or equivalent). Served by Modern.js's Node SSR runtime. Owns its own routing, meta tags, JSON-LD, analytics. This is what SEO crawlers, direct visitors, and backlinks hit. **Satisfies:** 1.1.1 (ModernJS SSR), 6 (SEO / JSON-LD / OG / availability), 7 (responsive), 4 (geo-distributed VMs behind a load balancer).
+
+2. **Module Federation 2.0 remote** — a static MF 2.0 remote with `mf-manifest.json` exposing feature-level entries to channel apps (Web, PWA). Channel apps load and render these entries inside their own SSR/CSR flow. **Satisfies:** 1.1.2 (MF 2.0 + manifest), 1.1.3 (React 18 + Suspense + `React.lazy`), 7 (cross-platform embedding).
+
+Both artifacts are built in CI from the same commit. A feature component imported from `src/features/online-board/` renders identically in both modes because the feature never knows which mode hosts it — it depends only on a narrow `HostContract` interface that both modes implement.
+
+### 1.2 Dependency direction (enforced by ESLint boundaries)
+
+```
+routes/ ─────────┐ (standalone entry only)
+ ▼
+ features/ ◄──── mf/ (remote entry only)
+ │
+ ┌────────────┼──────────────┐
+ ▼ ▼ ▼
+ ui/ shared/ observability/
+ │ │
+ ▼ ▼
+ i18n/ (otel, logger,
+ analytics)
+```
+
+`features/` never imports `routes/` or `mf/`. `ui/` never imports `features/`. Both build targets can tree-shake each other's entry code cleanly.
+
+### 1.3 `src/` tree
+
+```
+src/
+├── routes/ # Modern.js file-based routing (standalone mode only)
+│ ├── [lang]/
+│ │ ├── onlineboard/
+│ │ │ ├── page.tsx # /{lang}/onlineboard (start)
+│ │ │ ├── flight/[params]/page.tsx # /{lang}/onlineboard/flight/SU100-2025-01-15
+│ │ │ ├── departure/[params]/page.tsx # /{lang}/onlineboard/departure/SVO-2025-01-15
+│ │ │ ├── arrival/[params]/page.tsx # /{lang}/onlineboard/arrival/JFK-2025-01-15
+│ │ │ ├── route/[params]/page.tsx # /{lang}/onlineboard/route/SVO-JFK-2025-01-15
+│ │ │ └── [params]/page.tsx # /{lang}/onlineboard/SU100-2025-01-15 (details)
+│ │ ├── schedule/
+│ │ │ ├── page.tsx
+│ │ │ ├── route/[params]/page.tsx
+│ │ │ ├── route/[params]/[returnParams]/page.tsx
+│ │ │ └── [...flights]/page.tsx # catch-all for multi-leg URLs
+│ │ ├── flights-map/
+│ │ │ ├── page.tsx
+│ │ │ └── route/[params]/page.tsx
+│ │ ├── popular/page.tsx
+│ │ ├── page.tsx # / → redirect to /onlineboard
+│ │ └── layout.tsx # locale resolver, canonical, hreflang, analytics
+│ ├── error/[code]/page.tsx # 404, 500
+│ ├── layout.tsx # root html shell, , i18n provider
+│ └── page.tsx # / → redirect to /{default-lang}/onlineboard
+│
+├── mf/ # MF remote entry (remote mode only)
+│ ├── expose/
+│ │ ├── OnlineBoard.tsx
+│ │ ├── Schedule.tsx
+│ │ ├── FlightsMap.tsx
+│ │ └── PopularRequests.tsx
+│ └── host-contract.ts # TypeScript interface the host must implement
+│
+├── features/
+│ ├── online-board/
+│ │ ├── components/ # ported Angular components, JSX, same class names
+│ │ ├── hooks/ # useOnlineBoard, useLiveFlights
+│ │ ├── api.ts # REST client for this feature
+│ │ ├── url.ts # byte-exact URL serializer/parser
+│ │ ├── seo.ts # JSON-LD + OG tag builders
+│ │ └── index.ts # barrel — only public surface
+│ ├── schedule/ (same shape)
+│ ├── flights-map/ (same shape)
+│ └── popular-requests/(same shape)
+│
+├── ui/ # PrimeReact adapter + Aeroflot SCSS
+│ ├── primitives/ # Button, Input, Calendar, Autocomplete, …
+│ ├── layout/ # SearchResultsLayout, DetailsLayout
+│ ├── flights/ # 100+ ported flight-display components
+│ ├── icons/ # SVG icons as React components
+│ ├── seo/ # SeoHead
+│ ├── errors/ # ErrorBoundary
+│ └── styles/ # ported global SCSS, fonts, theme overrides
+│
+├── shared/
+│ ├── api/ # ApiClient, LRU cache, circuit breaker
+│ ├── signalr/ # @microsoft/signalr lifecycle wrapper
+│ ├── hooks/ # useLiveFlights, useSearchHistory, useMediaQuery
+│ ├── url/ # reusable URL codecs (date, flight-number)
+│ └── utils/ # date/tz helpers, formatters
+│
+├── i18n/
+│ ├── config.ts # i18next + react-i18next + i18next-icu setup
+│ ├── locales/ # ported JSON files (9 languages)
+│ └── resolver.ts # URL-prefix → language, SSR-aware
+│
+├── observability/
+│ ├── logger/ # Logger interface + JSON-lines transport
+│ ├── metrics/ # OpenTelemetry setup, OTLP exporter
+│ └── analytics/ # facade + adapters (metrica, ctm, variocube, dynatrace)
+│
+└── env/ # runtime env-var resolution
+```
+
+**Feature barrel rule:** `features/*/index.ts` is the only public entry of each feature. Both `routes/` and `mf/expose/` import exclusively through the barrel. This is what makes feature-by-feature strangler-fig migration safe.
+
+**Location rule:** ported Angular components from `FlightsModule` (the 100+ shared flight-display components) land in `src/ui/flights/`. Feature-specific components land in `features/*/components/`. The dividing line is "does more than one feature use it?"
+
+### 1.4 URL language-prefix policy
+
+**Language prefix always required.** `/{lang}/onlineboard/…` is the canonical form; missing-prefix URLs (`/onlineboard`) 301-redirect to the `Accept-Language`-detected locale. Every in-app link is generated with the prefix. Byte-exact parity with the Angular app's scheme.
+
+---
+
+## 2. Module Federation topology + build targets
+
+### 2.1 Two build targets, one `modern.config.ts`
+
+The repo has one `modern.config.ts` that switches behavior on `BUILD_TARGET=standalone|remote`, producing two distinct artifacts from the same source tree.
+
+**Standalone target (`BUILD_TARGET=standalone`)**
+
+- Entry: Modern.js conventional routing from `src/routes/`.
+- Output: SSR Node server + client bundle + static assets.
+- Deployed as: Dockerized Node service behind the load balancer.
+- Serves: `flights.aeroflot.ru` (direct visitors, SEO crawlers, deep links).
+- Plugins: `@modern-js/plugin-ssr`, `@modern-js/plugin-i18n`.
+
+**Remote target (`BUILD_TARGET=remote`)**
+
+- Entry: `src/mf/index.ts` (imports `src/mf/expose/*`).
+- Output: Rspack bundle with `mf-manifest.json`, ES module chunks, no Node server.
+- Deployed as: static files on CDN / object storage.
+- Served at: `https:///mf-manifest.json` + chunk files.
+- Plugin: `@module-federation/modern-js` (official Modern.js adapter for MF 2.0).
+
+**Why the remote is static, not SSR-serving.** The SSR mandate in requirement 1.1.1 is fulfilled by the *standalone* mode. The *remote* mode is for embedding inside channel apps that run their own SSR — hosts call our exposed components at request time and SSR them inside their own process. Our job is to ship JS the host can import and render, not to operate a second SSR tier for embedded use. This is the conventional MF 2.0 pattern and keeps the remote's operational surface minimal.
+
+### 2.2 `mf-manifest.json` and exposed modules
+
+Generated by `@module-federation/modern-js` at build time. Served at `https:///mf-manifest.json`.
+
+**Exposes (feature-level, 4 entries):**
+
+```
+name: "aeroflot_flights"
+exposes:
+ ./OnlineBoard → src/mf/expose/OnlineBoard.tsx
+ ./Schedule → src/mf/expose/Schedule.tsx
+ ./FlightsMap → src/mf/expose/FlightsMap.tsx
+ ./PopularRequests → src/mf/expose/PopularRequests.tsx
+```
+
+Each expose is a thin wrapper: imports the feature barrel, accepts a `hostContract` prop, renders the feature root. All four use `React.lazy()` internally for code-splitting within the feature (requirement 1.1.3).
+
+**Why feature-level, not page-level or component-level.** Page-level would explode the expose count and force the host to understand our internal routing. Component-level would couple the host to our UI adapter and kill independent deploys. Feature-level is the right granularity: one expose per "thing the channel app wants to embed," with internal routing handled inside the exposed component.
+
+### 2.3 Shared dependencies
+
+```
+shared:
+ react: { singleton: true, requiredVersion: "^18.2.0" }
+ react-dom: { singleton: true, requiredVersion: "^18.2.0" }
+ react-i18next: { singleton: true }
+ i18next: { singleton: true }
+ @module-federation/runtime: { singleton: true }
+```
+
+**Deliberately NOT shared** (bundled inside the remote): `primereact/*`, `src/ui/*`, `@microsoft/signalr`, `i18next-icu`, locale JSON, `@opentelemetry/*`, `leaflet`.
+
+**React singleton is non-negotiable** — mixing React instances breaks Suspense, Context, and Concurrent Mode in ways that are extremely hard to debug. Requirement 1.1.3 mandates Concurrent Mode, which forces `singleton: true`.
+
+### 2.4 Host contract (`src/mf/host-contract.ts`)
+
+The single interface the host must implement when embedding an exposed component. This is the entire embedding contract.
+
+```ts
+export interface HostContract {
+ /** Current language URL prefix, e.g. "ru", "en". Drives i18n + JSON-LD locale. */
+ locale: string;
+
+ /** Absolute base URL of the standalone site, used for canonical links + JSON-LD. */
+ canonicalOrigin: string;
+
+ /** Optional overrides for deep-link navigation inside the host's router.
+ * If omitted, the remote uses window.location. */
+ navigate?: (path: string) => void;
+
+ /** Optional analytics consent state. If omitted, consent is assumed true. */
+ consent?: { analytics: boolean; telemetry: boolean };
+
+ /** Optional host-provided logger to merge with ours (shipped to same backend). */
+ logger?: Logger;
+}
+```
+
+Everything else — data fetching, SignalR, state, styling, SEO tags — is handled inside the remote.
+
+### 2.5 Build artifacts and deploy shape
+
+| Mode | Artifact | Deploy target | Runtime |
+|---|---|---|---|
+| Standalone | `dist/standalone/` (Node server + client bundle + assets) | Dockerized on geo-distributed VMs behind LB (req 4) | Node 20, Modern.js SSR |
+| Remote | `dist/remote/` (static: `mf-manifest.json` + JS chunks + SCSS) | CDN / object storage, long-TTL on chunks, short-TTL on manifest | Static files only |
+
+CI build: `BUILD_TARGET=standalone modern build && BUILD_TARGET=remote modern build`. Both artifacts produced from the same commit in the same CI job and deployed in lockstep.
+
+**SPEC ASSUMPTION (req 9):** directory layout, exposed module names (`./OnlineBoard` etc.), and `name: "aeroflot_flights"` will be reconciled with the customer's standard remote-frontend template when it arrives. The reconciliation is a rename + manifest tweak, not a structural change.
+
+---
+
+## 3. SSR + routing + URL parity
+
+### 3.1 SSR request lifecycle (standalone mode)
+
+```
+1. LB → geo-routed to nearest Modern.js VM
+2. Node → Modern.js SSR entry receives the Request
+3. Middleware → resolve language from URL prefix (src/i18n/resolver.ts)
+ → attach request-scoped logger, trace id, otel span
+ → consent/analytics flags (cookies)
+4. Route match → Modern.js file router matches src/routes/[lang]/…
+5. Loader → route's loader() runs server-side:
+ → calls REST API via shared/api (lang + trace id forwarded)
+ → returns { data, seo } to the component
+6. Render → React 18 renderToPipeableStream with Suspense boundaries
+ → emits , meta, canonical, hreflang, JSON-LD, OG
+ → streaming HTML flushes shell first, feature content as data resolves
+7. Hydrate → client boots, i18next picks up SSR locale, features mount
+ → useLiveFlights opens SignalR ONLY on client (never during SSR)
+8. Response → HTML streamed; Cache-Control per route (§8.2)
+```
+
+### 3.2 Two SSR invariants, enforced
+
+1. **No fetching in component bodies.** Every request runs in a `loader()` or a `useEffect` — never at module top-level, never in a component body. ESLint rule (`react-hooks/exhaustive-deps` + custom rule banning `apiClient.` / `fetch(` outside `loader` / `useEffect` / event handlers). **Satisfies requirement 1.1.3 explicitly.**
+2. **No SignalR on the server.** `useLiveFlights` is a no-op during SSR and opens the websocket only after `useEffect` runs on the client. SSR delivers a data-complete initial render from REST; the websocket takes over for live updates after hydration.
+
+### 3.3 Routing — Modern.js file-based
+
+Route-order precedence matters in two cases:
+
+- **Online Board:** `flight/:p`, `departure/:p`, `arrival/:p`, `route/:p` are more specific than the bare `:params` details route, so they match first. Modern.js's static-before-dynamic rule handles this.
+- **Schedule:** the one-way and round-trip routes (`route/[params]` and `route/[params]/[returnParams]`) are static-prefixed and must match before the catch-all `[...flights]`.
+
+**Catch-all for multi-leg URLs.** `routes/[lang]/schedule/[...flights]/page.tsx` captures the tail as an array of segments. The loader parses it with a ported TS serializer (§3.5) and fetches the legs in parallel.
+
+**Language guard.** `src/routes/[lang]/layout.tsx` validates `params.lang ∈ {ru,en,es,fr,it,ja,ko,zh,de}` and issues a 404 otherwise.
+
+### 3.4 Loaders, Suspense, and `React.lazy`
+
+Every feature page is a `React.lazy()` import inside the route component:
+
+```tsx
+// routes/[lang]/onlineboard/[params]/page.tsx
+import { lazy, Suspense } from "react";
+const FlightDetails = lazy(() =>
+ import("@/features/online-board").then(m => ({ default: m.FlightDetails }))
+);
+
+export async function loader({ params }) {
+ const parsed = parseFlightDetailsUrl(params.params); // feature/url.ts
+ const data = await onlineBoardApi.getFlight(parsed); // server-side REST
+ const seo = buildFlightDetailsJsonLd(data); // feature/seo.ts
+ return { data, seo, parsed };
+}
+
+export default function Page() {
+ const { data, seo, parsed } = useLoaderData();
+ return (
+ <>
+
+ }>
+
+
+ >
+ );
+}
+```
+
+All three React-18 invariants from req 1.1.3 are enforced: loader pattern (no body side effects), Suspense boundary with skeleton, `React.lazy` dynamic import.
+
+### 3.5 URL parity — ported serializers
+
+Each feature has a `url.ts` module with pure functions:
+
+```ts
+// features/online-board/url.ts
+export function parseFlightUrlParams(raw: string): FlightQuery; // "SU100-2025-01-15" → {...}
+export function buildFlightUrlParams(q: FlightQuery): string; // {...} → "SU100-2025-01-15"
+```
+
+These are ports of the Angular URL resolvers (`OnlineBoardFlightNumberUrlParamsResolver`, `ScheduleRouteResolver`, etc.) translated to TypeScript. Byte-exact, pure, unit-tested.
+
+**Parity test suite** compares their output to the Angular app's for every route the Angular unit tests cover, plus fuzz tests (date-format variants, flight-number formats, multi-leg URLs with 2–5 segments). **Merge-blocking CI gate.**
+
+### 3.6 Canonicalization, hreflang, redirects
+
+- **Canonical:** every feature page emits ``. In MF-remote mode, `canonicalOrigin` comes from `HostContract.canonicalOrigin` so the canonical always points at the standalone site, never the embedding host. **This preserves SEO equity when embedded.**
+- **hreflang:** each page emits one `` per language (9 total) + `x-default` → `ru`. Generated by a shared helper; CI parity test asserts the set is identical across all 9 language variants of a given route.
+- **Redirects (all 301):** `/` → `/{default-lang}/onlineboard`; `/{lang}` → `/{lang}/onlineboard`; any missing-prefix URL → `Accept-Language`-detected equivalent.
+
+### 3.7 Deferred decisions (resolved in the implementation plan, not here)
+
+- Exact default-language resolution rule for missing-prefix URLs — will be decided against the current Angular behavior captured in Phase 0 access logs.
+- Default `canonicalOrigin` constant for MF-embedded mode when `HostContract.canonicalOrigin` is not supplied.
+
+---
+
+## 4. Data layer: REST, SignalR, state, hydration
+
+### 4.1 REST client (`src/shared/api/`)
+
+A single `ApiClient` — thin `fetch` wrapper, not a library.
+
+- **Isomorphic:** same code on Node (SSR loaders) and browser.
+- **Language injection:** every URL built from `(lang, endpoint, query)`.
+- **Trace propagation:** injects W3C `traceparent` / `tracestate` headers from the active OTel span.
+- **Timeouts:** 5s default, overridable per call. `ApiTimeoutError` on breach.
+- **Retries:** GET retries once on network error or 5xx with 200ms jitter. POST/PUT/DELETE never retry.
+- **Error typing:** `ApiHttpError(status)`, `ApiTimeoutError`, `ApiNetworkError` — features switch on class, not on strings.
+- **Circuit breaker:** 5 consecutive failures → open for 30s → half-open probe → closed on success.
+
+No Axios. No TanStack Query. No SWR.
+
+### 4.2 Caching strategy
+
+**SSR request cache (per-request, discarded after response).**
+`Map>` on the request context. Deduplicates concurrent `apiClient.get(url)` calls within one SSR render.
+
+**Client cache (per-tab, in-memory, TTL-bounded).**
+Custom ~80-line cache keyed by full URL. TTL: 30s for search results, 5m for static reference data (airports, cities). Replaces the Angular `CacheService` directly.
+
+**Shared server-side LRU cache (per-VM).**
+`@isaacs/ttlcache`, in-memory, ~100MB cap. Keyed by `(endpoint, queryParams, locale)`. TTL: 30s for live data, 5m for static data. Per-VM, not shared across VMs — no Redis in the frontend tier (adds failure modes).
+
+### 4.3 SSR → client data handoff
+
+1. **Server loader** calls REST, returns `initialData`.
+2. **Page component** passes `initialData` as a prop to the feature root.
+3. **Feature root** seeds local state from `initialData` on mount; `useLiveFlights` then opens the websocket (if applicable).
+
+No double-fetching. Data is serialized into the HTML via Modern.js's `useLoaderData` hydration, not a custom `window.__INITIAL_STATE__`.
+
+### 4.4 SignalR wrapper (`src/shared/signalr/`)
+
+Two layers.
+
+**Layer 1 — `SignalRConnection` (pure TS).** Singleton per hub URL. Reference-counted subscriptions. Single `HubConnection` with `withAutomaticReconnect([0, 2000, 10000, 30000])`. Grace-period close (5s after last unsubscribe) to survive React 18 Strict Mode double-mount and route-change thrash. Emits lifecycle events (`connected`, `disconnected`, `reconnecting`, `reconnected`) into logger and metrics. **Never imported during SSR** — dynamic-imported inside `useLiveFlights`'s `useEffect`.
+
+**Layer 2 — `useLiveFlights(params, initialFlights)` hook.**
+
+```ts
+function useLiveFlights(
+ params: { date: string; departure?: string; arrival?: string },
+ initialFlights: Flight[]
+): { flights: Flight[]; connectionStatus: "idle"|"connecting"|"live"|"reconnecting"|"offline" };
+```
+
+Behavior:
+
+1. Initializes from `initialFlights` (SSR payload).
+2. On `useEffect` mount: dynamic-imports `SignalRConnection`, subscribes to the channel.
+3. On `RefreshDate` push: merges into local state with a reducer that preserves UI state (selected flight, expanded rows).
+4. On unmount: unsubscribes. Grace-period close handles Strict Mode double-invoke correctly.
+5. SSR: returns `{ flights: initialFlights, connectionStatus: "idle" }` without touching the SignalR module.
+
+**Vitest test simulates two rapid mounts and asserts a single `HubConnection.start()` call.**
+
+### 4.5 State management
+
+**No global store.** No Redux, no Zustand, no Jotai.
+
+- `useReducer` for feature-root filter + result state (ports Angular `BehaviorSubject`s to a single filter reducer).
+- `useState` for local UI state.
+- `useContext` for request-scoped things: locale, `HostContract`, `ApiClient`.
+- `useSyncExternalStore` for subscribing to `SignalRConnection` lifecycle events.
+
+**`useSearchHistory`** (~60 lines, in `shared/hooks/`) replaces the Angular `SearchHistoryService`: per-language `localStorage` key, returns `[history, push, clear]`.
+
+### 4.6 Error handling path
+
+- **SSR loader throws `ApiHttpError(404)`** → renders `routes/error/[code]/page.tsx` with `code=404`, HTTP response 404.
+- **SSR loader throws `ApiHttpError(5xx)`** → same, with `code=500`, HTTP 500.
+- **SSR loader throws `ApiTimeoutError`** → "data temporarily unavailable" page with HTTP 503 + `Retry-After`, structured log at `error`.
+- **Client-side refetch error** → inline error UI (red banner, "Retry" button); logs at `warn`; `flights.api.error` counter. Never yanks the user to a full-page error.
+- **SignalR disconnect** → UI "offline" badge; keeps showing last-known data; logs at `info`; `flights.signalr.disconnect` counter.
+
+---
+
+## 5. UI adapter + styling + pixel-parity strategy
+
+### 5.1 The `src/ui/` boundary
+
+Every visible DOM element is produced by a component from `src/ui/`. No feature file imports `primereact/*` directly, imports Leaflet directly, or writes raw styled form controls. Enforced by ESLint `no-restricted-imports` + a custom rule banning bare `