What this PR does: Rebuilds the /shipment/:po detail page from a loosely stacked set of cards into a dense, scannable dashboard. The page now opens with a masthead containing a journey-progress ladder, a five-tile KPI strip (cost exposure, estimated delay, ports disrupted, next arrival, free time left), and a decision-countdown urgency band — all before the map. The map is promoted to a full-width hero. Below it, a 2:1 grid places port status + mitigation options on the left and voyage itinerary + cost exposure on the right, balancing column heights.
Signals page: The "Shipment Alerts" collapsible section gains an always-visible aggregate stats strip (tracked shipments, total exposure, ports disrupted) and redesigned alert cards with severity-colored left-accent bars, sentence-case disruption headlines, and a hover-lift micro-interaction.
Motion pass: A shimmer-sweep skeleton precisely mirrors the final layout (no content jump on load). Loaded content fades up via shipment-fade-up. The journey ladder bars grow left-to-right with staggered delays. KPI tiles stagger in. The urgent AlertTriangle icon pulses. Option cards and alert cards lift on hover. All animations are disabled under prefers-reduced-motion.
No server changes, no new npm dependencies. All data comes from the existing /api/shipment endpoint via useShipmentDetail. The data-testid attributes referenced by e2e tests are preserved.
shipment-fade-up entrance class.useMemo. Imports formatUsd from the new shared module.shipment-ladder-bar entrance class with 60ms per-segment delay. Active segment gets shipment-stage-active pulse. Unknown stages handled gracefully with a fallback message.shipment-kpi-tile CSS class for nth-child stagger. Derives next arrival from the first port call without an actual_arrival.PortStatusCard. Renders all ports in a sticky-header desktop table and mobile article cards, sorted disrupted-first by severity. Active disruptions get full narrative articles with started/resolution/confidence/hard-blocked/nationwide chips. Hover lift on mobile cards and disruption articles.CostTable. Renders a responsive 2-column grid of option cards. Recommended card gets emerald highlight + border intensification on hover. Each card shows cost, time saved, confidence, cost-component chips, and source attribution. Returns null when no non-watch actions exist.PortStatusPanel which shows all ports in a single cohesive table + disruption detail section. No other consumers existed.STAGE_ORDER, STAGE_LABELS, STAGE_VARIANTS, stageIndex(), stageLabel(), stageVariant(). Previously duplicated across shipment.tsx and the old card. Consumed by JourneyProgress, ShipmentPage badge, and ShipmentAlertCard.portStatusLabel(), portStatusVariant(), isPortDisrupted(), portSeverityDisplay(), severityChipStyle(). Shared across PortStatusPanel, VoyageItinerary, ShipmentKpiStrip, and ShipmentAlertCard.formatUsd(value) — null-safe dollar formatter (guards null/undefined/NaN, rounds before formatting). Shared across KPI strip, CostTable, MitigationOptions, ShipmentAlertCard, and alerts.tsx. Eliminates five inline $${Math.round(...).toLocaleString()} patterns.ShipmentMetric — reusable dt/dd tile with micro-label, value, optional note, and danger tone. Shared by CostTable and MitigationOptions. Eliminates duplicated inline metric patterns.hover:-translate-y-px hover:shadow-md). Arrow shifts on hover via group-hover:translate-x-0.5.ShipmentMetric dl grid. Surfaces reroute cost delta, airfreight uplift, wait-vs-act threshold, and tariff source. Mitigation options extracted to MitigationOptions. ActionButtons merged into card footer.AlertTriangle/Clock icons. Urgency tones (red/amber/neutral). Redundant exposure/free-time stats removed (now in KPI strip). shipment-urgent-icon pulse class on the triangle when isUrgent.all_ports_affected: true before extracting country codes. Previously showed "All X ports affected" for every disrupted port. Added CA → Canada to country map. Added Ban icon. Returns null when no nationwide ports.CardHeader (consistent card chrome). No-coordinates fallback restyled with port chips. WebGL fallback given matching header. Map height tuned for full-width hero position. DeckGL layer logic unchanged.buttonVariants. Full-width button on mobile. Modal restyled with design tokens, Check/Send/X lucide icons, and blurred overlay..shipment-* classes: shimmer sweep (shipment-shimmer), fade-up entrance (shipment-fade-up), KPI tile stagger (shipment-kpi-tile nth-child delays), ladder bar grow (shipment-ladder-bar), active stage pulse (shipment-stage-active), urgent icon pulse (shipment-urgent-icon), SVG map route trace (shipment-map-route), port dot blink (shipment-port-dot). Single @media (prefers-reduced-motion: reduce) block disables all of them. No existing keyframes modified.The original NationwideBanner extracted country codes from all disrupted ports and displayed "All X ports affected — a domestic reroute does not avoid this disruption." This was factually wrong: a port being impaired does not mean every port in that country is affected.
Fix: filter disruptedPorts to only entries where status.all_ports_affected === true before extracting country codes. The component now returns null when no ports carry that flag, so the banner only appears when the data actually warrants it. Also added CA → Canada to the country name map (the app tracks Montreal and Halifax routes).
The fix is in nationwide-banner.tsx: const nationwidePorts = disruptedPorts.filter(([, s]) => s.all_ports_affected).
The previous skeleton was three generic Skeleton blocks that bore no resemblance to the final layout. When data arrived, the page would reflow significantly.
The new ShipmentSkeleton renders: back-link skeleton, title + badge row, 6-segment journey ladder bars (matching STAGE_ORDER.length), 5-column KPI strip tiles, a countdown block, an animated SVG map placeholder (two shipment-map-route path traces + four shipment-port-dot circles), and the 2:1 grid with correctly-sized column blocks.
Every skeleton block carries a shipment-shimmer class (a 1.6s shimmer sweep via ::after pseudo-element) with staggered animationDelay values so tiles light up sequentially. The SVG map placeholder reuses the existing trace-draw and map-port-blink keyframes already in index.css.
All nine shipment-scoped animation classes are disabled by a single @media (prefers-reduced-motion: reduce) block at the end of the new CSS section in index.css:
.shipment-shimmer::after, .shipment-fade-up, .shipment-kpi-tile, .shipment-ladder-bar, .shipment-stage-active, .shipment-urgent-icon, .shipment-map-route, .shipment-map-route-b, .shipment-port-dot { animation: none !important; transform: none !important; opacity: 1 !important; }
The existing global animate-pulse on Skeleton is unaffected — Tailwind's built-in motion-safe variant already handles it. No other pages are affected because all new classes are prefixed shipment-.
The e2e smoke suite references data-testid="signals-shipment-alerts" on the collapsible Card in alerts.tsx. This attribute is preserved exactly as found.
New data-testid attributes added for the new widgets: shipment-detail (page wrapper), shipment-journey-progress, shipment-kpi-strip, shipment-decision-countdown, shipment-nationwide-banner, shipment-route-map, shipment-port-status, shipment-port-row, shipment-voyage-itinerary, shipment-cost-exposure, shipment-mitigation-options, shipment-option-card, shipment-alert-card. These are additive and do not conflict with existing selectors.
The original layout placed the map in the left column of a 2-col grid. When the right column (itinerary + cost) ran longer than the left (map + port cards), a large dead zone appeared below the map.
The fix: promote the map to full-width above the grid. Below it, a 2:1 grid places PortStatusPanel + MitigationOptions on the left (wide) and VoyageItinerary + CostTable on the right (narrow). This balances the two columns to within ~130px of each other in the typical data scenario.
On mobile, everything stacks: map → port status → mitigation options → voyage itinerary → cost exposure. The KPI strip collapses from 5 columns to 2 columns (sm: 3, lg: 5).
data-testid contract for e2e is preserved. The nationwide banner bug is fixed. The skeleton mirrors the final layout. All animations respect prefers-reduced-motion.