/**
 * NuAskSylvan — Time Corridor UI: PAST / NOW / NEXT (fullscreen in app shell when this tab is active).
 * Time corridor: shared rail height + inbound/outbound dots; PAST / NOW / NEXT **card rows** share one **`TIME_CORRIDOR_GRID`** (`auto 1fr auto 1fr auto 1fr auto`) so dashed joins stay aligned when swiping (no wider center column on NOW). NOW meta band
 * matches PAST/NEXT row count for aligned rails. Header avatar = profile photo (Flow Plan / cache pattern) → PowerUp; day strip removed.
 * PAST: **completed** activities only (`authoritative local end ≤ now`) from the loaded itinerary timeline; when none yet (e.g. trip starts tomorrow), the PAST rail shows **ghost** slots — never in-progress or future stops as if visited. **First paint:** PAST corridor + journey strip + places count initialize as **ghosts / zero** (no `DEMO` POI flash) until `loadPastCorridorData` + `computeTemporalCorridor` run. **Day summary copy:** the “{count} places” line and the **PLACES** ring both use **`placesActivityCount`** (length of **completed**), not the count of filled **visible** corridor slots (**pastRealCount**, max 3). **#178:** roles are split in `nuask-temporal-roles.js` — PAST/Earlier Stop = completed; CURRENT = `start <= now < end`; NEXT = `start > now`; Add Here 30m lock uses **latest started start**, not completed PAST.
 * Day summary + map stay mocked. **Panel strip** (过往 · 当下 · 接下来) below the header jumps/slides the horizontal corridor; corridor meta shows **times row only** above the card rail; localized stat labels on the day-summary grid (discovery is visual — corridor/timeline, no instructional banner). **Corridor swipe settle:** PAST/NOW/NEXT track uses **snap-x mandatory** + **snap-always** panels; CSS **`scroll-smooth` is not** on the track (smooth only for tab `goToPanel`); **`setPanel` / A2 chrome wait until scroll settles**, then force-snap to the nearest panel so mid-swipe remounts cannot leave the UI stuck between screens.
 * **Add Here — post-start lock:** For **30 minutes** after the start time of the **latest started / current** itinerary activity (independent of NOW Earlier Stop / completed PAST), **Add Here** is **disabled** (not merely a post-click error); **Replace Next** stays enabled when a real next row exists unless that row is a **fixed booking** (`hard_booking` / `booking_ref`) — then Replace Next is disabled proactively with localized hint (**#124.9**). While disabled, a **single amber status strip** under the action row explains the gate (same chrome for loading, no-reco, post-start lock, hard-booking replace block, and post-add en-route replace-only). `POST /api/micro-action/propose-slot` returns **`reason_code: "SLOT_AFTER_COMMITMENT"`** when the computed slot would pass the safe window before the next commitment; the UI shows **`nuask_propose_slot_after_commitment`** (10 locales), not raw English `reason` text. **`POST /api/micro-action/select`** patch failures map **`patch_code`** (`ERR_HARD_BOOKING_PROTECTED`, **`ERR_COMMIT_REVALIDATION`**, **`ERR_PATCH_OVERLAP`**) to NuAsk i18n keys in the amber strip (**#124.8**). Select sends **`coords`** + optional **`slot_confirmed`** for server-side x+y+z on existing-day adds (**#124.5**).
 * Travel-only / Decide→Go (2026-08-12): when straight-line travel to the next stop meets/exceeds the remaining window **or** post–Add Here en-route lock is active with POI coords, the **existing TOP reco rim** swaps **inner content only** to a Go card (map hero + ETA + Open directions — **CN → Gaode / GLOBAL → Google** via preview `region` + Smart Brain–aligned `detectMapsRegion`). Modes: **decide** (TOP reco + quiet Next · Maps) · **go_added** · **go_next** · back to decide when lock/leave-soon clear. **Mode B action row:** left **Undo** (enabled with today trip key — not session-only `undo_available`; lock persists `undo_available` in localStorage; select response `trip_key` hydrates mutation key after first create-minimal add) · right **Replace Next** (forced disabled). En-route copy is Undo-first (not Replace-only). **First-add undo:** server may `trip_torn_down` so user returns to no today Flow Plan. No new containers. Plan: **`docs/pillars/NOW_TV_DECIDE_GO_PLAN_LOCKED.md`**.
 * NOW rail: **A2 Option A (`NUASK_A2_COLLAPSE_CORRIDOR`)** — default **words strip** (prev · available window · next); a **centered frosted glass arrow** **seam-overlays** the corridor/TV edge (no reserved row) and expands to today’s full cards + sync orb + window band. Leave-soon / travel alerts stay visible while collapsed. When the flag is off, the full rail is always shown. “Available Window” sits in a full-width row **below** the three-column corridor (expanded) so dashed connectors stay one straight line; top-reco card uses taller viewport + hero band for more visible detail.
 * NOW refresh column: **small** confined ripple rings behind the sync orb (`clamp(76px–124px)`), not viewport-sized — avoids phone-wide “radar.” While syncing, the orb’s center specular is hidden so it does not read as a warm dot through the translucent sync mask. Orb glow + TOP header LED use soft gradients / `nuask-led-halo` (no `animate-ping`).
 * Inline latest reply: optional **hologram** panel (~half viewport) with cyan scan-line styling + **4-button** bar (Back · Full chat · Copy · Clear); keys **1–4** when holo is open; **Back** returns to compact strip; **Expand** restores holo from compact.
 * NOW top-reco header: HUD-style bar (cyan LED, **Top recommendation** title + tier badge column). Tier copy (**STRONG FIT** / …) uses a **single tap target**: tier pill + amber **!** open the same **popover** (`topRecoHudCaption`). **Alternatives** heading + **!** share one **button**; POI-hours **short line + !** share one **button**. Popovers: **Escape** / scoped **pointerdown outside** / **Got it** (`data-nuask-explain-root`). Sync orb halos use **`nuask-orb-idle-breathe`** (theme keyframes: **~1.28×** peak scale, same 2s cadence + opacity swing) so the pulse reads around the static 62px core.
 * Alternatives: rows show **PERSONALIZED** / **NEXT BEST** / **STRONG FIT** chips from `recommendation_tier` on the POI (not list index). Envelope first-add may distance-sort the three; nearest is not relabelled STRONG FIT. Tapping an alternative **snaps** the NOW inner scroll surface (`nowSectionScrollRef`) to **top**.
 * TOP reco HUD+: loading sync intensifies grid/LED/badge; subtle SVG noise on header.
 * TOP reco shell: **static** full-perimeter ring + inner card surface (**rim gutter** = parent `p-[2px]`, not child `m-[2px]`+`w-full` which clips the right/bottom edge under `overflow-hidden`); hero photo fallbacks: POI media → next itinerary activity image → map tile.
 * TOP reco gate: full recommendation container is hidden until an **explicit** NOW-orb tap completes (`nowRecoSyncPhase === 'ready'`). Default open shows **Available Window** (genuine duration + usable-interval range; **0m hides the range row**; **Add Here 30m lock hides the whole AW chrome**). **#178 occupancy idle TV (LOCKED):** while CURRENT is proven (`start ≤ now < end`), idle CI TV names that stop (image + existing write-up, no invented copy). **Tap NOW** uses the **same 30m post-start lock as Add Here** (`addHerePostStartLockActive` / `nowCorridorPrevStartMs`); silent when locked (no spinner / toast / explanation). Orb is **grey-tinted** (`aria-disabled`) while that 30m lock is active **or** leave-soon `go_next` (catalogue §16; not 现在出发); **Hold** is unchanged. **#181 Slice 1 (`UNLOCK_UI_GOLD` inner LEFT/RIGHT only):** after fray, LEFT situation stays mounted (occupancy + AW + CI; leave-soon compact **TOP** on LEFT when `go_next`). Tap NOW reco is RIGHT inside the same plate (does not replace LEFT). A2 seam arrow (`NUASK_A2_SEAM_ARROW_*`) appears inside the plate only after reco is ready. **#181 Slice 2:** NOW LEFT activity prep binds occupancy CURRENT (same occurrence identity as NEXT Before you go), not `next_activity_ref` / remaining[0]. `nowCiKeptEnrichment` is scoped to day + NOW owner + CURRENT. NEXT→NOW carry-forward matches CURRENT in the existing evaluate packet (`activity_preparation` / `later_preparation`). **#181 Slice 3:** durable/culture on NOW LEFT requires device GPS (`liveUserCoords`); no coords → no culture (fail closed). `#180` destination-string is not the trigger. No city-coordinate dictionary. Empty seed still silence. **#178-A:** on `go_added`, existing `nowCiVisibleLines` render **once**, **above** the 176px map (whole plate). Live `#178-A` CI-top + warning-bottom on cyan rim stays VERIFIED on production `sfRev=92`. Empty CI is quiet — no filler. Corridor strip does **not** repeat the same `travelLeaveAlert` during `go_next`. **#178-B** 0m hides range. **#175 Enter the fray** holds NOW TV until corridor hydrate + the current-latch CI evaluate attempt has settled (`nowEnterTheFrayHold`), including while leave-soon `go_next` is already true — then the full TV displays. Also while `nowRecoLoading` after an explicit orb tap. Not the idle destination. Ambient GPS retry does **not** re-enter. Auto-preview on open stays forbidden. Weather-only re-evaluate **keeps** prior enrichment bullets (`retainNowCiEnrichmentLines`) only while the NOW owner key is unchanged. On reveal, a short blur/translate-in animation runs.
 * Corridor chrome gate (NOW): collapsed words strip / expanded rail (+ expand arrow) stay hidden until **itineraries hydrate** (`corridorDataReady`) — then **`nuask-calm-fade-in`**. Corridor chrome wrapper uses **`z-[51]`** above the TV plate (**`z-[50]`**) so the seam arrow is not trapped under the middle container. **Expanded seam arrow:** `translate-y-[120%]` + **`mb-6`** so the frosted control sits in empty corridor→TV seam (does not cover Available Window / bridge copy); collapsed stays `translate-y-[60%]` / `mb-2`. PAST/NEXT corridor blocks still calm-fade after itinerary hydrate alone.
 * NEXT: **same time-corridor contract** as PAST/NOW — `CORRIDOR_META_BAND` wraps **times row only** (`TIME_CORRIDOR_GRID`), then card rail; third slot uses **`nextLiveSecond`** (honest empty chrome when none — never DEMO Merlion / Gardens). Below the rail, **Upcoming highlight** (selected FA1/FA2 identity, keep 152px hero) and **Before you go** (selected-activity CI preparation; no 86px second-activity image). FA1/FA2 are selectable; Available Window stays informational. Highlight opens existing activity-detail for the selected row; View on Map stays its own action.
 * NOW hotbar (panel NOW): **A2 preview (`NUASK_A2_COLLAPSE_HOTKEYS`)** — the same four hotkeys (Explore Nearby · Improve Plan · Use My Time · Emergency) are **not** a permanent strip. A **small frosted glass arrow** **seam-overlays** the TV/chat edge (half above / half into the dock; no dedicated bar height). Tap → frosted **drawer** with the four; choosing a hotkey **closes** the drawer and shows content in the existing TV/container (unchanged shells/APIs). When the flag is off, the permanent four-tile strip is restored. **Explore Nearby** → **`NuAskNearbyPoisShell`** (`POST /api/nuask/nearby-pois` with **`nearby_personality_focus: true`**: catalog still **near** device coords, but Smart Brain passes **`extractTravelerTags`** + optional **must-match** into POI Catalog and sorts **catalog score then distance**); hotkey icon = **📍** map-pin emoji with a light **CSS hue-rotate** so default red reads **emerald** (matches tile `ring`/`glow`); not a custom SVG pin. **Improve Plan** → **`NuAskWeatherPlanShell`** (`POST /api/nuask/weather-day-suggest`); **Use My Time** → **`NuAskUseMyTimeShell`**: **`POST /api/nuask/nearby-pois`** without personality flag, client **gap-fit** labels (travel + dwell vs **Available Window** minutes)—time-budget lane (Ask Sylvan via NOW hotkeys / TV chat, no in-shell CTA). **Emergency** POSTs `EMERGENCY` chat, **`NuAskEmergencyShell`** until Back/Cancel.
 * **Trip keys:** **`corridorTripKey`** feeds corridor/display + **`GET …/itineraries/:tripKey`**. Mutation bodies (**`/api/micro-action/select`**, **`/api/nuask/weather-day-suggest`**) attach **`trip_key`** only from **`corridorMutationTripKey`** (**`is_today_plan`** row); omitted when none (pairs Smart Brain Issue **#61**).
 * **SPA (AppShell):** For \`nu-ask-sylvan\`, \`pages/app.js\` **hides** app-shell bottom tabs and passes \`embeddedAppShell: false\` so this page renders **\`InternalShellTwinNav\`** (**Profile · Explore**; left caption SYS **\`ui:nuask_nav_profile\`**, same as Explore — not \`ui:tab_powerupai\`) beneath the inline chat strip. **NOW dock (Explore parity):** when A2 is off, hotkeys use **\`shellTwinDockOuter\` / \`shellTwinDockHubInner\`** (same tokens as Explore/Power Up hub); when A2 is on, that hub band is **omitted** (space returns to TV) and a floating frosted glass arrow opens the hotkey drawer. The **chat** band uses the same **\`#030508\` / hairline \`border-t\`** treatment as the immersive shell (no extra pulse — matches Explore hub calm strip); **\`InternalShellTwinNav\`** uses **\`staticTwinEdge\`** so the **60px twin** strip keeps **\`sf-twin-shell-nav-pulse\`** alone (same as Explore twin). **PAST / NEXT:** same dock band **persists empty** (no mic, no Hold-to-talk, no wordings). **NOW:** mic + composer (**tap-to-talk** STT sends optional **live GPS** so Smart Brain can soft-correct garbled local place names; inline chat is **tourist-centre / tour-guide** `nuask_info_only`); **Full chat** opens a **layered glass panel** over the TV (\`AskSylvanPage\` + \`embeddedExploreTv\` + \`embeddedGlassPanel\`). **NOW panel:** corridor strip stays fixed; **TV frame** (\`rounded-[13px]\` / \`overflow-hidden\`) uses **\`SYLVAN_TECH_THEME.classes.dockScrollPaddingBottom\`** + **\`tvRimMaxHeight\`** so the plate stays **above** the fixed dock (hotkeys or chat-only) + chat + twin nav (chat sits directly on top of twin nav, \`gap-0\`). Inner \`overflow-y-auto overscroll-y-contain\` only. **Open full chat** on NOW mounts **\`AskSylvanPage\`** (\`embeddedExploreTv\`) inside that TV frame — **NOW-only**; PAST/NEXT omit that control.
 *
 * **Responsive contract:** layout tokens mirror **`window.SYLVAN_TECH_THEME.layout`** (`pages/shared/sylvan-tech-theme.js`) and are bound here as **`NUASK_LAYOUT`** (with inline fallback if the script is absent). Typical manual QA is **iPhone 14 Pro Max** (~430×932 CSS px); do not add viewport-specific `sm:`/`md:` hacks for that frame—change the shared layout so **360–440px-wide** phones share the same behavior.
 * **Why this recommendation?** Traveler-facing short lines for all users (weather lines include live **`{temp}` / `{condition}` / `{pop}`** slots from preview `weather_context`); full engine-trace bullets (weather enums, down-rank, tags, scores) only when admin-gated (**AppShell `isAdmin` prop** and/or NuAsk self-check via **`GET /api/admin/check`** — belt-and-suspenders so a stale `app.js` cache cannot strand admins on traveler copy). Preview API fields stay intact for admin metrics.
 * **Ex.1 NOW radial (Alive #153 chrome; Hold Phase 1–5 UNLOCK_UI_GOLD 2026-08-20; HIS three-zone TV UNLOCK_UI_GOLD 2026-08-21; A10 conclude 2026-08-21; A22 why-not-B after Add 2026-08-21; F8 trust nudge / F10 conclude-exit stamp 2026-08-21):** Long-press the **pulsing NOW sync orb** is the **doorway only** — thumb release keeps the frosted intelligence space open (`data-ex1-hold-persistent`); **tap** L1→L2 to select (not press-drag-release). Enter uses `hold_mode=intelligence_space` (CONTEXT→TAG_EXPLORE→POI→COMMIT). Select posts LEARNING_KV + session `event`/`release`; **does not** close the overlay or mid-session dump to NOW TV — memory chips + POI focus stay in hologram. Governor Add/Replace runs **inside** the overlay (`Checking…` → optional **why-not-B** radial when ≥2 POIs → HIS TV **conclude plate**: See Flow Plan / Back to NOW). Not now → conclude (Keep exploring / I'm good); 2nd+ dismiss may open **trust_nudge** before conclude. Conclude exits stamp `conclude_exit` on session (fail soft). Tap NOW (short) still refreshes TOP only. No post-close `ex1GovernorIntent`. Phase 5: **no Hold fallbacks** (SOP six · invented companion · fuzzy bind). Enter miss / `SF_EX1=0` → fail closed: empty seats + silence + “Check-in isn't available right now.” See `EX1_HOLD_INTELLIGENCE_SPACE.md`.
 */
(function () {
  'use strict';

  /**
   * A2 (live feeling check): collapse NOW’s four hotkeys behind a floating frosted glass arrow → drawer.
   * Flip to false (or revert) + redeploy Pages to restore the permanent four-tile strip.
   */
  const NUASK_A2_COLLAPSE_HOTKEYS = true;

  /**
   * A2 Option A: NOW corridor collapses to a centered words strip; frosted glass arrow expands full rail.
   * Flip to false + redeploy to restore always-expanded corridor.
   */
  const NUASK_A2_COLLAPSE_CORRIDOR = true;

  /**
   * Shared seam-arrow chrome — frosted glass (see-through).
   * Fill/blur use inline styles so UA opaque `<button>` paint cannot win and so
   * translucency does not depend on Tailwind arbitrary opacity utilities.
   */
  const NUASK_A2_SEAM_ARROW_CLASS =
    'pointer-events-auto flex h-8 w-8 shrink-0 appearance-none items-center justify-center rounded-full border border-solid border-white/40 bg-transparent p-0 text-white/85 shadow-[0_4px_18px_rgba(0,0,0,0.25),inset_0_1px_0_rgba(255,255,255,0.35)] transition duration-200 motion-safe:active:scale-95 hover:text-white';
  const NUASK_A2_SEAM_ARROW_STYLE = {
    backgroundColor: 'rgba(255, 255, 255, 0.12)',
    backdropFilter: 'blur(14px) saturate(1.45)',
    WebkitBackdropFilter: 'blur(14px) saturate(1.45)'
  };

  const { useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } = React;

  const FONT =
    "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Inter', 'Segoe UI', Roboto, sans-serif";

  const ST = typeof window !== 'undefined' && window.SYLVAN_TECH_THEME ? window.SYLVAN_TECH_THEME : null;

  /** Add Here disabled for this many ms after the corridor “previous” activity start (Replace Next stays available). */
  const NUASK_ACTIVITY_POST_START_ADD_LOCK_MS = 30 * 60 * 1000;

  /** Reference accent ≈ #2DE2C5 — from shared theme when loaded */
  const C = ST?.palette || {
    accent: '#2DE2C5',
    accentMuted: 'rgba(45,226,197,0.35)',
    accentSoft: 'rgba(45,226,197,0.12)',
    bg: '#000000',
    card: '#111111',
    stroke: 'rgba(45,226,197,0.45)'
  };

  const DEMO = {
    nowLocal: '1:48 PM',
    windowEnd: '3:00 PM',
    windowMins: 42,
    prevTime: '11:30 AM',
    prevStop: 'ArtScience Museum',
    nextStop: 'Merlion Park',
    nextTime: '3:00 PM',
    pastStops: [
      { time: '8:30 AM', title: 'Changi Airport', sub: 'Arrived', icon: '✈️' },
      { time: '10:00 AM', title: 'Marina Bay Walk', sub: 'Walk', icon: '🚶' },
      { time: '11:30 AM', title: 'ArtScience Museum', sub: 'Visited', icon: '🚶' }
    ],
    stats: { places: 3, steps: '8,432', distance: '6.8 km', duration: '3h 15m', energy: 'High' },
    reco: {
      name: '% Arabica Coffee',
      tag: 'STRONG FIT',
      lineWalk: '5 min walk (0.4 km)',
      lineDwell: '35 min (Ideal duration)',
      lineRating: '4.6 (1.2k reviews)',
      blurb: 'Great coffee, calm vibe. Just 5 min walk from here.'
    },
    merlionDesc: 'Iconic Singapore landmark with amazing photo spots.',
    afterThat: { time: '4:00 PM', title: 'Gardens by the Bay', sub: 'Explore' },
    /** NOW hotbar — matches decision-radar spec; `target` null = mock only; per-tile glow + ring tint */
    nowHotbar: [
      {
        key: 'nearby',
        title: 'Explore Nearby',
        sub: 'Nearby picks ranked to your profile & personality.',
        icon: 'pin',
        glow: 'rgba(34,197,94,0.35)',
        ring: 'rgba(52,211,153,0.4)',
        target: null
      },
      {
        key: 'improve',
        title: 'Improve Plan',
        sub: 'Weather vs today’s stops.',
        icon: 'sliders',
        glow: 'rgba(56,189,248,0.35)',
        ring: 'rgba(125,211,252,0.4)',
        target: null
      },
      {
        key: 'time',
        title: 'Use My Time',
        sub: 'What fits your window.',
        icon: 'stopwatch',
        glow: 'rgba(167,139,250,0.35)',
        ring: 'rgba(196,181,253,0.4)',
        badgeMins: true,
        target: null
      },
      {
        key: 'emergency',
        title: 'Emergency',
        sub: 'Quick help if needed.',
        icon: 'shield',
        glow: 'rgba(248,130,18,0.35)',
        ring: 'rgba(251,162,52,0.4)',
        target: null
      }
    ]
  };

  /**
   * Single layout contract for **all phone widths** (360–440 CSS px): rem + svh/dvh + one max column width.
   * Canonical values live in `pages/shared/sylvan-tech-theme.js` — keep this fallback in sync.
   */
  const NUASK_LAYOUT = ST?.layout || {
    shell:
      'relative mx-auto flex h-full min-h-0 w-full max-w-[440px] flex-col overflow-x-hidden text-white [touch-action:manipulation]',
    sectionPad: 'px-4 pt-3',
    nowSectionPad: 'px-4 pt-3',
    recoTopDefault:
      'relative overflow-x-hidden rounded-2xl p-[2px]',
    recoTopOverlay:
      'relative overflow-x-hidden rounded-2xl p-[2px]'
  };

  /**
   * NOW TV corridor — same contract as Explore (`ExplorePage.jsx` exploreMainColumn + tv rim) and Power Up:
   * bottom padding clears the fixed hotbar + twin-nav stack; rim max-height caps the plate above the dock.
   * Source of truth: `pages/shared/sylvan-tech-theme.js` → `classes.dockScrollPaddingBottom`, `classes.tvRimMaxHeight`.
   */
  const NUASK_NOW_DOCK_SCROLL_PB =
    ST?.classes?.dockScrollPaddingBottom ||
    'pb-[max(0.75rem,calc(9.25rem+env(safe-area-inset-bottom)))]';
  const NUASK_NOW_TV_RIM_MAX =
    ST?.classes?.tvRimMaxHeight ||
    'max-h-[min(82svh,calc(100dvh-9.25rem-env(safe-area-inset-bottom)))]';

  /** Panel titles/subtitles default from `pages/logic/nuask-ui-i18n-bootstrap.js` → `window.NUASK_UI_DEFAULTS` */
  /** zh-CN (and future) packs from `pages/logic/nuask-ui-lang-packs.js` — generated by `scripts/merge-nuask-now-ui-strings.js`. */

  /** Match Smart Brain `/api/sys-message` normalization (`zh` → `zh-CN`). */
  function normalizeNuAskUiLang(code) {
    if (!code || typeof code !== 'string') return 'en';
    const l = code.trim().toLowerCase();
    if (l === 'zh' || l === 'zh-cn' || l === 'chinese') return 'zh-CN';
    return code.trim();
  }

  /**
   * PROFILE_KV `preferred_language` is sole preference authority (SYSTEM_MAP).
   * NuAsk uses this for STT `languageCode`, chat payloads, and SYS/UI locale — then app language state / cache.
   */
  function resolveNuAskPreferredLanguageCode() {
    if (typeof window === 'undefined') return 'en';
    try {
      const pl = window.SylvanFlowState?.getProfile?.()?.preferred_language;
      if (typeof pl === 'string' && pl.trim()) return pl.trim();
    } catch (e) {
      /* noop */
    }
    try {
      const gl = window.SylvanFlowState?.getLanguage?.();
      if (typeof gl === 'string' && gl.trim()) return gl.trim();
    } catch (e2) {
      /* noop */
    }
    return 'en';
  }

  /** English defaults overlaid with embedded locale pack when KV has not hydrated yet (avoids English UI for zh users). */
  function buildNuAskUiStringBase(langCode) {
    const defaults =
      typeof window !== 'undefined' && window.NUASK_UI_DEFAULTS ? { ...window.NUASK_UI_DEFAULTS } : {};
    const n = normalizeNuAskUiLang(langCode);
    const packs = typeof window !== 'undefined' ? window.NUASK_UI_LANG_PACKS : null;
    if (n === 'zh-CN' && packs && packs['zh-CN']) {
      return { ...defaults, ...packs['zh-CN'] };
    }
    return defaults;
  }

  function arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    const chunk = 0x8000;
    let binary = '';
    for (let i = 0; i < bytes.length; i += chunk) {
      const slice = bytes.subarray(i, i + chunk);
      binary += String.fromCharCode(...slice);
    }
    return btoa(binary);
  }

  /** Inline composer is plain text only — strip common markdown so **bold** does not show asterisks. */
  function plainTextFromChatMarkdown(s) {
    if (typeof s !== 'string' || !s) return '';
    let t = s.replace(/\r\n/g, '\n');
    t = t.replace(/\*\*([^*]+)\*\*/g, '$1');
    t = t.replace(/\*([^*]+)\*/g, '$1');
    t = t.replace(/`([^`]+)`/g, '$1');
    return t.trim();
  }

  const SF_API_BASE = typeof window !== 'undefined' && window.SF_API_BASE ? window.SF_API_BASE : '';
  const SMART_BRAIN_MEDIA_BASE = 'https://smart-brain.foofiebean.workers.dev';
  const SYLVANFLOW_LOGO_ASSET = '/assets/sylvanflow-logo.png';

  function normalizeRecoImageUrl(rawUrl) {
    const s = asText(rawUrl);
    if (!s) return '';
    // Catalog can provide photo tokens like "places/<place_id>/photos/<photo_ref>".
    // Convert these into proxy URLs that browser can load directly.
    if (s.startsWith('places/')) {
      return `${SMART_BRAIN_MEDIA_BASE}/place-photo?photo_name=${encodeURIComponent(s)}&maxwidth=800`;
    }
    return s;
  }

  function resolveRecoMediaImageUrl(poi) {
    const coverUrl = asText(poi?.media?.cover_image_url);
    if (coverUrl) return coverUrl;
    const coverKey = asText(poi?.media?.cover_image_r2_key || poi?.media?.cover_r2_key);
    if (coverKey) return `${SMART_BRAIN_MEDIA_BASE}/poi-image?key=${encodeURIComponent(coverKey)}`;
    const mediaImages = Array.isArray(poi?.media?.images) ? poi.media.images : [];
    for (const img of mediaImages) {
      if (typeof img === 'string' && img) return normalizeRecoImageUrl(img);
      const u = asText(img?.url || img?.src);
      if (u) return normalizeRecoImageUrl(u);
      const k = asText(img?.r2_key || img?.key);
      if (k) return `${SMART_BRAIN_MEDIA_BASE}/poi-image?key=${encodeURIComponent(k)}`;
    }
    return '';
  }

  /**
   * 5-day flow plan (catalog POIs) repeated across a fixed test window — from POI catalog
   * `data/poi-catalogs/global/r2_global_1.4_103.8_ac.json`. Client-only PAST corridor (not ITINERARY_KV).
   */
  const NUASK_FIVE_DAY_FLOW_PLAN_POIS = [
    {
      name: 'Gardens by the Bay',
      place_id: 'ChIJMxZ-kwQZ2jERdsqftXeWCWI',
      address: '18 Marina Gardens Dr, Singapore 018953',
      lat: 1.2815683,
      lng: 103.8636132
    },
    {
      name: 'Marina Bay Sands Singapore',
      place_id: 'ChIJA5LATO4Z2jER111V-v6abAI',
      address: '10 Bayfront Ave, Singapore 018956',
      lat: 1.2837574999999999,
      lng: 103.8591065
    },
    {
      name: 'Singapore Botanic Gardens',
      place_id: 'ChIJvWDbfRwa2jERgNnTOpAU3-o',
      address: '1 Cluny Rd, Singapore 259569',
      lat: 1.3138397,
      lng: 103.8159136
    },
    {
      name: 'National Museum of Singapore',
      place_id: 'ChIJD1u-EaMZ2jERaLhNfFkR45I',
      address: '93 Stamford Rd, Singapore 178897',
      lat: 1.296613,
      lng: 103.84850910000002
    },
    {
      name: 'Merlion Park',
      place_id: 'ChIJBTYg1g4Z2jERp_MBbu5erWY',
      address: '1 Fullerton Rd, Singapore 049213',
      lat: 1.2867449,
      lng: 103.85438719999999
    }
  ];

  const NUASK_SYNTHETIC_REGION = 'global_1.4_103.8';

  /**
   * Apr 30 → May 20 (inclusive), current calendar year: one activity per day, 8 AM–11 PM,
   * POI = NUASK_FIVE_DAY_FLOW_PLAN_POIS[dayOffset % 5] (5-day pattern repeats).
   */
  function buildSyntheticApr30ToMay20PastItinerary() {
    const items_by_day = {};
    const year = new Date().getFullYear();
    const cursor = new Date(year, 3, 30);
    cursor.setHours(0, 0, 0, 0);
    const end = new Date(year, 4, 20);
    end.setHours(0, 0, 0, 0);
    let cycleIndex = 0;
    for (; cursor.getTime() <= end.getTime(); cursor.setDate(cursor.getDate() + 1)) {
      const y = cursor.getFullYear();
      const mo = String(cursor.getMonth() + 1).padStart(2, '0');
      const da = String(cursor.getDate()).padStart(2, '0');
      const dayKey = `${y}-${mo}-${da}`;
      const p = NUASK_FIVE_DAY_FLOW_PLAN_POIS[cycleIndex % NUASK_FIVE_DAY_FLOW_PLAN_POIS.length];
      cycleIndex += 1;
      const label = cursor.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
      items_by_day[dayKey] = [
        {
          title: p.name,
          Title: p.name,
          start_time: '8:00 AM',
          start_time_local: `${label} · 8:00 AM`,
          end_time: '11:00 PM',
          end_time_local: '11:00 PM',
          location: p.address,
          Location: p.address,
          place_id: p.place_id,
          lat: p.lat,
          lng: p.lng,
          activity_type: '8 AM – 11 PM',
          catalog_region_key: NUASK_SYNTHETIC_REGION,
          description: `NuAsk PAST synthetic · 5-day loop day ${((cycleIndex - 1) % 5) + 1}/5 · not persisted.`
        }
      ];
    }
    return { items_by_day, tz: 'Asia/Singapore' };
  }

  /** Flatten itinerary activities in day order (sorted day keys). */
  function flattenItineraryActivities(itinerary) {
    const itemsByDay = itinerary?.items_by_day;
    if (!itemsByDay || typeof itemsByDay !== 'object') return [];
    const entries = Object.entries(itemsByDay).sort(([a], [b]) => String(a).localeCompare(String(b)));
    const out = [];
    for (const [dayKey, activities] of entries) {
      if (Array.isArray(activities)) {
        for (let i = 0; i < activities.length; i++) {
          const act = activities[i];
          if (!act || typeof act !== 'object') continue;
          if (act._hidden === true || act.hidden === true) continue;
          out.push({ ...act, __dayKey: dayKey, __activityIndex: i });
        }
      }
    }
    return out;
  }

  function formatActivityTimeLabel(activity) {
    const raw = activity.start_time_local || activity.start_time || activity.Start_Time || '';
    if (raw == null || raw === '') return '—';
    const s = String(raw).trim();
    const mins = parseTimeToMinutes(s);
    if (mins != null) {
      const d = new Date();
      d.setHours(Math.floor(mins / 60), mins % 60, 0, 0);
      return formatTimeLabel(d);
    }
    try {
      const d = new Date(s);
      if (!Number.isNaN(d.getTime())) {
        return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
      }
    } catch (e) {
      /* ignore */
    }
    return s.length > 12 ? `${s.slice(0, 12)}…` : s;
  }

  function getActivityLocationLabel(activity) {
    const loc = activity.location || activity.Location || '';
    if (loc && typeof loc === 'object') {
      const fromObj = loc.address || loc.name || loc.Location || '';
      return String(fromObj || '').trim();
    }
    return String(loc || '').trim();
  }

  function asText(value) {
    if (value == null) return '';
    if (typeof value === 'string') return value.trim();
    if (typeof value === 'number' || typeof value === 'boolean') return String(value);
    return '';
  }

  /** Issue #124.8 — map micro-action select `patch_code` / `code` to NuAsk i18n (amber strip), not raw API English. */
  function resolveNuAskMicroActionPatchError(data, strings) {
    if (!data || typeof data !== 'object') return null;
    const failed = data.ok === false || data.patch_status === 'failed';
    if (!failed) return null;
    const code = String(data.patch_code || data.code || '').trim();
    if (code === 'ERR_HARD_BOOKING_PROTECTED') {
      return strings.patchHardBookingProtected || strings.noFeasibleWindow || null;
    }
    if (code === 'ERR_COMMIT_REVALIDATION') {
      return strings.patchCommitRevalidation || strings.noFeasibleWindow || null;
    }
    if (code === 'ERR_PATCH_OVERLAP') {
      return strings.patchOverlap || strings.noFeasibleWindow || null;
    }
    if (code === 'ERR_COMMIT_CASCADE_LATE') {
      return strings.patchCommitCascadeLate || strings.noFeasibleWindow || null;
    }
    if (code === 'ERR_COMMIT_OPENING_HOURS') {
      return strings.patchCommitOpeningHours || strings.noFeasibleWindow || null;
    }
    if (code === 'ERR_DBL_POLICY_BLOCK') {
      return strings.patchGenericFailed || strings.noFeasibleWindow || null;
    }
    if (code) {
      return strings.patchGenericFailed || data.patch_error || data.message || data.error || null;
    }
    return strings.patchGenericFailed || data.patch_error || data.message || data.error || null;
  }

  /** Issue #124.9 — mirror `activityHasHardCommitment` for corridor UI (no worker import in Pages). */
  function activityLooksHardCommitted(activity) {
    if (!activity || typeof activity !== 'object') return false;
    if (activity.hard_booking === true) return true;
    if (activity.locked === true || activity.is_fixed === true) return true;
    const ref = activity.booking_ref;
    return ref != null && String(ref).trim().length > 0;
  }

  /** One ASCII letter for static map tile markers — raw emoji first-char can render as "?" on map previews. */
  function mapLabelLetter(title) {
    const s = asText(title);
    const m = s.match(/[A-Za-z0-9]/);
    return m ? m[0].toUpperCase() : 'N';
  }

  function renderCorridorIcon(iconValue, isPlaceholder) {
    if (isPlaceholder) {
      return (
        <img
          src={SYLVANFLOW_LOGO_ASSET}
          alt="SylvanFlow"
          className="h-6 w-6 object-contain"
          style={{ opacity: 0.58, filter: 'saturate(0.7) brightness(0.88)' }}
        />
      );
    }
    return <>{iconValue}</>;
  }

  function extractActivityCoords(activity) {
    if (!activity || typeof activity !== 'object') return null;
    const normalize = (latRaw, lngRaw) => {
      const a = Number(latRaw);
      const b = Number(lngRaw);
      if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
      if (Math.abs(a) <= 90 && Math.abs(b) <= 180) return { lat: a, lng: b };
      // Defensive swap for legacy/shapeshifted rows with reversed fields.
      if (Math.abs(b) <= 90 && Math.abs(a) <= 180) return { lat: b, lng: a };
      return null;
    };
    const pick = (obj) => {
      if (!obj || typeof obj !== 'object') return null;
      return (
        normalize(obj.lat ?? obj.latitude ?? obj.Lat, obj.lng ?? obj.longitude ?? obj.Lng) ||
        normalize(obj.coord_lat ?? obj.coordLat, obj.coord_lng ?? obj.coordLng)
      );
    };

    const locObj =
      activity.location && typeof activity.location === 'object' && !Array.isArray(activity.location)
        ? activity.location
        : activity.Location && typeof activity.Location === 'object' && !Array.isArray(activity.Location)
          ? activity.Location
          : null;
    const extrasObj =
      activity.extras && typeof activity.extras === 'object' && !Array.isArray(activity.extras)
        ? activity.extras
        : null;

    const candidates = [
      activity,
      locObj,
      activity.coords,
      activity.coordinates,
      extrasObj?.coord,
      locObj?.coord
    ];
    for (const c of candidates) {
      const v = pick(c);
      if (v) return v;
    }
    return null;
  }

  /**
   * Maps region (Smart Brain `detectRegion` parity): CN → Gaode, else Google.
   * Uses traveler/app region — not destination-bbox alone. Taiwan carved to GLOBAL.
   */
  function detectMapsRegion(coords) {
    if (!coords) return 'GLOBAL';
    const lat = Number(coords.lat);
    const lng = Number(coords.lng);
    if (!Number.isFinite(lat) || !Number.isFinite(lng)) return 'GLOBAL';
    const isTaiwan = lat >= 21.7 && lat <= 25.6 && lng >= 119.3 && lng <= 122.1;
    if (isTaiwan) return 'GLOBAL';
    const isChinaMainland = lat >= 18.0 && lat <= 54.0 && lng >= 73.0 && lng <= 135.0;
    return isChinaMainland ? 'CN' : 'GLOBAL';
  }

  function normalizeMapsRegion(region) {
    const s = String(region || '').trim().toUpperCase();
    return s === 'CN' ? 'CN' : 'GLOBAL';
  }

  /** Prefer API/preview region (where the user is); else detect from live user coords. */
  function resolveMapsRegion(storedRegion, userCoords) {
    const s = String(storedRegion || '').trim().toUpperCase();
    if (s === 'CN' || s === 'GLOBAL') return s;
    return detectMapsRegion(userCoords);
  }

  /** External maps deep link only (no in-app turn-by-turn). CN → Gaode; GLOBAL → Google. */
  function buildExternalDirectionsUrl(dest, origin, region) {
    if (!dest) return '';
    const dLat = Number(dest.lat);
    const dLng = Number(dest.lng);
    if (!Number.isFinite(dLat) || !Number.isFinite(dLng)) return '';
    const oLat = origin != null ? Number(origin.lat) : NaN;
    const oLng = origin != null ? Number(origin.lng) : NaN;
    const hasOrigin = Number.isFinite(oLat) && Number.isFinite(oLng);
    const mapsRegion = normalizeMapsRegion(region);
    if (mapsRegion === 'CN') {
      if (hasOrigin) {
        return `https://uri.amap.com/navigation?from=${encodeURIComponent(`${oLng},${oLat},Start`)}&to=${encodeURIComponent(`${dLng},${dLat},End`)}&mode=car&policy=1&src=sylvanflow&coordinate=gaode&callnative=1`;
      }
      return `https://uri.amap.com/marker?position=${encodeURIComponent(`${dLng},${dLat}`)}&name=${encodeURIComponent(asText(dest.name || dest.title) || 'Map point')}&src=sylvanflow&coordinate=gaode&callnative=1`;
    }
    const q = new URLSearchParams({ api: '1', destination: `${dLat},${dLng}` });
    if (hasOrigin) q.set('origin', `${oLat},${oLng}`);
    return `https://www.google.com/maps/dir/?${q.toString()}`;
  }

  /** Place pin / search link (Nearby · Use My Time). Same CN/GLOBAL rule as directions. */
  function buildExternalPlaceMapUrl(lat, lng, name, region) {
    const dLat = Number(lat);
    const dLng = Number(lng);
    if (!Number.isFinite(dLat) || !Number.isFinite(dLng)) return '';
    if (normalizeMapsRegion(region) === 'CN') {
      return `https://uri.amap.com/marker?position=${encodeURIComponent(`${dLng},${dLat}`)}&name=${encodeURIComponent(asText(name) || 'Map point')}&src=sylvanflow&coordinate=gaode&callnative=1`;
    }
    return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${dLat},${dLng}`)}`;
  }

  function pickRecoPoiCoords(poi) {
    if (!poi || typeof poi !== 'object') return null;
    const lat = Number(poi.lat ?? poi.latitude ?? poi.Lat);
    const lng = Number(poi.lng ?? poi.longitude ?? poi.Lng);
    if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
    return { lat, lng };
  }

  function pickActivityIcon(activity, fallbackIndex) {
    const text = [
      asText(activity.title),
      asText(activity.Title),
      asText(activity.activity_type),
      asText(activity.Activity_Type),
      asText(activity.category),
      asText(activity.description),
      asText(activity.Description),
      getActivityLocationLabel(activity)
    ]
      .join(' ')
      .toLowerCase();

    if (/airport|changi|flight|arriv|depart|terminal|航班|机场|抵达/.test(text)) return '✈️';
    if (/hotel|check.?in|stay|住宿|酒店/.test(text)) return '🏨';
    if (/dinner|lunch|breakfast|restaurant|food|cafe|bar|hawker|晚餐|午餐|早餐|餐|吃/.test(text)) return '🍽️';
    if (/museum|gallery|artscience|历史|博物馆|展览/.test(text)) return '🏛️';
    if (/garden|park|bay|botanic|nature|海湾|花园|公园/.test(text)) return '🌿';
    if (/temple|mosque|church|cathedral|monastery|庙|清真寺|教堂/.test(text)) return '🛕';
    if (/walk|stroll|hike|trail|漫步|步行|徒步/.test(text)) return '🚶';
    if (/shopping|mall|market|shop|购物|商场/.test(text)) return '🛍️';
    if (/zoo|safari|aquarium|ocean|wild|动物园|夜间野生动物/.test(text)) return '🦁';

    const icons = ['📍', '🚶', '🌿'];
    return icons[fallbackIndex % icons.length];
  }

  function activityToPastStop(activity, index) {
    const title = asText(activity.title) || asText(activity.Title) || 'Activity';
    const location = getActivityLocationLabel(activity);
    const sub =
      asText(activity.activity_type) ||
      asText(activity.Activity_Type) ||
      asText(activity.category) ||
      (location ? String(location).split(',')[0] : '') ||
      'Stop';
    return {
      time: formatActivityTimeLabel(activity),
      title,
      sub: sub.length > 16 ? `${sub.slice(0, 14)}…` : sub,
      icon: pickActivityIcon(activity, index)
    };
  }

  function buildPastStopsRow(activities, placeholder) {
    const ghost = placeholder || { time: '—', title: '—', sub: '—', icon: '◌', ghost: true };
    const fromApi = activities.slice(0, 3).map((a, i) => activityToPastStop(a, i));
    const out = [ghost, ghost, ghost];
    const start = Math.max(0, 3 - fromApi.length);
    for (let i = 0; i < fromApi.length; i++) out[start + i] = fromApi[i];
    return out;
  }

  function buildPastCardTargets(activities, tripKey) {
    const realTargets = activities.slice(0, 3).map((a) => {
      if (!a || !tripKey || !a.__dayKey || a.__activityIndex == null) return null;
      return {
        tripKey,
        dayKey: a.__dayKey,
        activityIndex: a.__activityIndex,
        activity: a
      };
    });
    const out = [null, null, null];
    const start = Math.max(0, 3 - realTargets.length);
    for (let i = 0; i < realTargets.length; i++) out[start + i] = realTargets[i];
    return out;
  }

  function getActivityImageUrl(activity) {
    return (
      asText(activity.image_url) ||
      asText(activity.Image_URL) ||
      asText(activity.photo_url) ||
      asText(activity.Photo_URL) ||
      ''
    );
  }

  function getActivityWriteUp(activity) {
    if (!activity || typeof activity !== 'object') return '';
    return (
      asText(activity.rich_description) ||
      asText(activity.Rich_Description) ||
      asText(activity.description) ||
      asText(activity.Description) ||
      ''
    );
  }

  /** Idle NOW occupancy card — proven CURRENT only; never invents title/body. */
  function buildNowCurrentStopCard(current, tripKey) {
    if (!current || typeof current !== 'object') return null;
    const title = asText(current.title) || asText(current.Title) || '';
    if (!title) return null;
    const detailTarget =
      tripKey && current.__dayKey != null && current.__activityIndex != null
        ? {
            tripKey,
            dayKey: current.__dayKey,
            activityIndex: current.__activityIndex,
            activity: current
          }
        : null;
    return {
      title,
      time: formatActivityTimeLabel(current),
      sub:
        asText(current.activity_type) ||
        asText(current.Activity_Type) ||
        (getActivityLocationLabel(current).split(',')[0] || ''),
      body: getActivityWriteUp(current),
      imageUrl: getActivityImageUrl(current),
      icon: pickActivityIcon(current, 1),
      coords: extractActivityCoords(current),
      detailTarget
    };
  }

  function buildPastJourneyItems(activities, placeholder) {
    const real = activities.slice(0, 3).map((a, idx) => ({
      time: formatActivityTimeLabel(a),
      title: asText(a.title) || asText(a.Title) || `Stop ${idx + 1}`,
      subtitle: getActivityLocationLabel(a) || asText(a.activity_type) || 'Journey',
      imageUrl: getActivityImageUrl(a),
      icon: pickActivityIcon(a, idx)
    }));
    const ghost =
      placeholder || { time: '—', title: '—', subtitle: '—', imageUrl: '', icon: '◌', ghost: true };
    const out = [ghost, ghost, ghost];
    const start = Math.max(0, 3 - real.length);
    for (let i = 0; i < real.length; i++) out[start + i] = real[i];
    return out;
  }

  /** First paint before corridor fetch — same shape as `buildPastStopsRow([], …)` ghosts (no demo POI flash). */
  const NUASK_INITIAL_PAST_STOP_GHOST = { time: '—', title: '—', sub: '—', icon: '◌', ghost: true };
  const NUASK_INITIAL_PAST_STOPS_ROW = [
    NUASK_INITIAL_PAST_STOP_GHOST,
    NUASK_INITIAL_PAST_STOP_GHOST,
    NUASK_INITIAL_PAST_STOP_GHOST
  ];
  const NUASK_INITIAL_PAST_JOURNEY_GHOST = {
    time: '—',
    title: '—',
    subtitle: '—',
    imageUrl: '',
    icon: '◌',
    ghost: true
  };
  const NUASK_INITIAL_PAST_JOURNEY_ITEMS = [
    NUASK_INITIAL_PAST_JOURNEY_GHOST,
    NUASK_INITIAL_PAST_JOURNEY_GHOST,
    NUASK_INITIAL_PAST_JOURNEY_GHOST
  ];

  function parseTimeToMinutes(raw) {
    const s = asText(raw).toUpperCase();
    if (!s) return null;
    const m12 = s.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/);
    if (m12) {
      let h = Number(m12[1]) % 12;
      const mins = Number(m12[2]);
      if (m12[3] === 'PM') h += 12;
      return h * 60 + mins;
    }
    const m24 = s.match(/^(\d{1,2}):(\d{2})$/);
    if (m24) {
      return Number(m24[1]) * 60 + Number(m24[2]);
    }
    return null;
  }

  function getActivityDateTime(activity) {
    const Temporal = typeof window !== 'undefined' ? window.NuAskTemporalRoles : null;
    if (Temporal && typeof Temporal.activityStartDateTime === 'function') {
      return Temporal.activityStartDateTime(activity);
    }
    const dayKey = asText(activity.__dayKey);
    if (!dayKey) return null;
    const mins = parseTimeToMinutes(activity.start_time_local || activity.start_time || activity.Start_Time);
    if (mins == null) return null;
    const [y, m, d] = dayKey.split('-').map((v) => Number(v));
    if (!y || !m || !d) return null;
    const dt = new Date(y, m - 1, d, 0, 0, 0, 0);
    dt.setMinutes(mins);
    return dt;
  }

  function formatTimeLabel(date) {
    return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
  }

  function formatDistanceLabel(km) {
    const n = Number(km);
    if (!Number.isFinite(n)) return '';
    if (n < 1) return `${Math.round(n * 1000)} m`;
    return `${n.toFixed(1)} km`;
  }

  /** Great-circle distance for transparent “leg” estimates (not routing). */
  function haversineKm(lat1, lng1, lat2, lng2) {
    const a1 = Number(lat1);
    const o1 = Number(lng1);
    const a2 = Number(lat2);
    const o2 = Number(lng2);
    if (![a1, o1, a2, o2].every((x) => Number.isFinite(x))) return null;
    const R = 6371;
    const toRad = (d) => (d * Math.PI) / 180;
    const dLat = toRad(a2 - a1);
    const dLng = toRad(o2 - o1);
    const h =
      Math.sin(dLat / 2) ** 2 +
      Math.cos(toRad(a1)) * Math.cos(toRad(a2)) * Math.sin(dLng / 2) ** 2;
    return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
  }

  /** Rough drive-time hint from straight-line km (catalog uses straight-line from device location). */
  function roughUrbanDriveMinutes(km, urbanKmh) {
    const k = Number(km);
    const v = Number(urbanKmh) > 0 ? Number(urbanKmh) : 22;
    if (!Number.isFinite(k) || k < 0) return null;
    return Math.max(1, Math.round((k / v) * 60));
  }

  function joinTagList(tags, maxItems) {
    const arr = Array.isArray(tags) ? tags.filter(Boolean).map((t) => String(t).trim()).filter(Boolean) : [];
    const cap = typeof maxItems === 'number' ? maxItems : 8;
    if (arr.length === 0) return '';
    const shown = arr.slice(0, cap);
    const extra = arr.length > cap ? ` (+${arr.length - cap} more)` : '';
    return `${shown.join(', ')}${extra}`;
  }

  function formatDurationMins(minsInput) {
    const mins = Math.max(0, Math.round(Number(minsInput) || 0));
    const h = Math.floor(mins / 60);
    const m = mins % 60;
    if (h <= 0) return `${m}m`;
    if (m <= 0) return `${h}h`;
    return `${h}h ${m}m`;
  }

  /** Walking pace for short legs — “Use My Time” gap estimates (tourist on foot). */
  function roughWalkMinutes(km) {
    const k = Number(km);
    const walkKmh = 5;
    if (!Number.isFinite(k) || k < 0) return null;
    return Math.max(1, Math.round((k / walkKmh) * 60));
  }

  /**
   * Travel minutes for gap-fit: walk if ≤2 km straight-line, else urban straight-line pace (matches other NOW copy).
   */
  function roughTravelMinutesForGap(km) {
    const k = Number(km);
    if (!Number.isFinite(k) || k < 0) return null;
    if (k <= 2) return roughWalkMinutes(k);
    return roughUrbanDriveMinutes(k);
  }

  /** Heuristic dwell for “does this POI fit the gap?” — catalog category string only (no new backend). */
  function estimateDwellMinutesForGap(primaryCategory) {
    const c = String(primaryCategory || '').toLowerCase();
    if (/meal|restaurant|food|cafe|coffee|dining|bar|bakery|bistro/.test(c)) return 45;
    if (/museum|gallery|art/.test(c)) return 75;
    if (/park|garden|nature|trail|beach|zoo|wildlife/.test(c)) return 40;
    if (/shop|mall|market|retail|store/.test(c)) return 35;
    if (/temple|church|mosque|historic|monument|landmark|attraction|walking/.test(c)) return 35;
    return 35;
  }

  const GAP_BUFFER_MIN = 5;

  /** Labels POIs for Use My Time: comfortable / tight / skip vs remaining window. */
  function buildGapFitRows(pois, windowMins) {
    const W = Math.max(0, Math.round(Number(windowMins) || 0));
    const list = Array.isArray(pois) ? pois : [];
    const rows = list.map((poi) => {
      const dm = typeof poi.distance_m === 'number' ? poi.distance_m : null;
      const km = dm != null ? dm / 1000 : null;
      const serverTravel =
        typeof poi.travel_minutes_estimate === 'number' && Number.isFinite(poi.travel_minutes_estimate)
          ? Math.max(1, Math.round(poi.travel_minutes_estimate))
          : null;
      const travelMin = serverTravel != null ? serverTravel : km != null ? roughTravelMinutesForGap(km) : null;
      const dwellMin = estimateDwellMinutesForGap(poi.primary_category);
      const totalMin =
        travelMin != null ? travelMin + dwellMin + GAP_BUFFER_MIN : null;
      let fit = 'unknown';
      if (totalMin != null && W > 0) {
        if (totalMin <= W * 0.55) fit = 'comfortable';
        else if (totalMin <= W) fit = 'tight';
        else fit = 'skip';
      } else if (W <= 0) fit = 'skip';
      return { poi, km, travelMin, dwellMin, totalMin, fit };
    });
    const rank = { comfortable: 0, tight: 1, skip: 2, unknown: 3 };
    rows.sort((a, b) => {
      const da = a.totalMin != null ? a.totalMin : 9999;
      const db = b.totalMin != null ? b.totalMin : 9999;
      if (rank[a.fit] !== rank[b.fit]) return rank[a.fit] - rank[b.fit];
      return da - db;
    });
    return rows;
  }

  function formatClientDayKeyLocal(date) {
    const y = date.getFullYear();
    const m = String(date.getMonth() + 1).padStart(2, '0');
    const d = String(date.getDate()).padStart(2, '0');
    return `${y}-${m}-${d}`;
  }

  function offsetDayKeyLocal(baseDate, deltaDays) {
    const d = new Date(baseDate);
    d.setHours(12, 0, 0, 0);
    d.setDate(d.getDate() + Number(deltaDays || 0));
    return formatClientDayKeyLocal(d);
  }

  function formatClientLocalHHMM(date) {
    const h = String(date.getHours()).padStart(2, '0');
    const m = String(date.getMinutes()).padStart(2, '0');
    return `${h}:${m}`;
  }

  function isPositionalActivityRef(ref) {
    return /^\d{4}-\d{2}-\d{2}:\d+$/.test(String(ref || '').trim());
  }

  function resolveOccurrenceIdentity(activity) {
    const key = typeof activity?.activity_key === 'string' && activity.activity_key.trim();
    if (key) return { type: 'activity_key', value: key.trim() };
    const poi = typeof activity?.poi_id === 'string' && activity.poi_id.trim();
    if (poi) return { type: 'poi_id', value: poi.trim() };
    const place = typeof activity?.place_id === 'string' && activity.place_id.trim();
    if (place) return { type: 'place_id', value: place.trim() };
    const gaode = typeof activity?.gaode_id === 'string' && activity.gaode_id.trim();
    if (gaode) return { type: 'gaode_id', value: gaode.trim() };
    return { type: 'none', value: null };
  }

  function matchNextSelectedPreparation(preparation, selectedActivity, peerActivities) {
    const ident = resolveOccurrenceIdentity(selectedActivity);
    if (!preparation || typeof preparation !== 'object') {
      return { outcome: 'unavailable', identityType: ident.type, matchCount: 0, texts: [] };
    }
    if (preparation.nothing_material === true) {
      return { outcome: 'nothing_material', identityType: ident.type, matchCount: 0, texts: [] };
    }
    if (ident.type === 'none' || !ident.value) {
      return { outcome: 'no_identity', identityType: 'none', matchCount: 0, texts: [] };
    }
    if (ident.type !== 'activity_key' && Array.isArray(peerActivities)) {
      let dup = 0;
      for (let i = 0; i < peerActivities.length; i++) {
        const raw = peerActivities[i]?.[ident.type];
        if (typeof raw === 'string' && raw.trim() === ident.value) dup += 1;
      }
      if (dup > 1) {
        return { outcome: 'duplicate', identityType: ident.type, matchCount: 0, texts: [] };
      }
    }
    const rows = [
      ...(Array.isArray(preparation.activity_preparation) ? preparation.activity_preparation : []),
      ...(Array.isArray(preparation.later_preparation) ? preparation.later_preparation : [])
    ];
    const matches = [];
    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      if (!row || typeof row !== 'object') continue;
      const ref = typeof row.activity_ref === 'string' ? row.activity_ref.trim() : '';
      if (!ref || isPositionalActivityRef(ref)) continue;
      if (ref !== ident.value) continue;
      const text = typeof row.text === 'string' ? row.text.trim() : '';
      if (text) matches.push(text);
    }
    if (!matches.length) {
      return { outcome: 'no_match', identityType: ident.type, matchCount: 0, texts: [] };
    }
    return { outcome: 'matched', identityType: ident.type, matchCount: matches.length, texts: matches };
  }

  function logContextPrepBind({ rid, dayKey, slot, bind }) {
    const requestRid = rid || 'none';
    const day = dayKey || 'none';
    const selectedSlot = slot || 'none';
    const identity = bind?.identityType || 'none';
    const matchN = Number(bind?.matchCount) || 0;
    const outcome = bind?.outcome || 'unavailable';
    console.log(
      `[CONTEXT_PREP_BIND] rid=${requestRid} day=${day} slot=${selectedSlot} identity=${identity} match_n=${matchN} outcome=${outcome}`
    );
  }

  /**
   * Structured packet current is material when the provider condition is wet/severe
   * or POP is high. Used only to order existing preparation text — no new copy.
   */
  function isMaterialCurrentWeather(current) {
    if (!current || typeof current !== 'object') return false;
    const condition = String(current.condition || '').toLowerCase();
    if (/rain|storm|snow|thunder|drizzle|sleet|shower/.test(condition)) return true;
    const pop = Number(current.pop);
    return Number.isFinite(pop) && pop >= 50;
  }


  const NUASK_WEATHER_CONDITION_FALLBACK = {
    rainy: 'rainy',
    sunny: 'sunny',
    clear: 'clear',
    partly_cloudy: 'partly cloudy',
    cloudy: 'cloudy',
    snowy: 'snowy',
    foggy: 'foggy'
  };

  function normalizeWeatherConditionSlug(condition) {
    const c = String(condition || '')
      .trim()
      .toLowerCase()
      .replace(/[_-]+/g, ' ')
      .replace(/\s+/g, ' ');
    if (!c) return '';
    if (/fog|mist|haze|霾|雾/.test(c)) return 'foggy';
    if (/snow|sleet|雪/.test(c)) return 'snowy';
    if (/rain|drizzle|shower|storm|thunder|雨/.test(c)) return 'rainy';
    if (/partly|broken cloud|多云/.test(c)) return 'partly_cloudy';
    if (/cloud|阴|曇|云/.test(c)) return 'cloudy';
    if (/sun|晴(?!朗)/.test(c) || c === 'sunny') return 'sunny';
    if (/clear|晴朗|快晴/.test(c)) return 'clear';
    if (NUASK_WEATHER_CONDITION_FALLBACK[c.replace(/ /g, '_')]) return c.replace(/ /g, '_');
    return '';
  }

  function localizeWeatherCondition(condition, strings) {
    const raw = String(condition || '').trim();
    const slug = normalizeWeatherConditionSlug(raw);
    const fromStrings = {
      rainy: strings && strings.weatherConditionRainy,
      sunny: strings && strings.weatherConditionSunny,
      clear: strings && strings.weatherConditionClear,
      partly_cloudy: strings && strings.weatherConditionPartlyCloudy,
      cloudy: strings && strings.weatherConditionCloudy,
      snowy: strings && strings.weatherConditionSnowy,
      foggy: strings && strings.weatherConditionFoggy
    };
    const labeled = slug ? String(fromStrings[slug] || '').trim() : '';
    if (labeled) return labeled;
    if (slug && NUASK_WEATHER_CONDITION_FALLBACK[slug]) return NUASK_WEATHER_CONDITION_FALLBACK[slug];
    return raw;
  }

  function weatherConditionIsWet(condition) {
    return /rain|storm|snow|thunder|drizzle|sleet|shower|雨|雪/i.test(String(condition || ''));
  }

  function weatherConditionIsClearDry(condition) {
    const c = String(condition || '').trim().toLowerCase();
    return /^(clear|sunny)$/.test(c) || /晴/.test(String(condition || ''));
  }

  function prepLineContradictsCurrentWeather(text, current) {
    const raw = String(text || '').trim();
    if (!raw || !current || typeof current !== 'object') return false;
    const cond = String(current.condition || '');
    const later =
      /if you(?:'re| are) heading out later|showers may develop|later today|如果稍后|稍后若|今晚若/i.test(
        raw
      );
    if (weatherConditionIsWet(cond)) {
      if (later) return false;
      return /无需携带雨具|不必带伞|不用带伞|无需雨具|无需雨衣|不需要带伞|no(?:t)? (?:need(?:ed)?(?: to)?|need to) (?:bring|carry|take).{0,32}(?:umbrella|rain\s*coat|rain\s*gear)|no umbrella needed|晴朗|天气晴(?!雨)/i.test(
        raw
      );
    }
    if (weatherConditionIsClearDry(cond)) {
      if (later) return false;
      return /记得带伞|请带雨具|带上雨衣|bring (?:an )?umbrella|carry (?:a )?rain(?:coat| gear)/i.test(
        raw
      );
    }
    return false;
  }

  function filterNowCiLinesAgainstWeather(lines, current, weatherLine) {
    const wx = String(weatherLine || '').trim();
    const src = Array.isArray(lines) ? lines : [];
    const out = [];
    for (let i = 0; i < src.length; i++) {
      const text = String(src[i] || '').trim();
      if (!text) continue;
      if (wx && text === wx) {
        out.push(text);
        continue;
      }
      if (prepLineContradictsCurrentWeather(text, current)) continue;
      out.push(text);
    }
    return out;
  }

  /**
   * #178-E — compact factual line from trusted weather.current only.
   * temp_c when finite; condition when non-empty. No wind, POP, forecast, or LLM phrasing.
   * Condition words use SYS 10-lang labels when `strings` is passed.
   */
  function formatTrustedCurrentWeatherLine(current, strings) {
    if (!current || typeof current !== 'object') return '';
    const rawTemp = current.temp_c;
    const tempPresent =
      rawTemp !== null &&
      rawTemp !== undefined &&
      rawTemp !== '';
    const parsedTemp = tempPresent ? Number(rawTemp) : NaN;
    const tempOk = tempPresent && Number.isFinite(parsedTemp);
    const condition = localizeWeatherCondition(String(current.condition || '').trim(), strings);
    if (tempOk && condition) return `${parsedTemp}°C · ${condition}`;
    if (tempOk) return `${parsedTemp}°C`;
    if (condition) return condition;
    return '';
  }

  function ciEvaluateHasTrustedCurrentWeather(data) {
    return Boolean(formatTrustedCurrentWeatherLine(data && data.weather && data.weather.current));
  }

  /**
   * #178-E CI latch role from existing NuAsk CURRENT/FUTURE classification.
   * TODAY_REMAINING = CURRENT exists OR FUTURE today is non-empty.
   * TODAY_EXHAUSTED = CURRENT is null AND FUTURE today is empty.
   * Does not parse activity times; uses helper output + existing __dayKey.
   */
  function nowCiTodayTemporalRole(classified, clientDayKey) {
    const current = classified && classified.current;
    const future = classified && Array.isArray(classified.future) ? classified.future : [];
    const day = String(clientDayKey || '').trim();
    let futureToday = 0;
    for (let i = 0; i < future.length; i++) {
      if (String((future[i] && future[i].__dayKey) || '').trim() === day) futureToday += 1;
    }
    if (current || futureToday > 0) return 'today_remaining';
    return 'today_exhausted';
  }

  function nowCiEvaluateLatchKey(clientDayKey, role) {
    return `${clientDayKey}:${role}`;
  }

  /**
   * Slice 3 GPS culture gate. Durable/culture on NOW LEFT requires device
   * coords. Fail closed. No destination-string city. No invented coordinate
   * dictionary. Empty seed still means silence even when GPS is present.
   */
  function nowCiHasDeviceGps(deviceCoords) {
    return !!(
      deviceCoords &&
      Number.isFinite(Number(deviceCoords.lat)) &&
      Number.isFinite(Number(deviceCoords.lng))
    );
  }

  /**
   * NOW TV lines: trusted weather.current first (if present), then successful
   * material preparation bound to occupancy CURRENT (not remaining[0] /
   * next_activity_ref), then Phase 5 durable_context only when device GPS
   * exists. later_preparation is included only when it matches CURRENT —
   * that is NEXT→NOW carry-forward. nothing_material / failed / null prep
   * suppress live-prep enrichment only — durable rows may still fill when
   * GPS is present. Max 3. Never reconstructs `${day}:index`.
   */
  function deriveNowCiVisibleLines(evaluatePacket, occupancyActivity, occupancyPeers, deviceCoords, strings) {
    const result = evaluatePacket && evaluatePacket.result;
    const currentWx = result && result.weather && result.weather.current;
    const weatherLine = formatTrustedCurrentWeatherLine(currentWx, strings);

    const lines = [];
    const seen = new Set();
    const pushText = (raw, isWeather) => {
      const text = String(raw || '').trim();
      if (!text || seen.has(text) || lines.length >= 3) return;
      if (!isWeather && prepLineContradictsCurrentWeather(text, currentWx)) return;
      seen.add(text);
      lines.push(text);
    };

    if (weatherLine) pushText(weatherLine, true);

    const ok = result && result.ok === true;
    const prep = ok ? result.preparation : null;
    const livePrepUsable = prep && typeof prep === 'object' && prep.nothing_material !== true;
    if (livePrepUsable) {
      const dayPrep = Array.isArray(prep.day_preparation) ? prep.day_preparation : [];
      const weatherMaterial = isMaterialCurrentWeather(result.weather && result.weather.current);
      if (weatherMaterial && dayPrep[0] && typeof dayPrep[0] === 'object') {
        pushText(dayPrep[0].text);
      }

      const isAmbient = result.preparation_state === 'ambient';
      if (!isAmbient && occupancyActivity) {
        const bind = matchNextSelectedPreparation(prep, occupancyActivity, occupancyPeers);
        if (bind.outcome === 'matched') {
          for (let i = 0; i < bind.texts.length; i++) pushText(bind.texts[i]);
        }
      }

      for (let i = 0; i < dayPrep.length; i++) {
        if (lines.length >= 3) break;
        const row = dayPrep[i];
        if (!row || typeof row !== 'object') continue;
        pushText(row.text);
      }
    }

    const durableAllowed = nowCiHasDeviceGps(deviceCoords);
    const durable = durableAllowed && result && Array.isArray(result.durable_context) ? result.durable_context : [];
    for (let i = 0; i < durable.length; i++) {
      const row = durable[i];
      if (!row || typeof row !== 'object') continue;
      const text = durableNowCiLineText(row);
      if (!text || seen.has(text)) continue;
      seen.add(text);
      lines.push(text);
    }

    return lines;
  }

  function durableNowCiLineText(row) {
    let lang = 'en';
    try {
      lang = String(resolveNuAskPreferredLanguageCode() || 'en');
    } catch {
      lang = 'en';
    }
    const zh = String(row.text_zh_CN || '').trim();
    if (/^zh/i.test(lang) && zh) return zh;
    return String(row.text || row.text_en || '').trim();
  }

  /**
   * Kept NOW enrichment is scoped to client day + NOW situation owner + CURRENT
   * occupancy identity + governing cultural locality. A previous stop / previous
   * day / gap / previous place must not leak (#183).
   */
  function nowCiSituationOwnerKey(clientDayKey, occupancyActivity, cultureLocalityKey) {
    const day = String(clientDayKey || '').trim() || 'none';
    const loc = String(cultureLocalityKey || '').trim() || 'noloc';
    const ident = resolveOccurrenceIdentity(occupancyActivity);
    if (ident.type === 'none' || !ident.value) return `${day}|now|gap|${loc}`;
    return `${day}|now|${ident.type}:${ident.value}|${loc}`;
  }

  /**
   * Keep last material CI bullets when a later evaluate is weather-only
   * (latch/role change, nothing_material). Does not invent copy.
   */
  function retainNowCiEnrichmentLines(derivedLines, keptEnrichment, weatherLine) {
    const derived = Array.isArray(derivedLines) ? derivedLines : [];
    const weather = String(weatherLine || '').trim();
    const enrichment = [];
    for (let i = 0; i < derived.length; i++) {
      const text = String(derived[i] || '').trim();
      if (!text || text === weather) continue;
      enrichment.push(text);
    }
    if (enrichment.length > 0) return derived.filter(Boolean).slice(0, 6);
    const out = [];
    const seen = new Set();
    const push = (raw) => {
      const text = String(raw || '').trim();
      if (!text || seen.has(text) || out.length >= 6) return;
      seen.add(text);
      out.push(text);
    };
    push(weather);
    const kept = Array.isArray(keptEnrichment) ? keptEnrichment : [];
    for (let i = 0; i < kept.length; i++) push(kept[i]);
    return out;
  }

  function getRecoKey(opt, idx) {
    return String(opt?.id || opt?.poi_id || `reco-${idx}`);
  }

  /** One horizontal “rail” height so PAST / NOW / NEXT icon axis lines up when swiping (time corridor). Match NOW refresh orb column (~120px). */
  const CORRIDOR_RAIL_H = 'min-h-[120px]';

  /**
   * **Identical** 7-column template on PAST / NOW / NEXT: inbound · card · dash · card · dash · card · outbound.
   * All three card slots are equal `1fr` so dashed segments share the same horizontal rhythm when the snap scroller moves — a wider center (e.g. `1.35fr`) skewed dashes at panel joins. Center column still fits the 120px orb via inner `max-w-[120px]` + `mx-auto`.
   */
  const TIME_CORRIDOR_GRID =
    'grid grid-cols-[auto_1fr_auto_1fr_auto_1fr_auto] items-center gap-[2px]';

  /** Meta band above the card rail (times row only — slot caption row removed). */
  const CORRIDOR_META_BAND = 'mb-1.5';

  /** Teal dashed segment (horizontal). */
  function CorridorDash() {
    return <div className="h-0 w-full border-t border-dashed" style={{ borderColor: C.stroke }} aria-hidden />;
  }

  /** Endpoint / handoff node on the corridor rail. */
  function CorridorEndDot() {
    return (
      <div
        className="h-1.5 w-1.5 shrink-0 rounded-full"
        style={{ backgroundColor: C.accent, boxShadow: `0 0 6px ${C.accentMuted}` }}
        aria-hidden
      />
    );
  }

  /** Solid teal cap — hands off to the next panel’s inbound dot (time corridor). */
  function CorridorTrailCap() {
    return (
      <div className={`relative flex w-7 shrink-0 items-center ${CORRIDOR_RAIL_H}`} aria-hidden>
        <div className="flex w-full flex-col justify-center">
          <div className="h-0 w-full border-t border-dashed opacity-[0.72]" style={{ borderColor: C.stroke }} />
        </div>
        <div
          className="absolute right-0 top-1/2 h-2 w-1 -translate-y-1/2 rounded-r-full"
          style={{ backgroundColor: C.accent, boxShadow: `0 0 6px ${C.accentMuted}` }}
        />
      </div>
    );
  }

  /** Inbound handoff from the previous panel (matches trail cap / prior dot). */
  function CorridorInbound() {
    return (
      <div className={`flex shrink-0 items-center gap-0 ${CORRIDOR_RAIL_H}`} aria-hidden>
        <CorridorEndDot />
        <div className="mx-0.5 flex w-2 min-w-[6px] flex-col justify-center">
          <CorridorDash />
        </div>
      </div>
    );
  }

  /** Outbound handoff into the next panel (pairs with NEXT / NOW inbound dot). */
  function CorridorOutbound() {
    return (
      <div className={`flex shrink-0 items-center gap-0 ${CORRIDOR_RAIL_H}`} aria-hidden>
        <div className="mx-0.5 flex w-2 min-w-[6px] flex-col justify-center">
          <CorridorDash />
        </div>
        <CorridorEndDot />
      </div>
    );
  }

  /**
   * Same horizontal footprint as `CorridorInbound` / `CorridorOutbound` but **no** rail `min-h-[120px]`.
   * Use in NOW micro-label + time rows only. Full corridor widgets reserve ~120px for card-row alignment; using them
   * (even with `invisible`) in label rows forced those rows to ~120px tall — huge gaps above/below the clock line vs PAST/NEXT.
   */
  function CorridorInboundLabelAlign() {
    return (
      <div className="flex shrink-0 items-center gap-0 self-center" aria-hidden>
        <CorridorEndDot />
        <div className="mx-0.5 flex w-2 min-w-[6px] flex-col justify-center">
          <CorridorDash />
        </div>
      </div>
    );
  }

  function CorridorOutboundLabelAlign() {
    return (
      <div className="flex shrink-0 items-center gap-0 self-center" aria-hidden>
        <div className="mx-0.5 flex w-2 min-w-[6px] flex-col justify-center">
          <CorridorDash />
        </div>
        <CorridorEndDot />
      </div>
    );
  }

  function corridorNavigate(view) {
    const legalAgreed = localStorage.getItem('sylvanflow_legal_agreed');
    const restrictedTabs = ['explore', 'ask-sylvan', 'nu-ask-sylvan'];
    if (restrictedTabs.includes(view) && !legalAgreed) {
      // SF_ALLOW_HARDEXIT_REDIRECT
      window.location.href = '/legal?return=/app';
      return;
    }
    if (window.appState && typeof window.appState.set === 'function') {
      window.appState.set({ view });
    }
  }

  function SlidersIconBrand() {
    /** Same sky accent as Explore `flowPick` + Power Up `flow-finder` hub slot (second tile). */
    const stroke = '#bae6fd';
    const strokeSoft = 'rgba(125,211,252,0.72)';
    return (
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden>
        <line x1="3" y1="7" x2="21" y2="7" stroke={stroke} strokeWidth="2" strokeLinecap="round" />
        <circle cx="14" cy="7" r="3" fill="#0f172a" stroke={strokeSoft} strokeWidth="1.5" />
        <line x1="3" y1="12" x2="21" y2="12" stroke={stroke} strokeWidth="2" strokeLinecap="round" />
        <circle cx="9" cy="12" r="3" fill="#0f172a" stroke={strokeSoft} strokeWidth="1.5" />
        <line x1="3" y1="17" x2="21" y2="17" stroke={stroke} strokeWidth="2" strokeLinecap="round" />
        <circle cx="16" cy="17" r="3" fill="#0f172a" stroke={strokeSoft} strokeWidth="1.5" />
      </svg>
    );
  }

  function HotbarIcon({ spec }) {
    if (spec.icon === 'sliders') return <SlidersIconBrand />;
    if (spec.icon === 'pin') {
      /** Classic 📍 (same glyph as before SVG swap); hue-rotate nudges platform “red pin” toward emerald to align with `spec.ring` / tile chrome. */
      return (
        <span
          className="text-[17px] leading-none inline-block"
          style={{
            filter: 'hue-rotate(118deg) saturate(1.08) brightness(1.03)',
            WebkitFilter: 'hue-rotate(118deg) saturate(1.08) brightness(1.03)'
          }}
          aria-hidden
        >
          📍
        </span>
      );
    }
    if (spec.icon === 'stopwatch') return <span className="text-[17px] leading-none">⏱</span>;
    if (spec.icon === 'shield') return <span className="text-[17px] leading-none">🛡️</span>;
    return null;
  }

  /**
   * Inline Crisis HUD for NOW hotbar Emergency: rose conic rim, LED + mono telemetry header, optional sweep.
   * Wraps `EmergencyInApp` with `embedded` so the list sits on transparent bg inside the dark shell.
   */
  function NuAskEmergencyShell({
    phase,
    errorMessage,
    emergencyId,
    languageCode: emergencyLang,
    onBack,
    onCancel,
    onRetry,
    nuAskT
  }) {
    const t = nuAskT || {};
    const roseGlow = 'rgba(244, 63, 94, 0.42)';

    const headerChrome = (rightSlot) => (
      <div
        className="relative flex shrink-0 items-center justify-between gap-2 border-b border-rose-500/30 bg-gradient-to-b from-rose-950/55 via-[#12080c] to-black/50 px-3 py-2.5"
        style={{ boxShadow: `inset 0 -1px 0 rgba(244,63,94,0.12)` }}
      >
        <div className="flex min-w-0 items-center gap-2">
          <span className="relative flex h-2.5 w-2.5 shrink-0">
            <span
              className="absolute inline-flex h-full w-full animate-ping rounded-full bg-rose-400/55 opacity-70"
              style={{ animationDuration: '1.05s' }}
              aria-hidden
            />
            <span
              className="relative inline-flex h-2.5 w-2.5 rounded-full bg-rose-500"
              style={{ boxShadow: `0 0 12px ${roseGlow}, 0 0 4px rgba(255,255,255,0.35)` }}
              aria-hidden
            />
          </span>
          <div className="min-w-0">
            <p className="font-mono text-[10px] uppercase leading-none tracking-[0.32em] text-rose-200/65">
              {t.emergencyCrisisLink || 'Crisis link'}
            </p>
            <p className="mt-1 min-w-0 bg-gradient-to-r from-rose-50 via-white to-rose-100/90 bg-clip-text font-bold uppercase tracking-[0.22em] text-transparent [font-size:clamp(10px,2.8vw,12px)]">
              {t.emergencyHeaderTitle || 'Emergency'}
            </p>
          </div>
        </div>
        <div className="shrink-0">{rightSlot}</div>
      </div>
    );

    return (
      <div
        className="relative flex h-full min-h-0 flex-col overflow-hidden rounded-[14px]"
        style={{ animation: 'nuask-emergency-reveal 0.42s cubic-bezier(0.22, 1, 0.36, 1) both' }}
      >
        <div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[14px]" aria-hidden>
          <div
            className="absolute left-1/2 top-1/2 h-[220%] min-h-[560px] w-[220%] min-w-[560px]"
            style={{
              transform: 'translate(-50%, -50%)',
              background:
                'conic-gradient(from 0deg at 50% 50%, transparent 0deg, transparent 42deg, rgba(244,63,94,0.14) 72deg, rgba(251,113,133,0.65) 94deg, rgba(254,202,202,0.35) 118deg, transparent 142deg, transparent 360deg)',
              animation: 'nuask-emergency-orbit 19s linear infinite'
            }}
          />
        </div>

        <div
          className="relative z-10 flex h-full min-h-0 flex-col overflow-hidden rounded-[14px] border border-rose-500/35"
          style={{
            background:
              'radial-gradient(100% 70% at 50% 0%, rgba(127,29,29,0.22), transparent 55%), linear-gradient(185deg, #160a10 0%, #090408 55%, #050305 100%)',
            boxShadow: `inset 0 0 0 1px rgba(244,63,94,0.14), 0 22px 50px -28px ${roseGlow}`
          }}
        >
          {phase === 'loading' ? (
            <>
              {headerChrome(
                <button
                  type="button"
                  onClick={onCancel}
                  className="text-[12px] font-semibold uppercase tracking-wide text-cyan-200/95 underline underline-offset-2"
                >
                  {t.cancel || 'Cancel'}
                </button>
              )}
              <div className="pointer-events-none relative h-[2px] w-full shrink-0 overflow-hidden bg-black/40" aria-hidden>
                <div
                  className="absolute inset-y-0 w-[38%] max-w-[180px] bg-gradient-to-r from-transparent via-rose-400/85 to-transparent"
                  style={{ animation: 'nuask-hud-sweep 1.35s ease-in-out infinite' }}
                />
              </div>
              <div className="relative flex min-h-0 flex-1 flex-col items-center justify-center px-6 pb-10 pt-5 text-center">
                <div
                  className="pointer-events-none absolute inset-0 opacity-[0.14]"
                  style={{
                    backgroundImage:
                      'linear-gradient(rgba(244,63,94,0.14) 1px, transparent 1px), linear-gradient(90deg, rgba(244,63,94,0.12) 1px, transparent 1px)',
                    backgroundSize: '20px 20px'
                  }}
                  aria-hidden
                />
                <p className="relative z-[1] font-mono text-[11px] uppercase tracking-[0.36em] text-rose-300/80">
                  {t.emergencyProtocol || 'Emergency protocol'}
                </p>
                <p className="relative z-[1] mt-3 text-[14px] font-semibold text-white">
                  {t.emergencyConnecting || 'Connecting…'}
                </p>
                <p className="relative z-[1] mt-2 max-w-[17rem] text-[13px] leading-relaxed text-rose-100/50">
                  {t.emergencyConnectHint ||
                    'Same request as typing EMERGENCY in Ask Sylvan — resolving services for your location.'}
                </p>
              </div>
            </>
          ) : null}

          {phase === 'error' ? (
            <>
              {headerChrome(
                <button
                  type="button"
                  onClick={onBack}
                  className="text-[12px] font-semibold uppercase tracking-wide text-cyan-200/95 underline underline-offset-2"
                >
                  {t.back || 'Back'}
                </button>
              )}
              <div className="flex min-h-[220px] flex-1 flex-col items-center justify-center gap-3 px-5 pb-12 pt-4 text-center">
                <p className="max-w-[18rem] text-[14px] leading-relaxed text-amber-200/95">
                  {errorMessage && String(errorMessage).trim()
                    ? errorMessage
                    : t.emergencyRequestFailed || 'Request failed. Try again or use Ask Sylvan.'}
                </p>
                <div className="flex flex-wrap items-center justify-center gap-2">
                  <button
                    type="button"
                    onClick={onBack}
                    className="rounded-lg border border-white/20 px-3 py-1.5 text-[12px] font-semibold uppercase tracking-wide text-white"
                  >
                    {t.back || 'Back'}
                  </button>
                  <button
                    type="button"
                    onClick={onRetry}
                    className="rounded-lg bg-rose-600 px-3 py-1.5 text-[12px] font-semibold uppercase tracking-wide text-white shadow-[0_8px_24px_-8px_rgba(225,29,72,0.55)]"
                  >
                    {t.retry || 'Retry'}
                  </button>
                </div>
              </div>
            </>
          ) : null}

          {phase === 'ready' && emergencyId ? (
            <>
              {headerChrome(
                <button
                  type="button"
                  onClick={onBack}
                  className="text-[12px] font-semibold uppercase tracking-wide text-cyan-200/95 underline underline-offset-2"
                >
                  {t.back || 'Back'}
                </button>
              )}
              <div className="min-h-0 flex-1 overflow-y-auto pb-3 pt-1">
                {typeof window !== 'undefined' && window.EmergencyInApp
                  ? React.createElement(window.EmergencyInApp, {
                      tabParams: {
                        emergency_id: emergencyId,
                        mode: 'emergency',
                        languageCode: emergencyLang || undefined
                      },
                      embedded: true
                    })
                  : (
                      <p className="p-4 text-[13px] text-amber-200">
                        {t.emergencyModuleNotLoaded || 'Emergency UI module not loaded.'}
                      </p>
                    )}
              </div>
            </>
          ) : null}
        </div>
      </div>
    );
  }

  /**
   * NOW hotbar **Explore Nearby**: ~5 catalog POIs near you, ranked by **traveler tags + personality** (catalog score), then distance (`nearby_personality_focus` on API).
   */
  function NuAskNearbyPoisShell({ phase, errorMessage, pois, onBack, nuAskT, mapsRegion }) {
    const t = nuAskT || {};
    const region = normalizeMapsRegion(mapsRegion);
    return (
      <div
        className="relative flex h-full min-h-0 flex-col overflow-hidden rounded-[14px] border border-cyan-400/30"
        style={{
          background:
            'radial-gradient(100% 60% at 50% 0%, rgba(6,95,70,0.15), transparent 50%), linear-gradient(185deg, #0f1724 0%, #060809 100%)',
          boxShadow: 'inset 0 0 0 1px rgba(45,226,197,0.12), 0 18px 40px -24px rgba(45,226,197,0.35)'
        }}
      >
        <div className="flex shrink-0 items-center justify-between border-b border-cyan-400/20 bg-black/30 px-3 py-2.5">
          <div className="min-w-0">
            <p className="font-mono text-[10px] uppercase tracking-[0.28em] text-cyan-400/55">{t.shellNearbyKicker || 'Nearby'}</p>
            <p className="mt-0.5 truncate text-[13px] font-semibold text-white">{t.shellNearbyTitle || 'Explore Nearby'}</p>
          </div>
          <button
            type="button"
            onClick={onBack}
            className="shrink-0 text-[12px] font-semibold uppercase tracking-wide text-cyan-200/95 underline underline-offset-2"
          >
            {t.back || 'Back'}
          </button>
        </div>
        <div className="min-h-0 flex-1 overflow-y-auto px-2 py-2 pb-4">
          {phase === 'loading' ? (
            <p className="py-8 text-center text-[14px] font-medium text-[#9CA3AF]">{t.loadingNearbyPois || 'Loading catalog POIs…'}</p>
          ) : null}
          {phase === 'error' ? (
            <p className="px-3 py-6 text-center text-[14px] leading-relaxed text-amber-200/95">{errorMessage}</p>
          ) : null}
          {phase === 'ready' && (!pois || pois.length === 0) ? (
            <p className="py-8 text-center text-[13px] text-[#9CA3AF]">{t.noPoisInRange || 'No catalog POIs in range.'}</p>
          ) : null}
          {phase === 'ready' && pois && pois.length > 0 ? (
            <ul className="space-y-2">
              {pois.map((poi, i) => {
                const mapsUrl = buildExternalPlaceMapUrl(poi.lat, poi.lng, poi.name, region);
                const dm =
                  typeof poi.distance_m === 'number'
                    ? poi.distance_m < 1000
                      ? `${poi.distance_m} m`
                      : `${(poi.distance_m / 1000).toFixed(1)} km`
                    : '—';
                const cat = poi.primary_category ? String(poi.primary_category) : nuAskUiStrings.poiWord || 'POI';
                return (
                  <li key={`${poi.place_id || poi.name || 'poi'}-${i}`}>
                    <a
                      href={mapsUrl}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="block rounded-xl border border-white/[0.08] bg-black/40 px-3 py-2.5 transition hover:border-cyan-400/25 hover:bg-black/55"
                    >
                      <p className="text-[15px] font-semibold leading-snug text-white">{poi.name || 'Place'}</p>
                      <p className="mt-1 text-[12px] text-cyan-200/75">
                        {cat} · {dm}
                      </p>
                    </a>
                  </li>
                );
              })}
            </ul>
          ) : null}
        </div>
      </div>
    );
  }

  /** NOW hotbar **Improve Plan**: weather-first + indoor/outdoor hints + advisory reorder (same day). */
  function NuAskWeatherPlanShell({ phase, errorMessage, data, onBack, nuAskT, onApplyOrder, applyPhase, applyError }) {
    const t = nuAskT || {};
    const snaps = Array.isArray(data?.weather_snapshots) ? data.weather_snapshots : [];
    const acts = Array.isArray(data?.activities) ? data.activities : [];
    const sugg = Array.isArray(data?.suggestions) ? data.suggestions : [];
    const proposed = Array.isArray(data?.proposed_order) ? data.proposed_order : [];
    const shifts = Array.isArray(data?.applyable_shifts) ? data.applyable_shifts : [];
    const canApply = shifts.length > 0 && data?.route_feasibility?.reorder_applied === true;
    const rf = data?.route_feasibility;
    const ch = rf?.chronological_route;
    const wr = rf?.weather_route;
    return (
      <div
        className="relative flex h-full min-h-0 flex-col overflow-hidden rounded-[14px] border border-sky-400/30"
        style={{
          background:
            'radial-gradient(100% 55% at 50% 0%, rgba(14,165,233,0.12), transparent 52%), linear-gradient(185deg, #0c1522 0%, #060809 100%)',
          boxShadow: 'inset 0 0 0 1px rgba(56,189,248,0.12), 0 18px 40px -24px rgba(56,189,248,0.28)'
        }}
      >
        <div className="flex shrink-0 items-center justify-between border-b border-sky-400/25 bg-black/35 px-3 py-2.5">
          <div className="min-w-0">
            <p className="font-mono text-[10px] uppercase tracking-[0.28em] text-sky-400/55">{t.shellImproveKicker || 'Weather first'}</p>
            <p className="mt-0.5 truncate text-[13px] font-semibold text-white">{t.shellImproveTitle || 'Improve Plan'}</p>
          </div>
          <button
            type="button"
            onClick={onBack}
            className="shrink-0 text-[12px] font-semibold uppercase tracking-wide text-sky-200/95 underline underline-offset-2"
          >
            {t.back || 'Back'}
          </button>
        </div>
        <div className="min-h-0 flex-1 overflow-y-auto px-2.5 py-2 pb-4">
          {phase === 'loading' ? (
            <p className="py-8 text-center text-[14px] font-medium text-[#9CA3AF]">{t.loadingWeatherPlan || 'Analyzing weather and today’s plan…'}</p>
          ) : null}
          {phase === 'error' ? (
            <p className="px-2 py-6 text-center text-[14px] leading-relaxed text-amber-200/95">{errorMessage}</p>
          ) : null}
          {phase === 'ready' && data ? (
            <>
              <div className="mb-3 flex flex-wrap gap-1.5">
                {snaps.map((s, i) => (
                  <div
                    key={`${s.label}-${i}`}
                    className="min-w-0 flex-1 rounded-lg border border-white/10 bg-black/40 px-2 py-1.5 text-center"
                  >
                    <p className="text-[10px] font-mono uppercase tracking-wide text-sky-300/70">{s.label}</p>
                    <p className="mt-0.5 text-[13px] font-semibold text-white">
                      {s.temp_c != null ? `${s.temp_c}°` : '—'}
                    </p>
                    <p className="text-[11px] leading-tight text-[#9CA3AF]">
                      {s.condition || '—'}
                      {s.pop != null ? ` · ${s.pop}% ${t.weatherRainWord || 'rain'}` : ''}
                    </p>
                  </div>
                ))}
              </div>
              <p className="mb-2 text-[12px] font-medium text-sky-200/90">
                {(t.weatherRiskNextHoursTemplate || 'Next ~{hours}h risk:').replace(
                  '{hours}',
                  String(data.horizon_hours || 6)
                )}{' '}
                <span className="text-white">
                  {data.horizon_precipitation_risk === 'elevated'
                    ? t.weatherRiskHigher || 'Higher'
                    : t.weatherRiskLower || 'Lower'}
                </span>
              </p>
              <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#6B7280]">
                {t.weatherExposureHeading || 'Your day (exposure guess)'}
              </p>
              <ul className="mb-3 space-y-1.5">
                {acts.map((a, i) => (
                  <li
                    key={`act-${i}-${a.title}`}
                    className="rounded-lg border border-white/[0.07] bg-black/30 px-2.5 py-1.5 text-[13px]"
                  >
                    <span className="font-medium text-white">{a.title}</span>
                    <span className="text-[#9CA3AF]"> · {a.start_time_local || '—'}</span>
                    <span
                      className={`ml-1 rounded px-1 py-px text-[11px] font-semibold ${
                        a.exposure === 'outdoor'
                          ? 'bg-emerald-950/80 text-emerald-300'
                          : a.exposure === 'indoor' || a.exposure === 'mixed'
                            ? 'bg-slate-800 text-sky-200'
                            : 'bg-black/50 text-[#9CA3AF]'
                      }`}
                    >
                      {a.exposure}
                    </span>
                    {a.in_horizon ? (
                      <span className="ml-1 text-[11px] text-amber-200/90">{t.weatherInWindow || 'in window'}</span>
                    ) : null}
                  </li>
                ))}
              </ul>
              {sugg.length > 0 ? (
                <div className="mb-3 rounded-xl border border-amber-500/20 bg-amber-950/25 px-2.5 py-2">
                  <p className="text-[11px] font-semibold uppercase tracking-wide text-amber-200/80">
                    {t.weatherSuggestionsHeading || 'Suggestions'}
                  </p>
                  {sugg.map((t, i) => (
                    <p key={`sg-${i}`} className="mt-1.5 text-[13px] leading-relaxed text-amber-50/95">
                      {t}
                    </p>
                  ))}
                </div>
              ) : null}
              {ch && ch.legs > 0 ? (
                <div className="mb-3 rounded-lg border border-white/[0.08] bg-black/25 px-2.5 py-2">
                  <p className="text-[11px] font-semibold uppercase tracking-wide text-[#6B7280]">
                    {t.weatherRouteSenseHeading || 'Route sense (mapped stops)'}
                  </p>
                  <p className="mt-1 text-[12px] leading-relaxed text-[#9CA3AF]">
                    {(t.weatherClockOrderTemplate ||
                      'Clock order ≈ {drive_min} min straight-line leg total between stops with coordinates')
                      .replace('{drive_min}', String(ch.drive_min ?? '—'))}
                    {rf?.reorder_applied && wr && wr.legs > 0
                      ? ` · ${(t.weatherSuggestedOrderTemplate || 'weather-suggested order ≈ {drive_min} min.')
                          .replace('{drive_min}', String(wr.drive_min ?? '—'))}`
                      : ''}
                  </p>
                  {rf?.skipped_weather_reorder_reason ? (
                    <p className="mt-1.5 text-[12px] leading-relaxed text-sky-200/90">{rf.skipped_weather_reorder_reason}</p>
                  ) : null}
                </div>
              ) : null}
              {proposed.length > 0 ? (
                <div className="mb-2">
                  <p className="text-[12px] font-semibold uppercase tracking-wide text-[#6B7280]">
                    {t.weatherSuggestedVisitOrderHeading || 'Suggested visit order'}
                  </p>
                  <ol className="mt-1.5 list-decimal space-y-1 pl-4 text-[13px] text-[#D1D5DB]">
                    {proposed.map((p, i) => (
                      <li key={`pr-${i}-${p.title}`}>
                        <span className="font-medium text-white">{p.title}</span>
                        <span className="text-[#9CA3AF]">
                          {' '}
                          ({p.proposed_start_time_local || p.start_time_local || '—'}
                          {p.hard_booking || p.pinned ? ` · ${t.weatherFixedPin || 'Fixed'}` : ''}
                          {p.old_start_time_local &&
                          p.proposed_start_time_local &&
                          p.old_start_time_local !== p.proposed_start_time_local
                            ? ` · was ${p.old_start_time_local}`
                            : ''}
                          )
                        </span>
                        <span className="block text-[11px] text-sky-200/75">{p.reason}</span>
                      </li>
                    ))}
                  </ol>
                  {canApply ? (
                    <div className="mt-3">
                      <button
                        type="button"
                        disabled={applyPhase === 'loading'}
                        onClick={() => onApplyOrder?.(shifts)}
                        className="w-full rounded-xl border border-sky-400/40 bg-sky-950/50 px-3 py-2.5 text-[13px] font-semibold text-sky-50 disabled:opacity-60"
                      >
                        {applyPhase === 'loading'
                          ? t.weatherApplyLoading || 'Applying order…'
                          : t.weatherApplyOrder || 'Apply this order'}
                      </button>
                      <p className="mt-1.5 text-[11px] leading-relaxed text-[#9CA3AF]">
                        {t.weatherApplyHint ||
                          'Moves soft stops only. Fixed bookings stay put. You can undo from Flow Plan if needed.'}
                      </p>
                      {applyPhase === 'error' && applyError ? (
                        <p className="mt-1.5 text-[12px] text-amber-200/95">{applyError}</p>
                      ) : null}
                      {applyPhase === 'done' ? (
                        <p className="mt-1.5 text-[12px] text-emerald-300/95">
                          {t.weatherApplyDone || 'Order applied to today’s plan.'}
                        </p>
                      ) : null}
                    </div>
                  ) : null}
                </div>
              ) : null}
              <p className="text-[11px] leading-relaxed text-[#6B7280]">{data.disclaimer}</p>
            </>
          ) : null}
        </div>
      </div>
    );
  }

  /**
   * NOW hotbar **Use My Time**: time-budget lane — nearby catalog POIs labeled by travel + dwell vs **Available Window**.
   * Uses `POST /api/nuask/nearby-pois` + client gap-fit math (distinct from distance-only Nearby list and Improve Plan).
   */
  function NuAskUseMyTimeShell({ phase, errorMessage, windowMins, endLabel, rows, onBack, nuAskT, mapsRegion }) {
    const t = nuAskT || {};
    const region = normalizeMapsRegion(mapsRegion);
    const W = Math.max(0, Math.round(Number(windowMins) || 0));
    return (
      <div
        className="relative flex h-full min-h-0 flex-col overflow-hidden rounded-[14px] border border-violet-400/35"
        style={{
          background:
            'radial-gradient(100% 55% at 50% 0%, rgba(124,58,237,0.14), transparent 52%), linear-gradient(185deg, #150c1f 0%, #060809 100%)',
          boxShadow: 'inset 0 0 0 1px rgba(167,139,250,0.14), 0 18px 40px -24px rgba(124,58,237,0.32)'
        }}
      >
        <div className="flex shrink-0 items-center justify-between border-b border-violet-400/25 bg-black/35 px-3 py-2.5">
          <div className="min-w-0">
            <p className="font-mono text-[10px] uppercase tracking-[0.28em] text-violet-400/55">{t.shellTimeKicker || 'Time budget'}</p>
            <p className="mt-0.5 truncate text-[13px] font-semibold text-white">{t.shellTimeTitle || 'Use My Time'}</p>
            <p className="mt-1 text-[11px] leading-snug text-violet-200/75">
              ~{formatDurationMins(W)} {t.timeLeftWord || 'left'} · {t.untilWord || 'until'} <span className="text-white/90">{endLabel || '—'}</span>
            </p>
          </div>
          <button
            type="button"
            onClick={onBack}
            className="shrink-0 text-[12px] font-semibold uppercase tracking-wide text-violet-200/95 underline underline-offset-2"
          >
            {t.back || 'Back'}
          </button>
        </div>
        <div className="min-h-0 flex-1 overflow-y-auto px-2.5 py-2 pb-4">
          {phase === 'loading' ? (
            <p className="py-8 text-center text-[14px] font-medium text-[#9CA3AF]">{t.scoringWindowPois || 'Scoring POIs against your window…'}</p>
          ) : null}
          {phase === 'error' ? (
            <p className="px-2 py-6 text-center text-[14px] leading-relaxed text-amber-200/95">{errorMessage}</p>
          ) : null}
          {phase === 'ready' && W <= 0 ? (
            <div className="rounded-xl border border-amber-500/25 bg-amber-950/30 px-3 py-3">
              <p className="text-[13px] font-semibold text-amber-100">{t.noMinutesInWindow || 'No minutes left in this window'}</p>
              <p className="mt-1.5 text-[12px] leading-relaxed text-[#9CA3AF]">{t.adjustPlanHint || 'Adjust your plan or use Improve Plan if today needs reordering.'}</p>
            </div>
          ) : null}
          {phase === 'ready' && W > 0 && (!rows || rows.length === 0) ? (
            <p className="py-8 text-center text-[13px] text-[#9CA3AF]">{t.noPoisInRange || 'No catalog POIs in range.'}</p>
          ) : null}
          {phase === 'ready' && W > 0 && rows && rows.length > 0 ? (
            <>
              <p className="mb-2 text-[11px] leading-relaxed text-[#9CA3AF]">
                {t.gapFitFootnote ||
                  `Straight-line travel + typical dwell + ${GAP_BUFFER_MIN}m buffer — estimates only (not live traffic).`}
              </p>
              <ul className="space-y-2">
                {rows.map((row, i) => {
                  const poi = row.poi;
                  const mapsUrl = buildExternalPlaceMapUrl(poi.lat, poi.lng, poi.name, region);
                  const dm =
                    typeof poi.distance_m === 'number'
                      ? poi.distance_m < 1000
                        ? `${poi.distance_m} m`
                        : `${(poi.distance_m / 1000).toFixed(1)} km`
                      : '—';
                  const cat = poi.primary_category ? String(poi.primary_category) : nuAskUiStrings.poiWord || 'POI';
                  const badge =
                    row.fit === 'comfortable'
                      ? {
                          text: t.fitComfortable || 'Comfortable fit',
                          cls: 'bg-emerald-950/90 text-emerald-200 border-emerald-500/35'
                        }
                      : row.fit === 'tight'
                        ? { text: t.fitTight || 'Tight', cls: 'bg-amber-950/80 text-amber-100 border-amber-500/35' }
                        : row.fit === 'skip'
                          ? { text: t.fitWontFit || "Won't fit", cls: 'bg-zinc-900/90 text-zinc-400 border-white/10' }
                          : { text: '—', cls: 'bg-black/40 text-[#9CA3AF] border-white/10' };
                  const leg =
                    row.travelMin != null && row.totalMin != null
                      ? (t.gapFitLegTemplate || '~{travel}m travel + ~{dwell}m visit + buffer → ~{total}m')
                          .replace('{travel}', String(row.travelMin))
                          .replace('{dwell}', String(row.dwellMin))
                          .replace('{total}', String(row.totalMin))
                      : '—';
                  return (
                    <li key={`umt-${poi.place_id || poi.name || i}-${i}`}>
                      <a
                        href={mapsUrl}
                        target="_blank"
                        rel="noopener noreferrer"
                        className={`block rounded-xl border px-3 py-2.5 transition hover:border-violet-400/35 hover:bg-black/55 ${
                          row.fit === 'skip' ? 'border-white/[0.06] bg-black/25 opacity-80' : 'border-white/[0.08] bg-black/40'
                        }`}
                      >
                        <div className="flex items-start justify-between gap-2">
                          <p className="min-w-0 text-[15px] font-semibold leading-snug text-white">
                            {poi.name || nuAskUiStrings.placeWord || 'Place'}
                          </p>
                          <span className={`shrink-0 rounded border px-1.5 py-px text-[10px] font-bold uppercase tracking-wide ${badge.cls}`}>
                            {badge.text}
                          </span>
                        </div>
                        <p className="mt-1 text-[12px] text-violet-200/75">
                          {cat} · {dm}
                        </p>
                        <p className="mt-1 font-mono text-[11px] leading-relaxed text-[#9CA3AF]">{leg}</p>
                      </a>
                    </li>
                  );
                })}
              </ul>
            </>
          ) : null}
        </div>
      </div>
    );
  }

  function NuAskSylvanPage({ embeddedAppShell = false, isAdmin = false } = {}) {
    const scrollerRef = useRef(null);
    const tourPanelNavRef = useRef(null);
    const tourNowRailRef = useRef(null);
    /** Engine-trace why: AppShell prop + local `/api/admin/check` (Analytics-style) so CDN-stale app.js without `isAdmin` still unlocks admins. */
    const [adminWhyUnlocked, setAdminWhyUnlocked] = useState(!!isAdmin);
    useEffect(() => {
      if (isAdmin) setAdminWhyUnlocked(true);
    }, [isAdmin]);
    useEffect(() => {
      let cancelled = false;
      (async () => {
        try {
          const sessionToken =
            typeof localStorage !== 'undefined' ? localStorage.getItem('sylvanflow_session_token') : null;
          if (!sessionToken || sessionToken.length <= 20) return;
          const base =
            (typeof window !== 'undefined' && window.SF_API_BASE) ||
            'https://sf-api-gateway.foofiebean.workers.dev';
          const fetchFn =
            typeof window !== 'undefined' && window.SylvanFlowAuth?.authenticatedFetch
              ? window.SylvanFlowAuth.authenticatedFetch.bind(window.SylvanFlowAuth)
              : null;
          if (!fetchFn) return;
          const res = await fetchFn(`${base}/api/admin/check`, {
            headers: { 'Content-Type': 'application/json' }
          });
          if (!res || !res.ok) return;
          const data = await res.json().catch(() => null);
          if (!cancelled && data && data.ok && data.isAdmin === true) {
            setAdminWhyUnlocked(true);
          }
        } catch (_) {
          /* non-admin / network — keep traveler why */
        }
      })();
      return () => {
        cancelled = true;
      };
    }, []);
    const [corridorTourOpen, setCorridorTourOpen] = useState(false);
    const [corridorTourForce, setCorridorTourForce] = useState(false);
    const [corridorTourAwaitOpen, setCorridorTourAwaitOpen] = useState(false);
    const [corridorTourDontShow, setCorridorTourDontShow] = useState(false);
    const [corridorTourLabels, setCorridorTourLabels] = useState({
      skip: 'Skip',
      next: 'Next',
      done: 'Done',
      dontShowAgain: "Don't show this again",
      corridorTitle: 'Time corridor',
      corridorBody:
        'Your day is organized in three panels — Past, Now, and Next. Swipe horizontally or tap a tab to move through time.',
      nowTitle: 'The corridor rail',
      nowBody:
        'Each panel shows times and stops along your trip. Now is where you decide what to do in this moment; Past and Next help you reflect and plan ahead.'
    });
    const [panel, setPanel] = useState(1);
    const [profile, setProfile] = useState(() => window.SylvanFlowState?.getProfile?.() || null);
    const [profilePhotoUrl, setProfilePhotoUrl] = useState(null);
    const [pastStopsRow, setPastStopsRow] = useState(() => NUASK_INITIAL_PAST_STOPS_ROW.map((s) => ({ ...s })));
    const [pastCardTargets, setPastCardTargets] = useState([null, null, null]);
    const [pastJourneyItems, setPastJourneyItems] = useState(() =>
      NUASK_INITIAL_PAST_JOURNEY_ITEMS.map((s) => ({ ...s }))
    );
    const [placesActivityCount, setPlacesActivityCount] = useState(0);
    const [corridorActivities, setCorridorActivities] = useState([]);
    const [corridorTripKey, setCorridorTripKey] = useState(null);
    /** Trip key for today's plan only — sent on micro-action select / weather-day so future-only itineraries are not hinted. */
    const [corridorMutationTripKey, setCorridorMutationTripKey] = useState(null);
    const hasActiveTripContext = useMemo(() => {
      const now = new Date();
      const y = offsetDayKeyLocal(now, -1);
      const t = offsetDayKeyLocal(now, 0);
      const tm = offsetDayKeyLocal(now, 1);
      const activeWindow = new Set([y, t, tm]);
      return corridorActivities.some((a) => activeWindow.has(asText(a?.__dayKey)));
    }, [corridorActivities]);
    const [nowPrevCard, setNowPrevCard] = useState({
      time: DEMO.prevTime,
      title: DEMO.prevStop,
      sub: 'Visited',
      icon: '🏛️'
    });
    const [nowNextCard, setNowNextCard] = useState({
      time: DEMO.nextTime,
      title: DEMO.nextStop,
      sub: 'Next',
      icon: '🦁'
    });
    /** Shared after-next itinerary card for NOW corridor / Go / HIS (DEMO when no data). Live NEXT uses `nextLiveSecond`. */
    const [nextAfterCard, setNextAfterCard] = useState({
      time: DEMO.afterThat.time,
      title: DEMO.afterThat.title,
      sub: DEMO.afterThat.sub,
      icon: '🌿',
      imageUrl: ''
    });
    /** When itinerary has a second future stop + trip context — opens activity detail (same as PAST journey cards). */
    const [nextAfterCardTarget, setNextAfterCardTarget] = useState(null);
    const [hasUpcomingActivity, setHasUpcomingActivity] = useState(false);
    /** Next corridor commitment is a fixed booking — Replace Next disabled proactively (#124.9). */
    const [nowNextHardBooking, setNowNextHardBooking] = useState(false);
    /** Start time (ms) of the latest started/current itinerary row — drives 30m Add Here lock (not Earlier Stop). */
    const [nowCorridorPrevStartMs, setNowCorridorPrevStartMs] = useState(0);
    /** NEXT hero body: first non-empty description from the next commitment activity (KV/bootstrap fallback if empty). */
    const [nextHighlightBody, setNextHighlightBody] = useState('');
    const [nowNextImageUrl, setNowNextImageUrl] = useState('');
    const [nowRecoOptions, setNowRecoOptions] = useState([]);
    const [nowRecoPersistUntilMs, setNowRecoPersistUntilMs] = useState(0);
    const [nowRecoLoading, setNowRecoLoading] = useState(false);
    /** First successful refresh completion gates TOP reco shell (no demo flash before sync). */
    const [nowRecoSyncPhase, setNowRecoSyncPhase] = useState('awaiting');
    /** Slice 1: LEFT situation vs RIGHT reco. After Tap NOW completes, land on RIGHT. Never tapped → LEFT. */
    const [nowTvInnerPane, setNowTvInnerPane] = useState('left');
    /** HUD “!” explain popovers: `reco_top` | `alternatives_slot_time` | `poi_hours` */
    const [nuAskExplainKey, setNuAskExplainKey] = useState(null);
    const [topRecoRevealKey, setTopRecoRevealKey] = useState(0);
    /**
     * NOW hotbar detail overlay (replaces TOP recommendation column).
     * Emergency: same contract as chat — POST /api/chat/send message "EMERGENCY", then embed EmergencyInApp.
     */
    const [nowHotbarDetail, setNowHotbarDetail] = useState(null);
    /**
     * A2 preview — Tools sheet open state (`NUASK_A2_COLLAPSE_HOTKEYS`).
     * Revert feeling: set flag false at file top, or revert the commit.
     */
    const [nowHotkeysOpen, setNowHotkeysOpen] = useState(false);
    /** Keep A2 tools sheet mounted while slide-down exit runs. */
    const [nowHotkeysLeaving, setNowHotkeysLeaving] = useState(false);

    const prefersHubMotionReduce = () =>
      !!(window.SYLVAN_TECH_THEME && window.SYLVAN_TECH_THEME.prefersReducedMotion?.());

    const closeNowHotkeys = useCallback(() => {
      if (nowHotkeysLeaving) return;
      if (!nowHotkeysOpen) {
        setNowHotkeysLeaving(false);
        return;
      }
      if (prefersHubMotionReduce()) {
        setNowHotkeysOpen(false);
        setNowHotkeysLeaving(false);
        return;
      }
      setNowHotkeysLeaving(true);
      setNowHotkeysOpen(false);
    }, [nowHotkeysOpen, nowHotkeysLeaving]);

    const openNowHotkeys = useCallback(() => {
      setNowHotkeysLeaving(false);
      setNowHotkeysOpen(true);
    }, []);

    const onNowHotkeysSheetAnimEnd = useCallback(
      (e) => {
        if (!nowHotkeysLeaving) return;
        const name = e && e.animationName != null ? String(e.animationName) : '';
        if (name && name.indexOf('sf-hub-tools-sheet-out') === -1) return;
        setNowHotkeysLeaving(false);
      },
      [nowHotkeysLeaving]
    );
    /** A2 Option A — NOW corridor expanded (full rail). Default collapsed = words strip. */
    const [nowCorridorExpanded, setNowCorridorExpanded] = useState(false);
    /**
     * Corridor / PAST / NEXT itinerary chrome — hidden until first itineraries fetch completes
     * and temporal cards are derived. NOW strip additionally waits for first TOP reco ready
     * so the time corridor does not sit above Enter-the-fray on first entry.
     */
    const [corridorDataReady, setCorridorDataReady] = useState(false);
    const [corridorCalmRevealKey, setCorridorCalmRevealKey] = useState(0);
    const corridorHydratedRef = useRef(false);
    const corridorDataReadyRef = useRef(false);
    /** Latches true once NOW preview sync reaches `ready` — keeps strip visible on later refreshes. */
    const [nowRecoEverReady, setNowRecoEverReady] = useState(false);
    const NUASK_CALM_FADE_STYLE = {
      /* Opacity-only (no transform) — safe near fixed/overlay chrome. */
      animation: 'nuask-calm-fade-in 0.72s cubic-bezier(0.22, 1, 0.36, 1) both'
    };
    /** Explore Nearby: TOP-slot catalog list (distance-only, no traveler-tag personalization) */
    const [nowNearbyOpen, setNowNearbyOpen] = useState(false);
    const [nowNearbyPhase, setNowNearbyPhase] = useState('idle');
    const [nowNearbyPois, setNowNearbyPois] = useState([]);
    const [nowNearbyError, setNowNearbyError] = useState('');
    /** Improve Plan: weather vs today’s itinerary (advisory). */
    const [nowWeatherPlanOpen, setNowWeatherPlanOpen] = useState(false);
    const [nowWeatherPlanPhase, setNowWeatherPlanPhase] = useState('idle');
    const [nowWeatherPlanData, setNowWeatherPlanData] = useState(null);
    /** Full Ask Sylvan thread inside NOW TV bezel (\`AskSylvanPage\` + \`embeddedExploreTv\`), not standalone \`ask-sylvan\` route. */
    const [nowTvAskOpen, setNowTvAskOpen] = useState(false);
    const [nowWeatherPlanError, setNowWeatherPlanError] = useState('');
    const [nowWeatherApplyPhase, setNowWeatherApplyPhase] = useState('idle');
    const [nowWeatherApplyError, setNowWeatherApplyError] = useState('');
    /** Use My Time: gap-fit panel (nearby POIs scored vs Available Window). */
    const [nowUseMyTimeOpen, setNowUseMyTimeOpen] = useState(false);
    const [nowUseMyTimePhase, setNowUseMyTimePhase] = useState('idle');
    const [nowUseMyTimeError, setNowUseMyTimeError] = useState('');
    const [nowUseMyTimeRows, setNowUseMyTimeRows] = useState([]);
    const [nowRecoError, setNowRecoError] = useState('');
    const [nowRecoMicroActionId, setNowRecoMicroActionId] = useState('');
    const [selectedRecoKey, setSelectedRecoKey] = useState(null);
    const [nowRecoActionLoading, setNowRecoActionLoading] = useState('');
    const [nowRecoActionError, setNowRecoActionError] = useState('');
    const [nowRecoActionInfo, setNowRecoActionInfo] = useState('');
    const [nowEnRouteLockUntilMs, setNowEnRouteLockUntilMs] = useState(0);
    const [nowEnRouteLockPoiName, setNowEnRouteLockPoiName] = useState('');
    const [nowEnRouteLockPoiId, setNowEnRouteLockPoiId] = useState('');
    const [nowEnRouteLockLat, setNowEnRouteLockLat] = useState(null);
    const [nowEnRouteLockLng, setNowEnRouteLockLng] = useState(null);
    const [nowRecoProposal, setNowRecoProposal] = useState(null);
    /** After successful Add Here with `undo_available` — shows Undo in third action slot (#124.10). */
    const [nowUndoAvailable, setNowUndoAvailable] = useState(false);
    const [corridorReloadTick, setCorridorReloadTick] = useState(0);
    const [pastMiniMapImgFailed, setPastMiniMapImgFailed] = useState(false);
    const [showLocationPrompt, setShowLocationPrompt] = useState(false);
    const [topRecoImageIdx, setTopRecoImageIdx] = useState(0);
    const [topRecoImageExhausted, setTopRecoImageExhausted] = useState(false);
    const [topRecoExpanded, setTopRecoExpanded] = useState(true);
    const [expandedAltRecoKey, setExpandedAltRecoKey] = useState(null);
    const [nowClock, setNowClock] = useState(() => {
      const now = new Date();
      const anchor = parseTimeToMinutes(DEMO.nextTime);
      const end = new Date(now);
      if (anchor != null) {
        end.setHours(Math.floor(anchor / 60), anchor % 60, 0, 0);
        // Keep a forward-looking window if current time has passed today's anchor.
        if (end.getTime() < now.getTime()) end.setDate(end.getDate() + 1);
      } else {
        end.setMinutes(end.getMinutes() + DEMO.windowMins);
      }
      const mins = Math.max(0, Math.round((end.getTime() - now.getTime()) / 60000));
      return {
        nowLabel: formatTimeLabel(now),
        endLabel: formatTimeLabel(end),
        rangeStartLabel: formatTimeLabel(now),
        mins,
        minsUntilNextStart: mins
      };
    });
    const [nuAskUiStrings, setNuAskUiStrings] = useState(() => {
      const base = buildNuAskUiStringBase(
        typeof window !== 'undefined' ? resolveNuAskPreferredLanguageCode() : 'en'
      );
      return Object.keys(base).length > 0
        ? base
        : {
            pastSubtitle: 'Your journey so far',
            chatPlaceholder: 'Ask...',
            panelPastTitle: 'PAST',
            panelNowTitle: 'NOW',
            panelNowSubtitle: 'Your moment to decide',
            panelNextTitle: 'NEXT',
            panelNextSubtitle: "What's coming up",
            panelSwitcherAria: 'Time corridor panels'
          };
    });

    const nowHotbarItems = useMemo(() => {
      const byKey = {
        nearby: { title: nuAskUiStrings.hotbarNearbyTitle, sub: nuAskUiStrings.hotbarNearbySub },
        improve: { title: nuAskUiStrings.hotbarImproveTitle, sub: nuAskUiStrings.hotbarImproveSub },
        time: { title: nuAskUiStrings.hotbarTimeTitle, sub: nuAskUiStrings.hotbarTimeSub },
        emergency: { title: nuAskUiStrings.hotbarEmergencyTitle, sub: nuAskUiStrings.hotbarEmergencySub }
      };
      return DEMO.nowHotbar.map((h) => ({
        ...h,
        title: byKey[h.key]?.title || h.title,
        sub: byKey[h.key]?.sub || h.sub
      }));
    }, [nuAskUiStrings]);

    /** NOW panel — inline Ask (same APIs as AskSylvanPage; optional full-chat navigation). */
    const [nuAskInlineInput, setNuAskInlineInput] = useState('');
    const [nuAskInlineSending, setNuAskInlineSending] = useState(false);
    const [nuAskInlineTyping, setNuAskInlineTyping] = useState(false);
    const [nuAskLastAssistantText, setNuAskLastAssistantText] = useState('');
    const [nuAskLatestVisible, setNuAskLatestVisible] = useState(false);
    const [nuAskLatestMounted, setNuAskLatestMounted] = useState(false);
    /** Half-viewport “hologram” latest reply vs compact strip (Back returns to strip). */
    const [nuAskLatestHolo, setNuAskLatestHolo] = useState(false);
    const [nuAskInlineError, setNuAskInlineError] = useState('');
    const [nowLockTickMs, setNowLockTickMs] = useState(() => Date.now());
    const NUASK_INLINE_MAX_LEN = 240;
    const NUASK_LATEST_TTL_MS = 120000;
    const NUASK_LATEST_FADE_MS = 500;
    const NUASK_STT_MAX_MS = 20000;
    /** Minimum spoken length before stop→transcribe (avoids empty WebM / accidental double-tap). */
    const NUASK_STT_MIN_MS = 700;
    /** Secondary empty-clip guard (server rejects ~480B; WebM headers alone are often <1KB). */
    const NUASK_STT_MIN_BLOB_BYTES = 480;
    const NUASK_STT_MAX_BYTES = 1000000;
    const EN_ROUTE_LOCK_STORAGE_KEY = 'nuask_en_route_lock_v1';
    const nuAskInlineInputRef = useRef('');
    const nuAskInlineSendingRef = useRef(false);
    const nuAskSttRecorderRef = useRef(null);
    const nuAskSttStreamRef = useRef(null);
    const nuAskSttChunksRef = useRef([]);
    const nuAskSttTimeoutRef = useRef(null);
    const nuAskSttStartedAtRef = useRef(0);
    /** Ignore stop taps in the first ~350ms after start (ghost second tap from the same gesture). */
    const nuAskSttArmMsRef = useRef(0);
    const nuAskSttStartingRef = useRef(false);
    const nuAskLatestHideTimerRef = useRef(null);
    const nuAskLatestClearTimerRef = useRef(null);
    const [nuAskSttPhase, setNuAskSttPhase] = useState('idle');
    /** Ex.1 hold radial — long-press pulsing NOW sync orb (not mic). */
    const [ex1RadialOpen, setEx1RadialOpen] = useState(false);
    const [ex1RadialAnchor, setEx1RadialAnchor] = useState(null);
    const [ex1RadialHint, setEx1RadialHint] = useState('');
    /** Alive session manifest (null → fail closed empty seats; never SOP six as Hold menu). */
    const [ex1AliveManifest, setEx1AliveManifest] = useState(null);
    /** Server companion_line only — display-only; never client-invent on enter miss. */
    const [ex1AliveCompanionLine, setEx1AliveCompanionLine] = useState(null);
    /** Hold Phase 3 — faint memory chips (Tired · Tonight protected · Quiet · …). */
    const [ex1MemoryChips, setEx1MemoryChips] = useState([]);
    /** Hold Phase 3 — POI focus card inside hologram (not NOW TV). */
    const [ex1FocusCard, setEx1FocusCard] = useState(null);
    /** NuAsk HIS — floating phase question above the wheel (§6 / §9). */
    const [ex1SpacePrompt, setEx1SpacePrompt] = useState(null);
    /** Hold Phase 4 — Governor-in-space status (Checking… / Added…). */
    const [ex1FieldStatus, setEx1FieldStatus] = useState('');
    /** HIS bottom TV — path / POI / Governor (Stage Design V1 · UNLOCK_UI_GOLD 2026-08-21). */
    const [ex1HisTv, setEx1HisTv] = useState(null);
    /** False until Alive enter settles (or fails closed). Gates seats + companion reveal. */
    const [ex1RadialSeatsReady, setEx1RadialSeatsReady] = useState(false);
    const ex1HoldTimerRef = useRef(null);
    const ex1HoldArmedRef = useRef(false);
    const ex1AliveSessionRef = useRef(null);
    const ex1AliveEnterInFlightRef = useRef(null);
    const ex1PendingTvPoiRef = useRef(null);
    const ex1HoldGovernorBusyRef = useRef(false);
    const ex1SubmitRecoRef = useRef(null);
    /** Phase 5.3 — last Hold enter deny (`ERR_EX1_FLAG_OFF` when kill switch). */
    const ex1HoldDenyCodeRef = useRef(null);
    const ex1OrbClickSkipRef = useRef(false);
    const nuAskNowOrbBtnRef = useRef(null);
    const [nuAskSttSupported] = useState(() => {
      try {
        return !!(navigator?.mediaDevices?.getUserMedia && window?.MediaRecorder);
      } catch (e) {
        return false;
      }
    });
    const nowRecoReqSeqRef = useRef(0);
    const nowRecoInFlightRef = useRef(false);
    /** Phase 4 Step 3/4 — latest ok evaluate body (lifecycle) + render packet. */
    const nowCiEvaluateResultRef = useRef(null);
    const nowCiEvaluateInFlightRef = useRef(false);
    const nowTraceRidRef = useRef(null);
    const nowTraceFinalEmittedRef = useRef(false);
    const nowTraceFinalFingerprintRef = useRef(null);
    const nowCiEvaluateDayKeyRef = useRef('');
    const nowCiAmbientGpsRetryDayKeyRef = useRef('');
    const [nowCiEvaluateResult, setNowCiEvaluateResult] = useState(null);
    const [nowCiTemporalRole, setNowCiTemporalRole] = useState('');
    /** #175: last NOW-open CI evaluate attempt latch (success, fail, or skip). GPS retry must not write this. */
    const [nowCiNowOpenAttemptKey, setNowCiNowOpenAttemptKey] = useState('');
    /** Last non-weather NOW CI bullets — survive a weather-only re-evaluate on latch change. */
    const [nowCiKeptEnrichment, setNowCiKeptEnrichment] = useState([]);
    const [nextSelectedSlot, setNextSelectedSlot] = useState('fa1');
    const [nextSelectedOccurrenceToken, setNextSelectedOccurrenceToken] = useState('');
    const nextOtherDayPrepRef = useRef({});
    const nextOtherDayInFlightRef = useRef({});
    const nextPreparationLanguageRef = useRef('en');
    const nextSelectedDayKeyRef = useRef('');
    const [nextOtherDayPrep, setNextOtherDayPrep] = useState(null);
    const nuAskNowDockRef = useRef(null);
    const [nuAskNowDockHeight, setNuAskNowDockHeight] = useState(0);
    const nowTopRecoScrollAnchorRef = useRef(null);
    /** NOW “TV” scroll surface — TOP reco / shells / loading fray scroll inside this only (corridor strip stays fixed). */
    const nowSectionScrollRef = useRef(null);
    const prevNowRecoLoadingRef = useRef(false);
    const wasNowRecoLoadingForRevealRef = useRef(false);

    /** Snap inner NOW scroll so the TOP stack (hero image + header) is flush to the top of the TV column. */
    const snapNowRecoScrollToTop = useCallback(() => {
      try {
        const wrap = nowSectionScrollRef.current;
        if (!wrap) return;
        if (typeof wrap.scrollTo === 'function') wrap.scrollTo({ top: 0, behavior: 'auto' });
        else wrap.scrollTop = 0;
      } catch {
        /* no-op */
      }
    }, []);

    const handleAlternativeRowClick = useCallback(
      (opt, fullListIdx, altRowIndex) => {
        const key = getRecoKey(opt, fullListIdx);
        setSelectedRecoKey(key);
        setExpandedAltRecoKey((prev) => (prev === key ? null : key));
        requestAnimationFrame(() => {
          requestAnimationFrame(() => snapNowRecoScrollToTop());
        });
      },
      [snapNowRecoScrollToTop]
    );

    const [nowRecoTravelerTags, setNowRecoTravelerTags] = useState([]);
    /** Last `/api/micro-action/preview` weather snapshot — feeds “Why this recommendation?”. */
    const [nowRecoWeatherContext, setNowRecoWeatherContext] = useState(null);
    /** Maps provider region from preview / device — CN → Gaode, GLOBAL → Google. null until known. */
    const [nowMapsRegion, setNowMapsRegion] = useState(null);
    const [nowNextCoords, setNowNextCoords] = useState(null);
    /** Device coords for travel math — updated by NOW geolocation poll and by successful refresh. */
    const [liveUserCoords, setLiveUserCoords] = useState(null);

    const resolveDeviceCoords = useCallback(async () => {
      try {
        const demoFn = typeof window !== 'undefined' ? window.SF_resolveDemoDeviceCoords : null;
        if (typeof demoFn === 'function') {
          const demo = demoFn(window.SylvanFlowState?.getProfile?.());
          if (demo && Number.isFinite(demo.lat) && Number.isFinite(demo.lng)) {
            return { lat: demo.lat, lng: demo.lng };
          }
        }
      } catch (e) {
        /* noop */
      }
      if (!navigator.geolocation) return null;
      try {
        const pos = await new Promise((resolve, reject) => {
          navigator.geolocation.getCurrentPosition(resolve, reject, {
            enableHighAccuracy: true,
            timeout: 10000,
            maximumAge: 120000
          });
        });
        return { lat: pos.coords.latitude, lng: pos.coords.longitude };
      } catch (e) {
        return null;
      }
    }, []);

    const nowClockRef = useRef(nowClock);
    const [nowCurrentStop, setNowCurrentStop] = useState(null);
    const nowNextCoordsRef = useRef(nowNextCoords);
    const travelLeaveAlertRef = useRef(null);
    const normalizePoiIdKey = useCallback((v) => asText(v).trim().toLowerCase(), []);
    const normalizeNameKey = useCallback((v) => {
      const s = asText(v).toLowerCase();
      // Canonicalize punctuation/spacing so "Malay Heritage Centre", "Malay-Heritage Centre", etc. match.
      return s.replace(/[^a-z0-9\u4e00-\u9fff]+/g, ' ').trim();
    }, []);
    const itineraryPoiIdSet = useMemo(() => {
      const out = new Set();
      const now = new Date();
      const y = offsetDayKeyLocal(now, -1);
      const t = offsetDayKeyLocal(now, 0);
      const tm = offsetDayKeyLocal(now, 1);
      for (const a of corridorActivities || []) {
        const day = asText(a?.__dayKey);
        if (day && day !== y && day !== t && day !== tm) continue;
        const pid = normalizePoiIdKey(
          a?.poi_id ||
          a?.poiId ||
          a?.catalog_poi_id ||
          a?.place_id ||
          a?.google_place_id ||
          a?.id
        );
        if (pid) out.add(pid);
      }
      return out;
    }, [corridorActivities, normalizePoiIdKey]);
    const itineraryNameSet = useMemo(() => {
      const out = new Set();
      const now = new Date();
      const y = offsetDayKeyLocal(now, -1);
      const t = offsetDayKeyLocal(now, 0);
      const tm = offsetDayKeyLocal(now, 1);
      for (const a of corridorActivities || []) {
        const day = asText(a?.__dayKey);
        if (day && day !== y && day !== t && day !== tm) continue;
        const n = normalizeNameKey(a?.title || a?.name || a?.poi_name || a?.location_name || '');
        if (n) out.add(n);
      }
      // Also suppress anything currently shown in corridor cards (covers race before activities refresh).
      [nowPrevCard?.title, nowNextCard?.title, nextAfterCard?.title].forEach((name) => {
        const n = normalizeNameKey(name);
        if (n) out.add(n);
      });
      return out;
    }, [corridorActivities, normalizeNameKey, nowPrevCard?.title, nowNextCard?.title, nextAfterCard?.title]);
    const engageEnRouteLock = useCallback((poiName, poiId, coords) => {
      setNowEnRouteLockPoiName(asText(poiName).trim());
      setNowEnRouteLockPoiId(normalizePoiIdKey(poiId));
      const c = pickRecoPoiCoords(coords) || pickRecoPoiCoords({ lat: coords?.lat, lng: coords?.lng });
      setNowEnRouteLockLat(c ? c.lat : null);
      setNowEnRouteLockLng(c ? c.lng : null);
      setNowEnRouteLockUntilMs(Date.now() + 30 * 60 * 1000);
      setNowRecoPersistUntilMs(Date.now() + 30 * 60 * 1000);
    }, [normalizePoiIdKey]);

    useEffect(() => {
      if (typeof window !== 'undefined' && window.SYLVAN_TECH_THEME?.ensureKeyframes) {
        window.SYLVAN_TECH_THEME.ensureKeyframes();
      }
    }, []);

    useEffect(() => {
      const loadNuAskStrings = async () => {
        const language = resolveNuAskPreferredLanguageCode();
        const defaults = buildNuAskUiStringBase(language);
        const loadMap =
          typeof window !== 'undefined' && Array.isArray(window.NUASK_I18N_LOAD_MAP)
            ? window.NUASK_I18N_LOAD_MAP
            : [];
        try {
          const pairs = await Promise.all(
            loadMap.map(async ([prop, key]) => {
              const v = await window.getSysMessage?.(key, language);
              return [prop, v];
            })
          );
          const next = { ...defaults };
          for (const [prop, v] of pairs) {
            if (v != null && String(v).trim() !== '') next[prop] = v;
          }
          // Stale SYS_MESSAGES_KV may still say "Hold to talk" / 按住说话 — force tap-toggle
          // copy from language-aware defaults (EN bootstrap + zh-CN pack), never English-only
          // NUASK_UI_DEFAULTS (that overwrote preferred-language UI).
          const voiceKeys = [
            'voiceTapToTalk',
            'voiceLimitHint',
            'voiceRecordingHint',
            'voiceTapStop',
            'voiceTooShort',
            'voiceKeepGoing',
            'voiceNoSpeech',
            'voiceMicDenied',
            'voiceBusy'
          ];
          for (const k of voiceKeys) {
            if (defaults[k]) next[k] = defaults[k];
          }
          // Stale SYS_MESSAGES_KV may still say "Replace-only" / use Replace Next — Mode B is Undo-first.
          const enRouteGoKeys = [
            'enRouteReplaceOnlyTitle',
            'enRouteReplaceOnlyBody',
            'enRouteCountdownTemplate'
          ];
          for (const k of enRouteGoKeys) {
            if (defaults[k]) next[k] = defaults[k];
          }
          const baked =
            typeof window !== 'undefined' && window.NUASK_UI_DEFAULTS ? window.NUASK_UI_DEFAULTS : null;
          if (baked) {
            for (const k of Object.keys(baked)) {
              if (voiceKeys.includes(k) || enRouteGoKeys.includes(k)) continue;
              const cur = next[k];
              if (cur == null || (typeof cur === 'string' && cur.trim() === '')) {
                next[k] = baked[k];
              }
            }
          }
          setNuAskUiStrings(next);
        } catch (e) {
          /* keep fallback strings */
        }
      };

      loadNuAskStrings();
      const onLang = () => loadNuAskStrings();
      const onProfile = () => loadNuAskStrings();
      if (window.SylvanFlowState?.on) {
        window.SylvanFlowState.on('language', onLang);
        window.SylvanFlowState.on('profile', onProfile);
        return () => {
          window.SylvanFlowState.off('language', onLang);
          window.SylvanFlowState.off('profile', onProfile);
        };
      }
      return undefined;
    }, []);

    useEffect(() => {
      if (nuAskExplainKey == null) return undefined;
      const onDown = (ev) => {
        const el = ev.target;
        if (!(el instanceof Element)) return;
        if (el.closest(`[data-nuask-explain-root="${nuAskExplainKey}"]`)) return;
        setNuAskExplainKey(null);
      };
      const onKey = (ev) => {
        if (ev.key === 'Escape') setNuAskExplainKey(null);
      };
      document.addEventListener('pointerdown', onDown, true);
      window.addEventListener('keydown', onKey);
      return () => {
        document.removeEventListener('pointerdown', onDown, true);
        window.removeEventListener('keydown', onKey);
      };
    }, [nuAskExplainKey]);

    useEffect(() => {
      nowClockRef.current = nowClock;
    }, [nowClock]);

    useEffect(() => {
      nowNextCoordsRef.current = nowNextCoords;
    }, [nowNextCoords]);

    /** Light geolocation while NOW is visible so travel-vs-window works without tapping sync first. */
    useEffect(() => {
      if (panel !== 1) return undefined;
      let cancelled = false;
      const read = () => {
        try {
          const demoFn = typeof window !== 'undefined' ? window.SF_resolveDemoDeviceCoords : null;
          if (typeof demoFn === 'function') {
            const demo = demoFn(window.SylvanFlowState?.getProfile?.());
            if (demo && Number.isFinite(demo.lat) && Number.isFinite(demo.lng)) {
              if (!cancelled) {
                setLiveUserCoords({ lat: demo.lat, lng: demo.lng });
                setNowMapsRegion((prev) => (prev != null ? prev : detectMapsRegion({ lat: demo.lat, lng: demo.lng })));
              }
              return;
            }
          }
        } catch (e) {
          /* noop */
        }
        if (!navigator.geolocation) return;
        navigator.geolocation.getCurrentPosition(
          (pos) => {
            if (cancelled) return;
            const next = { lat: pos.coords.latitude, lng: pos.coords.longitude };
            setLiveUserCoords(next);
            setNowMapsRegion((prev) => (prev != null ? prev : detectMapsRegion(next)));
          },
          () => {},
          { enableHighAccuracy: false, timeout: 12000, maximumAge: 60000 }
        );
      };
      read();
      const id = setInterval(read, 40000);
      return () => {
        cancelled = true;
        clearInterval(id);
      };
    }, [panel]);

    useEffect(() => {
      const computeTemporalCorridor = () => {
        const now = new Date();
        const ghostPastStop = {
          time: '—',
          title: nuAskUiStrings.panelPastTitle || 'PAST',
          sub: hasActiveTripContext
            ? (nuAskUiStrings.noLiveRecommendationYet || 'Standby')
            : (nuAskUiStrings.errNoTrip || 'No active trip linked'),
          icon: '◌',
          ghost: true
        };
        const ghostPastJourney = {
          time: '—',
          title: nuAskUiStrings.panelPastTitle || 'PAST',
          subtitle: hasActiveTripContext
            ? (nuAskUiStrings.noLiveRecommendationYet || 'Standby')
            : (nuAskUiStrings.errNoTrip || 'No active trip linked'),
          imageUrl: '',
          icon: '◌',
          ghost: true
        };
        const Temporal = typeof window !== 'undefined' ? window.NuAskTemporalRoles : null;
        const roles =
          Temporal && typeof Temporal.classifyCorridorActivities === 'function'
            ? Temporal.classifyCorridorActivities(corridorActivities, now)
            : { completed: [], current: null, latestStarted: null, future: [] };
        const completedOnly = roles.completed || [];
        const futureOnly = roles.future || [];
        setNowCiTemporalRole(nowCiTodayTemporalRole(roles, formatClientDayKeyLocal(now)));
        setNowCurrentStop(
          hasActiveTripContext ? buildNowCurrentStopCard(roles.current, corridorTripKey) : null
        );

        /** Do not fall back to in-progress/future rows when completed is empty — that mislabels them as PAST (Issue #93 / #178). */
        const visiblePast = completedOnly.slice(-3);

        setPlacesActivityCount(completedOnly.length);
        setPastStopsRow(buildPastStopsRow(visiblePast, ghostPastStop));
        setPastCardTargets(buildPastCardTargets(visiblePast, corridorTripKey));
        setPastJourneyItems(buildPastJourneyItems(visiblePast, ghostPastJourney));

        if (!hasActiveTripContext) {
          const standbyTitle = nuAskUiStrings.panelNowTitle || 'NOW';
          const standbySub = nuAskUiStrings.errNoTrip || 'No active trip linked';
          setNowPrevCard({ time: '—', title: standbyTitle, sub: standbySub, icon: '◌' });
          setNowNextCard({ time: '—', title: standbyTitle, sub: standbySub, icon: '◌' });
          setNextHighlightBody('');
          setNowNextImageUrl('');
          setNowNextCoords(null);
          setNextAfterCard({
            time: '—',
            title: nuAskUiStrings.panelNextTitle || 'NEXT',
            sub: standbySub,
            icon: '◌',
            imageUrl: ''
          });
          setNextAfterCardTarget(null);
          setHasUpcomingActivity(false);
          setNowNextHardBooking(false);
          setNowCorridorPrevStartMs(0);
          setNowCurrentStop(null);
          setNowClock({
            nowLabel: formatTimeLabel(now),
            endLabel: formatTimeLabel(now),
            rangeStartLabel: formatTimeLabel(now),
            mins: 0,
            minsUntilNextStart: 0
          });
          if (corridorHydratedRef.current && !corridorDataReadyRef.current) {
            corridorDataReadyRef.current = true;
            setCorridorDataReady(true);
            setCorridorCalmRevealKey((k) => k + 1);
          }
          return;
        }

        const prev = completedOnly.length > 0 ? completedOnly[completedOnly.length - 1] : null;
        const next = futureOnly.length > 0 ? futureOnly[0] : null;
        const lockRow = roles.latestStarted;
        const lockStartMs =
          lockRow && lockRow.__startDt ? lockRow.__startDt.getTime() : 0;
        setNowCorridorPrevStartMs(Number.isFinite(lockStartMs) && lockStartMs > 0 ? lockStartMs : 0);

        if (prev) {
          setNowPrevCard({
            time: formatActivityTimeLabel(prev),
            title: asText(prev.title) || asText(prev.Title) || DEMO.prevStop,
            sub:
              asText(prev.activity_type) ||
              asText(prev.Activity_Type) ||
              (getActivityLocationLabel(prev).split(',')[0] || 'Visited'),
            icon: pickActivityIcon(prev, 0)
          });
        } else {
          setNowPrevCard({
            time: '—',
            title: nuAskUiStrings.panelPastTitle || 'PAST',
            sub: nuAskUiStrings.noLiveRecommendationYet || 'Standby',
            icon: '◌'
          });
        }

        if (next) {
          setHasUpcomingActivity(true);
          setNowNextHardBooking(activityLooksHardCommitted(next));
          setNowNextCard({
            time: formatActivityTimeLabel(next),
            title: asText(next.title) || asText(next.Title) || DEMO.nextStop,
            sub:
              asText(next.activity_type) ||
              asText(next.Activity_Type) ||
              (getActivityLocationLabel(next).split(',')[0] || 'Next'),
            icon: pickActivityIcon(next, 1)
          });
          setNextHighlightBody(
            asText(next.rich_description) ||
              asText(next.Rich_Description) ||
              asText(next.description) ||
              asText(next.Description) ||
              ''
          );
          setNowNextImageUrl(getActivityImageUrl(next));
          setNowNextCoords(extractActivityCoords(next));
        } else {
          setHasUpcomingActivity(false);
          setNowNextHardBooking(false);
          setNowNextCard({
            time: '—',
            title: nuAskUiStrings.panelNowTitle || 'NOW',
            sub: nuAskUiStrings.noLiveRecommendationYet || 'No live recommendation yet',
            icon: '◌'
          });
          setNextHighlightBody('');
          setNowNextImageUrl('');
          setNowNextCoords(null);
        }

        const after = futureOnly.length > 1 ? futureOnly[1] : null;
        if (after) {
          setNextAfterCard({
            time: formatActivityTimeLabel(after),
            title: asText(after.title) || asText(after.Title) || DEMO.afterThat.title,
            sub:
              asText(after.activity_type) ||
              asText(after.Activity_Type) ||
              (getActivityLocationLabel(after).split(',')[0] || DEMO.afterThat.sub),
            icon: pickActivityIcon(after, 2),
            imageUrl: getActivityImageUrl(after)
          });
          setNextAfterCardTarget(
            corridorTripKey && after.__dayKey != null && after.__activityIndex != null
              ? {
                  tripKey: corridorTripKey,
                  dayKey: after.__dayKey,
                  activityIndex: after.__activityIndex,
                  activity: after
                }
              : null
          );
        } else {
          setNextAfterCard({
            time: '—',
            title: nuAskUiStrings.panelNextTitle || 'NEXT',
            sub: nuAskUiStrings.noLiveRecommendationYet || 'No live recommendation yet',
            icon: '◌',
            imageUrl: ''
          });
          setNextAfterCardTarget(null);
        }

        const aw =
          Temporal && typeof Temporal.computeGenuineAvailableWindow === 'function'
            ? Temporal.computeGenuineAvailableWindow({
                now,
                current: roles.current,
                nextFuture: next
              })
            : {
                mins: 0,
                endDt: now,
                rangeStartDt: now,
                wallClockMinsToNext: 0,
                hasKnownWindowEnd: false
              };
        const end = aw.endDt instanceof Date ? aw.endDt : now;
        const rangeStart = aw.rangeStartDt instanceof Date ? aw.rangeStartDt : now;
        setNowClock({
          nowLabel: formatTimeLabel(now),
          endLabel: formatTimeLabel(end),
          rangeStartLabel: formatTimeLabel(rangeStart),
          mins: Number.isFinite(Number(aw.mins)) ? Math.max(0, Math.round(Number(aw.mins))) : 0,
          minsUntilNextStart: Number.isFinite(Number(aw.wallClockMinsToNext))
            ? Math.max(0, Math.round(Number(aw.wallClockMinsToNext)))
            : 0
        });
        if (corridorHydratedRef.current && !corridorDataReadyRef.current) {
          corridorDataReadyRef.current = true;
          setCorridorDataReady(true);
          setCorridorCalmRevealKey((k) => k + 1);
        }
      };

      computeTemporalCorridor();
      const timer = setInterval(computeTemporalCorridor, 30000);
      return () => clearInterval(timer);
    }, [corridorActivities, corridorTripKey, hasActiveTripContext, nuAskUiStrings]);

    const refreshNowRecommendations = async () => {
      if (nowEnRouteLockUntilMs > Date.now()) {
        setNowRecoActionInfo(
          (nuAskUiStrings.enRouteReplaceOnlyTitle || 'On the way to {poi}.')
            .replace(/\{poi\}/g, nowEnRouteLockPoiName || asText(nowNextCard?.title) || (nuAskUiStrings.panelNowTitle || 'NOW'))
        );
        return;
      }
      const TemporalTap = typeof window !== 'undefined' ? window.NuAskTemporalRoles : null;
      const t = Date.now();
      const startMs = nowCorridorPrevStartMs;
      // Same 30m post-start lock as Add Here. Silent (no spinner / toast / copy). Grey orb is the signal.
      // Hold (HIS) is not gated here.
      const tapNowAllowed =
        TemporalTap && typeof TemporalTap.tapNowSidetripPreviewAllowed === 'function'
          ? TemporalTap.tapNowSidetripPreviewAllowed(startMs, t, NUASK_ACTIVITY_POST_START_ADD_LOCK_MS)
          : !(
              hasActiveTripContext &&
              Number.isFinite(startMs) &&
              startMs > 0 &&
              t >= startMs &&
              t < startMs + NUASK_ACTIVITY_POST_START_ADD_LOCK_MS
            );
      if (!tapNowAllowed) {
        return;
      }
      // Leave-soon / travel-only: sidetrip preview is a no-op. Same silent grey as 30m lock.
      if (travelLeaveAlertRef.current) {
        return;
      }
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        setNowRecoSyncPhase('ready');
        return;
      }
      if (nowRecoInFlightRef.current) return;

      const reqSeq = ++nowRecoReqSeqRef.current;
      nowRecoInFlightRef.current = true;
      setNowRecoLoading(true);
      setNowRecoError('');
      setNowRecoActionError('');
      setNowRecoActionInfo('');
      setNowRecoWeatherContext(null);

      const getCoords = resolveDeviceCoords;

      try {
        const coords = await getCoords();
        if (!coords) {
          setShowLocationPrompt(true);
          throw new Error(nuAskUiStrings.errLocationRequired || 'Live location is required. Please enable location and refresh.');
        }
        setShowLocationPrompt(false);
        setLiveUserCoords(coords);
        setNowMapsRegion(detectMapsRegion(coords));

        const nextAct = nowNextCoordsRef.current;
        const clock = nowClockRef.current;
        if (nextAct && clock && Number(clock.minsUntilNextStart) > 0) {
          const kmEdge = haversineKm(coords.lat, coords.lng, nextAct.lat, nextAct.lng);
          if (kmEdge != null) {
            const travelEdgeMin = roughUrbanDriveMinutes(kmEdge);
            if (travelEdgeMin >= Number(clock.minsUntilNextStart) - 2) {
              setNowRecoOptions([]);
              setNowRecoMicroActionId('');
              setSelectedRecoKey(null);
              setNowRecoTravelerTags([]);
              setNowRecoWeatherContext(null);
              return;
            }
          }
        }

        const now = new Date();
        const body = {
          coords,
          latitude: coords.lat,
          longitude: coords.lng,
          location: { lat: coords.lat, lng: coords.lng },
          intent: 'what do i do now?',
          client_day_key: formatClientDayKeyLocal(now),
          client_local_hhmm: formatClientLocalHHMM(now),
          client_weekday: now.getDay(),
          context: {
            now_local: nowClock.nowLabel,
            available_window_mins: nowClock.mins,
            previous_activity: nowPrevCard.title,
            next_activity: nowNextCard.title
          }
        };

        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/micro-action/preview`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body)
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();
        if (!res.ok || !data?.ok)
          throw new Error(data?.error || nuAskUiStrings.errLoadRecommendations || 'Unable to load recommendations');
        if (reqSeq !== nowRecoReqSeqRef.current) return;
        const nextAfter = nowNextCoordsRef.current;
        const clockAfter = nowClockRef.current;
        if (nextAfter && clockAfter && clockAfter.mins > 0 && coords) {
          const kmLate = haversineKm(coords.lat, coords.lng, nextAfter.lat, nextAfter.lng);
          if (kmLate != null && roughUrbanDriveMinutes(kmLate) >= clockAfter.mins - 2) {
            setNowRecoOptions([]);
            setNowRecoMicroActionId('');
            setSelectedRecoKey(null);
            setNowRecoTravelerTags([]);
            setNowRecoWeatherContext(null);
            return;
          }
        }
        const rawOptions = Array.isArray(data.options) ? data.options.slice(0, 6) : [];
        const lockName = normalizeNameKey(nowEnRouteLockPoiName);
        const lockPoiId = normalizePoiIdKey(nowEnRouteLockPoiId);
        let options = rawOptions
          .filter((opt) => {
            const pid = normalizePoiIdKey(
              opt?.poi?.poi_id ||
              opt?.poi?.id ||
              opt?.poi?.catalog_poi_id ||
              opt?.poi?.place_id ||
              opt?.poi?.google_place_id ||
              opt?.poi_id
            );
            if (pid) {
              if (lockPoiId && pid === lockPoiId) return false;
              if (itineraryPoiIdSet.has(pid)) return false;
            }
            const name = normalizeNameKey(opt?.poi?.name || opt?.summary || '');
            if (!name) return true;
            if (lockName && name === lockName) return false;
            if (itineraryNameSet.has(name)) return false;
            return true;
          })
          .slice(0, 3);
        if (data?.envelope?.first_add === true && options.length > 1) {
          options = [...options].sort((a, b) => {
            const da = Number(a?.poi?.distance_km);
            const db = Number(b?.poi?.distance_km);
            const na = Number.isFinite(da) ? da : 1e9;
            const nb = Number.isFinite(db) ? db : 1e9;
            return na - nb;
          });
        }
        setNowRecoOptions(options);
        setNowRecoPersistUntilMs(Date.now() + 360000);
        setNowRecoMicroActionId(asText(data.micro_action_id) || '');
        setSelectedRecoKey(options.length > 0 ? getRecoKey(options[0], 0) : null);
        setNowRecoTravelerTags(Array.isArray(data.traveler_tags_used) ? data.traveler_tags_used : []);
        setNowRecoWeatherContext(data.weather_context && typeof data.weather_context === 'object' ? data.weather_context : null);
        setNowMapsRegion(normalizeMapsRegion(data.region));
        setLiveUserCoords(coords);
      } catch (e) {
        if (reqSeq !== nowRecoReqSeqRef.current) return;
        setNowRecoError(e?.message || nuAskUiStrings.errLoadRecommendations || 'Unable to load recommendations');
        setNowRecoOptions([]);
        setNowRecoPersistUntilMs(0);
        setNowRecoMicroActionId('');
        setSelectedRecoKey(null);
        setNowRecoTravelerTags([]);
        setNowRecoWeatherContext(null);
      } finally {
        if (reqSeq === nowRecoReqSeqRef.current) {
          setNowRecoLoading(false);
          setNowRecoSyncPhase('ready');
        }
        nowRecoInFlightRef.current = false;
      }
    };

    const clearNowHotbarDetail = useCallback(() => {
      setNowHotbarDetail(null);
    }, []);

    const closeNearbyPanel = useCallback(() => {
      setNowNearbyOpen(false);
      setNowNearbyPhase('idle');
      setNowNearbyPois([]);
      setNowNearbyError('');
    }, []);

    const closeWeatherPlanPanel = useCallback(() => {
      setNowWeatherPlanOpen(false);
      setNowWeatherPlanPhase('idle');
      setNowWeatherPlanData(null);
      setNowWeatherPlanError('');
    }, []);

    const closeUseMyTimePanel = useCallback(() => {
      setNowUseMyTimeOpen(false);
      setNowUseMyTimePhase('idle');
      setNowUseMyTimeError('');
      setNowUseMyTimeRows([]);
    }, []);

    /** POST /api/nuask/weather-day-suggest — same calendar day; needs trip_key + location. */
    const readStoredChatId = useCallback(() => {
      try {
        const s = localStorage.getItem('sylvanflow_chat_id');
        if (s && typeof s === 'string' && s.startsWith('chat:')) return s;
      } catch (e) {
        /* ignore */
      }
      return null;
    }, []);

    const logNuAskChatEvent = useCallback(
      async (eventType, acknowledgement, meta = {}) => {
        if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) return;
        const eventId = `${eventType}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
        const rid = typeof meta?.rid === 'string' ? meta.rid.trim() : '';
        try {
          const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/chat/event`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              ...(rid ? { 'x-request-id': rid } : {})
            },
            body: JSON.stringify({
              chat_id: readStoredChatId(),
              event_type: eventType,
              event_id: eventId,
              acknowledgement,
              meta
            })
          });
          window.SylvanFlowAuth.handleAuthResponse(res);
        } catch (_) {
          // Non-blocking audit write.
        }
      },
      [SF_API_BASE, readStoredChatId]
    );

    const openWeatherDaySuggestFromNowHotbar = useCallback(async () => {
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        setNowWeatherPlanOpen(true);
        setNowWeatherPlanPhase('error');
        setNowWeatherPlanError(nuAskUiStrings.errApiUnavailable || 'API not available.');
        return;
      }
      if (!corridorMutationTripKey) {
        setNowWeatherPlanOpen(true);
        setNowWeatherPlanPhase('error');
        setNowWeatherPlanError(nuAskUiStrings.errNoTrip || '');
        return;
      }
      setNowHotbarDetail(null);
      setNowNearbyOpen(false);
      setNowNearbyPhase('idle');
      setNowNearbyPois([]);
      setNowNearbyError('');
      setNowUseMyTimeOpen(false);
      setNowUseMyTimePhase('idle');
      setNowUseMyTimeRows([]);
      setNowUseMyTimeError('');
      setNowWeatherPlanOpen(true);
      setNowWeatherPlanPhase('loading');
      setNowWeatherPlanError('');
      setNowWeatherPlanData(null);
      setNowWeatherApplyPhase('idle');
      setNowWeatherApplyError('');
      void logNuAskChatEvent('nuask_hotbar_improve_plan', 'NuAsk hotbar: Improve Plan.', { panel });

      const getCoords = resolveDeviceCoords;

      let coords = liveUserCoords;
      if (!coords || typeof coords.lat !== 'number') {
        coords = await getCoords();
      }
      if (!coords) {
        setShowLocationPrompt(true);
        setNowWeatherPlanPhase('error');
        setNowWeatherPlanError(nuAskUiStrings.errLocationForWeather || 'Turn on location for weather and timing.');
        return;
      }
      setShowLocationPrompt(false);
      setLiveUserCoords(coords);
      setNowMapsRegion((prev) => (prev != null ? prev : detectMapsRegion(coords)));

      try {
        const now = new Date();
        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/nuask/weather-day-suggest`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            coords,
            trip_key: corridorMutationTripKey,
            client_day_key: formatClientDayKeyLocal(now),
            client_local_hhmm: formatClientLocalHHMM(now),
            horizon_hours: 6
          })
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();
        if (!res.ok || data.ok !== true) throw new Error(data?.error || 'Request failed');
        setNowWeatherPlanData(data);
        setNowWeatherPlanPhase('ready');
      } catch (e) {
        setNowWeatherPlanPhase('error');
        setNowWeatherPlanError(e?.message || nuAskUiStrings.errWeatherAnalyze || 'Could not analyze weather vs plan.');
      }
    }, [SF_API_BASE, liveUserCoords, corridorMutationTripKey, nuAskUiStrings, logNuAskChatEvent, panel]);

    const applyWeatherDayOrder = useCallback(
      async (shifts) => {
        if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
          setNowWeatherApplyPhase('error');
          setNowWeatherApplyError(nuAskUiStrings.errApiUnavailable || 'API not available.');
          return;
        }
        if (!corridorMutationTripKey) {
          setNowWeatherApplyPhase('error');
          setNowWeatherApplyError(
            nuAskUiStrings.errWeatherApplyNoTrip || 'No today Flow Plan to update.'
          );
          return;
        }
        const list = Array.isArray(shifts) ? shifts : [];
        if (list.length < 1) return;
        setNowWeatherApplyPhase('loading');
        setNowWeatherApplyError('');
        try {
          const res = await window.SylvanFlowAuth.authenticatedFetch(
            `${SF_API_BASE}/api/nuask/weather-day-apply`,
            {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                trip_key: corridorMutationTripKey,
                client_day_key: formatClientDayKeyLocal(new Date()),
                shifts: list
              })
            }
          );
          window.SylvanFlowAuth.handleAuthResponse(res);
          const data = await res.json();
          if (!res.ok || data.ok !== true) {
            throw new Error(data?.error || data?.message || 'Apply failed');
          }
          setNowWeatherApplyPhase('done');
          void logNuAskChatEvent('nuask_weather_day_apply', 'NuAsk Improve Plan: applied soft-stop order.', {
            panel,
            shifts: data.shifts_applied || list.length
          });
          setCorridorReloadTick((t) => t + 1);        } catch (e) {
          setNowWeatherApplyPhase('error');
          setNowWeatherApplyError(e?.message || 'Could not apply order.');
        }
      },
      [SF_API_BASE, corridorMutationTripKey, nuAskUiStrings, logNuAskChatEvent, panel]
    );

    /** TOP-slot ~5 POIs: nearby + traveler personality via `nearby_personality_focus` on POST /api/nuask/nearby-pois. */
    const openNearbyFromNowHotbar = useCallback(async () => {
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        setNowHotbarDetail(null);
        setNowNearbyOpen(true);
        setNowNearbyPhase('error');
        setNowNearbyError(nuAskUiStrings.errApiUnavailable || 'API not available.');
        return;
      }
      setNowHotbarDetail(null);
      setNowWeatherPlanOpen(false);
      setNowWeatherPlanPhase('idle');
      setNowWeatherPlanData(null);
      setNowWeatherPlanError('');
      setNowUseMyTimeOpen(false);
      setNowUseMyTimePhase('idle');
      setNowUseMyTimeRows([]);
      setNowUseMyTimeError('');
      setNowNearbyOpen(true);
      setNowNearbyPhase('loading');
      setNowNearbyError('');
      setNowNearbyPois([]);
      void logNuAskChatEvent('nuask_hotbar_explore_nearby', 'NuAsk hotbar: Explore Nearby.', { panel });

      const getCoords = resolveDeviceCoords;

      let coords = liveUserCoords;
      if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
        coords = await getCoords();
      }
      if (!coords) {
        setShowLocationPrompt(true);
        setNowNearbyPhase('error');
        setNowNearbyError(nuAskUiStrings.errLocationNearby || 'Turn on location to see nearby catalog POIs.');
        return;
      }
      setShowLocationPrompt(false);
      setLiveUserCoords(coords);
      setNowMapsRegion((prev) => (prev != null ? prev : detectMapsRegion(coords)));

      try {
        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/nuask/nearby-pois`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            coords,
            limit: 5,
            nearby_personality_focus: true,
            client_day_key: formatClientDayKeyLocal(new Date()),
            ...(corridorMutationTripKey ? { trip_key: corridorMutationTripKey } : {})
          })
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();
        if (!res.ok || data.ok !== true) throw new Error(data?.error || 'Request failed');
        setNowNearbyPois(Array.isArray(data.pois) ? data.pois : []);
        setNowNearbyPhase('ready');
      } catch (e) {
        setNowNearbyPhase('error');
        setNowNearbyError(e?.message || nuAskUiStrings.errLoadNearby || 'Could not load nearby POIs.');
      }
    }, [SF_API_BASE, liveUserCoords, corridorMutationTripKey, nuAskUiStrings, logNuAskChatEvent, panel]);

    /** Use My Time: same catalog nearby fetch as Explore Nearby, scored vs **Available Window** (gap-fit). */
    const openUseMyTimeFromNowHotbar = useCallback(async () => {
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        setNowUseMyTimeOpen(true);
        setNowUseMyTimePhase('error');
        setNowUseMyTimeError(nuAskUiStrings.errApiUnavailable || 'API not available.');
        return;
      }
      setNowHotbarDetail(null);
      setNowNearbyOpen(false);
      setNowNearbyPhase('idle');
      setNowNearbyPois([]);
      setNowNearbyError('');
      setNowWeatherPlanOpen(false);
      setNowWeatherPlanPhase('idle');
      setNowWeatherPlanData(null);
      setNowWeatherPlanError('');
      setNowUseMyTimeOpen(true);
      setNowUseMyTimePhase('loading');
      setNowUseMyTimeError('');
      setNowUseMyTimeRows([]);
      void logNuAskChatEvent('nuask_hotbar_use_my_time', 'NuAsk hotbar: Use My Time.', { panel });

      const windowMins = Math.max(0, Math.round(Number(nowClockRef.current?.mins) || 0));

      const getCoords = resolveDeviceCoords;

      let coords = liveUserCoords;
      if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
        coords = await getCoords();
      }
      if (!coords) {
        setShowLocationPrompt(true);
        setNowUseMyTimePhase('error');
        setNowUseMyTimeError(nuAskUiStrings.errLocationNearby || 'Turn on location to score places against your window.');
        return;
      }
      setShowLocationPrompt(false);
      setLiveUserCoords(coords);
      setNowMapsRegion((prev) => (prev != null ? prev : detectMapsRegion(coords)));

      try {
        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/nuask/nearby-pois`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            coords,
            limit: 8,
            client_day_key: formatClientDayKeyLocal(new Date()),
            ...(corridorMutationTripKey ? { trip_key: corridorMutationTripKey } : {})
          })
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();
        if (!res.ok || data.ok !== true) throw new Error(data?.error || 'Request failed');
        const pois = Array.isArray(data.pois) ? data.pois : [];
        const rows = buildGapFitRows(pois, windowMins);
        setNowUseMyTimeRows(rows);
        setNowUseMyTimePhase('ready');
      } catch (e) {
        setNowUseMyTimePhase('error');
        setNowUseMyTimeError(e?.message || nuAskUiStrings.errLoadNearby || 'Could not load POIs for your window.');
      }
    }, [SF_API_BASE, liveUserCoords, corridorMutationTripKey, nuAskUiStrings, logNuAskChatEvent, panel]);

    /** Mirrors Ask Sylvan `sendMessage` for exact `EMERGENCY` intent + chat_id + coords + languageCode. */
    const startEmergencyFromNowHotbar = useCallback(async () => {
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        setNowHotbarDetail({
          kind: 'emergency',
          phase: 'error',
          errorMessage: nuAskUiStrings.errApiUnavailable || 'API not available.'
        });
        return;
      }
      setNowNearbyOpen(false);
      setNowNearbyPhase('idle');
      setNowNearbyPois([]);
      setNowNearbyError('');
      setNowWeatherPlanOpen(false);
      setNowWeatherPlanPhase('idle');
      setNowWeatherPlanData(null);
      setNowWeatherPlanError('');
      setNowUseMyTimeOpen(false);
      setNowUseMyTimePhase('idle');
      setNowUseMyTimeRows([]);
      setNowUseMyTimeError('');
      setNowHotbarDetail({ kind: 'emergency', phase: 'loading' });
      void logNuAskChatEvent('nuask_hotbar_emergency', 'NuAsk hotbar emergency triggered.', { panel });

      const getCoords = async () => {
        const base = await resolveDeviceCoords();
        if (base) return { ...base, accuracy: 25 };
        if (!navigator.geolocation) return null;
        try {
          const pos = await new Promise((resolve, reject) => {
            navigator.geolocation.getCurrentPosition(resolve, reject, {
              enableHighAccuracy: true,
              timeout: 10000,
              maximumAge: 120000
            });
          });
          return {
            lat: pos.coords.latitude,
            lng: pos.coords.longitude,
            accuracy: pos.coords.accuracy
          };
        } catch (e) {
          return null;
        }
      };

      try {
        const coords = await getCoords();
        if (!coords) {
          setNowHotbarDetail({
            kind: 'emergency',
            phase: 'error',
            errorMessage: nuAskUiStrings.errLocationRequired || 'Turn on location to load emergency services near you.'
          });
          return;
        }
        setLiveUserCoords(coords);
        setNowMapsRegion((prev) => (prev != null ? prev : detectMapsRegion(coords)));
        const languageCode = resolveNuAskPreferredLanguageCode();
        const requestBody = {
          message: 'EMERGENCY',
          chat_id: readStoredChatId(),
          languageCode,
          latitude: coords.lat,
          longitude: coords.lng,
          location: {
            lat: coords.lat,
            lng: coords.lng,
            ...(typeof coords.accuracy === 'number' && Number.isFinite(coords.accuracy)
              ? { accuracy: coords.accuracy }
              : {})
          }
        };

        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/chat/send`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(requestBody)
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();

        if (!res.ok || data.ok !== true) {
          setNowHotbarDetail({
            kind: 'emergency',
            phase: 'error',
            errorMessage: data?.error || data?.message || nuAskUiStrings.emergencyRequestFailed || 'Emergency request failed.'
          });
          return;
        }

        if (data.chat_id && typeof data.chat_id === 'string') {
          try {
            localStorage.setItem('sylvanflow_chat_id', data.chat_id);
          } catch (e) {
            /* ignore */
          }
        }

        const emergencyId = data.ai?.emergency_id || data.emergency_id;
        if (!emergencyId || String(emergencyId).trim() === '') {
          const hint =
            typeof data.ai_reply === 'string' && data.ai_reply ? data.ai_reply.slice(0, 280) : '';
          setNowHotbarDetail({
            kind: 'emergency',
            phase: 'error',
            errorMessage: hint || nuAskUiStrings.emergencyRequestFailed || 'No emergency session id returned. Try Ask Sylvan chat.'
          });
          return;
        }

        setNowHotbarDetail({
          kind: 'emergency',
          phase: 'ready',
          emergencyId: String(emergencyId).trim(),
          languageCode
        });
      } catch (e) {
        setNowHotbarDetail({
          kind: 'emergency',
          phase: 'error',
          errorMessage: e?.message || nuAskUiStrings.emergencyRequestFailed || 'Could not reach the server.'
        });
      }
    }, [SF_API_BASE, nuAskUiStrings, logNuAskChatEvent, panel, readStoredChatId]);

    const scheduleNuAskLatestHide = useCallback(() => {
      if (nuAskLatestHideTimerRef.current) {
        clearTimeout(nuAskLatestHideTimerRef.current);
        nuAskLatestHideTimerRef.current = null;
      }
      if (nuAskLatestClearTimerRef.current) {
        clearTimeout(nuAskLatestClearTimerRef.current);
        nuAskLatestClearTimerRef.current = null;
      }
      nuAskLatestHideTimerRef.current = setTimeout(() => {
        setNuAskLatestVisible(false);
      }, NUASK_LATEST_TTL_MS);
      nuAskLatestClearTimerRef.current = setTimeout(() => {
        setNuAskLatestMounted(false);
      }, NUASK_LATEST_TTL_MS + NUASK_LATEST_FADE_MS);
    }, [NUASK_LATEST_FADE_MS, NUASK_LATEST_TTL_MS]);

    const showNuAskLatestCard = useCallback(
      (nextText) => {
        const normalized = plainTextFromChatMarkdown(typeof nextText === 'string' ? nextText.trim() : '');
        if (normalized) setNuAskLastAssistantText(normalized);
        const hasText = normalized || nuAskLastAssistantText;
        if (!hasText) return;
        setNuAskLatestMounted(true);
        setNuAskLatestVisible(true);
        setNuAskLatestHolo(true);
        scheduleNuAskLatestHide();
      },
      [nuAskLastAssistantText, scheduleNuAskLatestHide]
    );

    const touchNuAskChatBox = useCallback(() => {
      if (!nuAskLastAssistantText) return;
      setNuAskLatestMounted(true);
      setNuAskLatestVisible(true);
      setNuAskLatestHolo(true);
      scheduleNuAskLatestHide();
    }, [nuAskLastAssistantText, scheduleNuAskLatestHide]);

    useEffect(() => {
      if (!nuAskLastAssistantText) setNuAskLatestHolo(false);
    }, [nuAskLastAssistantText]);

    const dismissNuAskLatestHolo = useCallback(() => setNuAskLatestHolo(false), []);

    const clearNuAskLatestReply = useCallback(() => {
      setNuAskLastAssistantText('');
      setNuAskLatestVisible(false);
      setNuAskLatestMounted(false);
      setNuAskLatestHolo(false);
      if (nuAskLatestHideTimerRef.current) {
        clearTimeout(nuAskLatestHideTimerRef.current);
        nuAskLatestHideTimerRef.current = null;
      }
      if (nuAskLatestClearTimerRef.current) {
        clearTimeout(nuAskLatestClearTimerRef.current);
        nuAskLatestClearTimerRef.current = null;
      }
    }, []);

    const copyNuAskLatestReply = useCallback(async () => {
      const t = nuAskLastAssistantText;
      if (!t || typeof navigator === 'undefined' || !navigator.clipboard?.writeText) return;
      try {
        await navigator.clipboard.writeText(t);
      } catch (e) {
        /* noop */
      }
    }, [nuAskLastAssistantText]);

    const openNowFullChatInTv = useCallback(() => {
      setNowTvAskOpen(true);
    }, []);

    const closeNowTvAsk = useCallback(() => {
      setNowTvAskOpen(false);
    }, []);

    useEffect(() => {
      if (panel !== 1 || !nuAskLatestHolo || !nuAskLastAssistantText) return undefined;
      const onKey = (e) => {
        if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
        const el = e.target;
        if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) return;
        const k = e.key;
        if (k === '1') {
          e.preventDefault();
          dismissNuAskLatestHolo();
        } else if (k === '2') {
          e.preventDefault();
          openNowFullChatInTv();
        } else if (k === '3') {
          e.preventDefault();
          void copyNuAskLatestReply();
        } else if (k === '4') {
          e.preventDefault();
          clearNuAskLatestReply();
        }
      };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [
      panel,
      nuAskLatestHolo,
      nuAskLastAssistantText,
      dismissNuAskLatestHolo,
      copyNuAskLatestReply,
      clearNuAskLatestReply,
      openNowFullChatInTv
    ]);

    /** Same `/api/chat/status` polling contract as `AskSylvanPage.waitForAssistantReply`; updates NuAsk last-reply only. */
    const waitForNuAskAssistantReply = useCallback(
      async ({ timeoutMs = 12000, intervalMs = 1500, sinceTimestamp } = {}) => {
        const start = Date.now();
        while (Date.now() - start < timeoutMs) {
          let statusUrl = `${SF_API_BASE}/api/chat/status`;
          if (sinceTimestamp) statusUrl += `?last_message_time=${encodeURIComponent(sinceTimestamp)}`;
          const res = await window.SylvanFlowAuth.authenticatedFetch(statusUrl);
          window.SylvanFlowAuth.handleAuthResponse(res);
          if (res.ok) {
            const data = await res.json();
            if (data?.ok && Array.isArray(data?.new_messages) && data.new_messages.length > 0) {
              const additions = data.new_messages
                .filter((m) => m.type === 'assistant' || m.ai_reply)
                .map((m) => ({
                  text: m.text || m.ai_reply,
                  timestamp: m.timestamp || m.created_at_utc || new Date().toISOString()
                }));
              if (additions.length) {
                const last = additions[additions.length - 1];
                const t = last.text != null ? String(last.text) : '';
                if (t) return t;
              }
            }
          }
          await new Promise((r) => setTimeout(r, intervalMs));
        }
        return null;
      },
      [SF_API_BASE]
    );

    const sendNuAskInlineChat = useCallback(async () => {
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        setNuAskInlineError(nuAskUiStrings.errApiUnavailable || 'Chat is unavailable.');
        return;
      }
      if (nuAskInlineSendingRef.current) return;
      const text = nuAskInlineInputRef.current.trim().slice(0, NUASK_INLINE_MAX_LEN);
      if (!text) return;

      nuAskInlineSendingRef.current = true;
      setNuAskInlineSending(true);
      setNuAskInlineError('');
      nuAskInlineInputRef.current = '';
      setNuAskInlineInput('');

      const userTimestamp = new Date().toISOString();
      const languageCode = resolveNuAskPreferredLanguageCode();
      const chatId = readStoredChatId();

      const requestBody = {
        message: text,
        chat_id: chatId || null,
        languageCode,
        chat_mode: 'nuask_info_only'
      };

      if (
        liveUserCoords &&
        typeof liveUserCoords.lat === 'number' &&
        Number.isFinite(liveUserCoords.lat) &&
        typeof liveUserCoords.lng === 'number' &&
        Number.isFinite(liveUserCoords.lng)
      ) {
        requestBody.latitude = liveUserCoords.lat;
        requestBody.longitude = liveUserCoords.lng;
        requestBody.location = {
          lat: liveUserCoords.lat,
          lng: liveUserCoords.lng,
          ...(typeof liveUserCoords.accuracy === 'number' && Number.isFinite(liveUserCoords.accuracy)
            ? { accuracy: liveUserCoords.accuracy }
            : {})
        };
        if (window.uiLogger) {
          window.uiLogger.uiLog('[NUASK_INLINE] send with location', {
            hasAccuracy: typeof liveUserCoords.accuracy === 'number'
          });
        }
      }

      try {
        if (window.uiLogger) {
          window.uiLogger.uiLog('[NUASK_INLINE] send start', {
            messageLength: text.length,
            hasChatId: !!chatId,
            hasLocation: !!requestBody.location
          });
        }

        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/chat/send`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(requestBody)
        });
        window.SylvanFlowAuth.handleAuthResponse(res);

        let data;
        try {
          data = await res.json();
        } catch (jsonError) {
          if (window.uiLogger) {
            window.uiLogger.uiError('[NUASK_INLINE] JSON parse error', { message: jsonError?.message });
          }
          const invalidResponseMsg =
            (typeof window !== 'undefined' && window.getSysMessage
              ? await window.getSysMessage('ui:invalid_response')
              : null) || 'Sorry, I received an invalid response. Please try again.';
          setNuAskInlineError(invalidResponseMsg);
          return;
        }

        if (!res.ok || data.ok === false) {
          const errText = data?.message || data?.error || 'Could not send message.';
          setNuAskInlineError(errText);
          return;
        }

        if (data.ok !== true) {
          const unexpectedResponseMsg =
            (typeof window !== 'undefined' && window.getSysMessage
              ? await window.getSysMessage('ui:unexpected_response')
              : null) || 'Sorry, I received an unexpected response. Please try again.';
          setNuAskInlineError(unexpectedResponseMsg);
          return;
        }

        if (data.chat_id && typeof data.chat_id === 'string') {
          try {
            localStorage.setItem('sylvanflow_chat_id', data.chat_id);
          } catch (e) {
            /* ignore */
          }
        }

        if (typeof data.ai_reply === 'string' && data.ai_reply.trim() !== '') {
          const nextLatest = data.ai_reply.trim();
          showNuAskLatestCard(nextLatest);
          return;
        }

        setNuAskInlineTyping(true);
        const polled = await waitForNuAskAssistantReply({
          timeoutMs: 12000,
          intervalMs: 1500,
          sinceTimestamp: userTimestamp
        });
        setNuAskInlineTyping(false);
        if (polled) {
          const nextLatest = polled.trim();
          showNuAskLatestCard(nextLatest);
        } else {
          const timeoutMessage =
            (typeof window !== 'undefined' && window.getSysMessage
              ? await window.getSysMessage('ui:chat_timeout')
              : null) || "Sylvan didn't respond. Please try again.";
          setNuAskInlineError(timeoutMessage);
        }
      } catch (e) {
        if (window.uiLogger) {
          window.uiLogger.uiWarn('[NUASK_INLINE] send failed', { message: e?.message });
        }
        setNuAskInlineError(e?.message || nuAskUiStrings.errApiUnavailable || 'Could not reach the server.');
      } finally {
        nuAskInlineSendingRef.current = false;
        setNuAskInlineSending(false);
        setNuAskInlineTyping(false);
      }
    }, [
      SF_API_BASE,
      liveUserCoords,
      readStoredChatId,
      waitForNuAskAssistantReply,
      nuAskUiStrings,
      showNuAskLatestCard
    ]);

    const cleanupNuAskSttCapture = useCallback(() => {
      if (nuAskSttTimeoutRef.current) {
        clearTimeout(nuAskSttTimeoutRef.current);
        nuAskSttTimeoutRef.current = null;
      }
      const stream = nuAskSttStreamRef.current;
      if (stream) {
        for (const track of stream.getTracks()) {
          try {
            track.stop();
          } catch (e) {
            /* noop */
          }
        }
      }
      nuAskSttStreamRef.current = null;
      nuAskSttRecorderRef.current = null;
      nuAskSttStartingRef.current = false;
    }, []);

    const transcribeNuAskVoiceBlob = useCallback(
      async (blob, durationMs) => {
        if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
          setNuAskInlineError(nuAskUiStrings.errApiUnavailable || 'Voice input is unavailable.');
          setNuAskSttPhase('idle');
          return;
        }
        try {
          setNuAskInlineError('');
          setNuAskSttPhase('transcribing');
          const arr = await blob.arrayBuffer();
          if (arr.byteLength > NUASK_STT_MAX_BYTES) {
            setNuAskInlineError(
              nuAskUiStrings.voiceClipTooLarge || 'Voice clip too large. Keep clips under 20 seconds.'
            );
            setNuAskSttPhase('idle');
            return;
          }
          const audioBase64 = arrayBufferToBase64(arr);
          const sttLanguageCode = resolveNuAskPreferredLanguageCode();
          const sttBody = {
            audio_base64: audioBase64,
            mime_type: blob.type || 'audio/webm',
            duration_ms: durationMs,
            languageCode: sttLanguageCode
          };
          // GPS helps server soft-correct garbled local place names (non-identity).
          if (
            liveUserCoords &&
            typeof liveUserCoords.lat === 'number' &&
            Number.isFinite(liveUserCoords.lat) &&
            typeof liveUserCoords.lng === 'number' &&
            Number.isFinite(liveUserCoords.lng)
          ) {
            sttBody.latitude = liveUserCoords.lat;
            sttBody.longitude = liveUserCoords.lng;
          }
          const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/stt/transcribe`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(sttBody)
          });
          window.SylvanFlowAuth.handleAuthResponse(res);
          const data = await res.json().catch(() => ({}));
          if (!res.ok || data?.ok !== true) {
            const errText =
              data?.error ||
              data?.reason ||
              data?.message ||
              (nuAskUiStrings.errApiUnavailable || 'Could not transcribe voice.');
            setNuAskInlineError(errText);
            setNuAskSttPhase('idle');
            return;
          }
          const transcript = typeof data?.text === 'string' ? data.text.trim() : '';
          if (!transcript) {
            setNuAskInlineError(nuAskUiStrings.voiceNoSpeech || 'No speech detected. Please try again.');
            setNuAskSttPhase('idle');
            return;
          }
          const clipped = plainTextFromChatMarkdown(transcript.slice(0, NUASK_INLINE_MAX_LEN));
          nuAskInlineInputRef.current = clipped;
          setNuAskInlineInput(clipped);
          touchNuAskChatBox();
          setNuAskSttPhase('idle');
          void sendNuAskInlineChat();
        } catch (e) {
          setNuAskInlineError(e?.message || nuAskUiStrings.errApiUnavailable || 'Could not transcribe voice.');
          setNuAskSttPhase('idle');
        }
      },
      [SF_API_BASE, nuAskUiStrings, sendNuAskInlineChat, touchNuAskChatBox, liveUserCoords]
    );

    const stopNuAskVoiceCapture = useCallback(() => {
      const activeRecorder = nuAskSttRecorderRef.current;
      if (!activeRecorder || activeRecorder.state !== 'recording') return;
      const elapsed = Date.now() - (nuAskSttStartedAtRef.current || Date.now());
      // Too-soon stop: keep recording (tap-toggle) instead of aborting into a false "too short".
      if (elapsed < NUASK_STT_MIN_MS || Date.now() < (nuAskSttArmMsRef.current || 0)) {
        setNuAskInlineError(
          nuAskUiStrings.voiceKeepGoing || 'Keep speaking — tap again when you are finished.'
        );
        return;
      }
      setNuAskInlineError('');
      setNuAskSttPhase('transcribing');
      try {
        // Flush final chunk before stop (Safari/Chrome often omit the last buffer otherwise).
        if (typeof activeRecorder.requestData === 'function') {
          try {
            activeRecorder.requestData();
          } catch (_) {
            /* noop */
          }
        }
        // Brief yield so ondataavailable can land before onstop on flaky mobile WebViews.
        setTimeout(() => {
          try {
            const rec = nuAskSttRecorderRef.current;
            if (rec && rec.state === 'recording') rec.stop();
          } catch (e) {
            setNuAskInlineError(nuAskUiStrings.voiceStopFailed || 'Could not stop recording.');
            setNuAskSttPhase('idle');
            cleanupNuAskSttCapture();
          }
        }, 80);
      } catch (e) {
        setNuAskInlineError(nuAskUiStrings.voiceStopFailed || 'Could not stop recording.');
        setNuAskSttPhase('idle');
        cleanupNuAskSttCapture();
      }
    }, [NUASK_STT_MIN_MS, cleanupNuAskSttCapture, nuAskUiStrings]);

    const startNuAskVoiceCapture = useCallback(async () => {
      if (!nuAskSttSupported) {
        setNuAskInlineError(nuAskUiStrings.voiceUnsupported || 'Voice input is not supported on this device.');
        return;
      }
      // Allow a fresh take even if a prior STT/send left phase sticky.
      if (nuAskInlineSending || nuAskInlineTyping) {
        setNuAskInlineError(nuAskUiStrings.voiceBusy || 'Still sending the last message — try again in a moment.');
        return;
      }
      if (nuAskSttPhase === 'transcribing') {
        // Recover from a stuck "transcribing…" strip so a second take is possible.
        setNuAskSttPhase('idle');
      }
      if (nuAskSttStartingRef.current) return;
      const activeRecorder = nuAskSttRecorderRef.current;
      if (activeRecorder && activeRecorder.state === 'recording') return;
      nuAskSttStartingRef.current = true;
      try {
        setNuAskInlineError('');
        setNuAskInlineInput('');
        nuAskInlineInputRef.current = '';
        // Prefer processing that helps far/quiet mics; still device-dependent.
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: {
            echoCancellation: true,
            noiseSuppression: true,
            autoGainControl: true
          }
        });
        nuAskSttStreamRef.current = stream;
        nuAskSttChunksRef.current = [];
        nuAskSttStartedAtRef.current = Date.now();
        nuAskSttArmMsRef.current = Date.now() + 400;
        const mimeCandidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
        const pickedMime = mimeCandidates.find((m) => window.MediaRecorder.isTypeSupported(m));
        const recorder = pickedMime ? new MediaRecorder(stream, { mimeType: pickedMime }) : new MediaRecorder(stream);
        nuAskSttRecorderRef.current = recorder;
        recorder.ondataavailable = (evt) => {
          if (evt?.data && evt.data.size > 0) nuAskSttChunksRef.current.push(evt.data);
        };
        recorder.onerror = () => {
          setNuAskInlineError(nuAskUiStrings.voiceRecordFailed || 'Voice recording failed. Please try again.');
          setNuAskSttPhase('idle');
          cleanupNuAskSttCapture();
        };
        recorder.onstop = async () => {
          // One more tick for late final chunks after requestData + stop.
          await new Promise((r) => setTimeout(r, 30));
          const chunks = Array.isArray(nuAskSttChunksRef.current) ? nuAskSttChunksRef.current.slice() : [];
          nuAskSttChunksRef.current = [];
          const elapsed = Math.max(1, Date.now() - (nuAskSttStartedAtRef.current || Date.now()));
          const blob = chunks.length > 0 ? new Blob(chunks, { type: recorder.mimeType || 'audio/webm' }) : null;
          cleanupNuAskSttCapture();
          if (!blob || blob.size <= 0) {
            setNuAskInlineError(
              nuAskUiStrings.voiceTooShort ||
                'No audio captured — tap the mic, speak, then tap again to stop.'
            );
            setNuAskSttPhase('idle');
            return;
          }
          // Prefer duration over raw byte size (WebM headers vary; short speech can be small).
          if (elapsed < NUASK_STT_MIN_MS) {
            setNuAskInlineError(
              nuAskUiStrings.voiceTooShort ||
                'Recording too short — tap the mic again after you finish speaking.'
            );
            setNuAskSttPhase('idle');
            return;
          }
          if (blob.size < NUASK_STT_MIN_BLOB_BYTES) {
            setNuAskInlineError(
              nuAskUiStrings.voiceTooShort ||
                'Recording too short — tap the mic again after you finish speaking.'
            );
            setNuAskSttPhase('idle');
            return;
          }
          await transcribeNuAskVoiceBlob(blob, Math.min(elapsed, NUASK_STT_MAX_MS));
        };
        // 250ms timeslice so chunks accumulate during the take; final requestData flushes the rest.
        recorder.start(250);
        setNuAskSttPhase('recording');
        nuAskSttStartingRef.current = false;
        nuAskSttTimeoutRef.current = setTimeout(() => {
          const current = nuAskSttRecorderRef.current;
          if (current && current.state === 'recording') {
            stopNuAskVoiceCapture();
          }
        }, NUASK_STT_MAX_MS + 300);
      } catch (e) {
        setNuAskInlineError(
          nuAskUiStrings.voiceMicDenied || 'Microphone permission denied or unavailable.'
        );
        setNuAskSttPhase('idle');
        cleanupNuAskSttCapture();
      }
    }, [
      NUASK_STT_MAX_MS,
      NUASK_STT_MIN_BLOB_BYTES,
      NUASK_STT_MIN_MS,
      cleanupNuAskSttCapture,
      nuAskInlineSending,
      nuAskInlineTyping,
      nuAskSttPhase,
      nuAskSttSupported,
      nuAskUiStrings,
      stopNuAskVoiceCapture,
      transcribeNuAskVoiceBlob
    ]);

    /** Tap once to start recording; tap again to stop + transcribe (not hold-to-talk). */
    const handleNuAskVoiceToggle = useCallback(
      (evt) => {
        evt?.preventDefault?.();
        evt?.stopPropagation?.();
        touchNuAskChatBox();
        if (nuAskInlineSending || nuAskInlineTyping) {
          setNuAskInlineError(
            nuAskUiStrings.voiceBusy || 'Still sending the last message — try again in a moment.'
          );
          return;
        }
        const activeRecorder = nuAskSttRecorderRef.current;
        const isRecording =
          nuAskSttPhase === 'recording' || (activeRecorder && activeRecorder.state === 'recording');
        if (isRecording) {
          stopNuAskVoiceCapture();
          return;
        }
        // If strip still says "transcribing…", recover and start a new take.
        void startNuAskVoiceCapture();
      },
      [
        nuAskInlineSending,
        nuAskInlineTyping,
        nuAskSttPhase,
        nuAskUiStrings,
        startNuAskVoiceCapture,
        stopNuAskVoiceCapture,
        touchNuAskChatBox
      ]
    );

    const clearEx1HoldTimer = useCallback(() => {
      if (ex1HoldTimerRef.current) {
        clearTimeout(ex1HoldTimerRef.current);
        ex1HoldTimerRef.current = null;
      }
    }, []);

    /** Block NuAsk corridor/TV pan while hold is arming (before overlay mounts). */
    const ex1PanLockCleanupRef = useRef(null);
    const releaseEx1PanLock = useCallback(() => {
      const cleanup = ex1PanLockCleanupRef.current;
      ex1PanLockCleanupRef.current = null;
      if (typeof cleanup === 'function') {
        try {
          cleanup();
        } catch (_) { /* ignore */ }
      }
    }, []);
    const installEx1PanLock = useCallback(() => {
      if (ex1PanLockCleanupRef.current) return;
      const blockPan = (e) => {
        if (e.cancelable) e.preventDefault();
      };
      document.addEventListener('touchmove', blockPan, { passive: false, capture: true });
      document.addEventListener('wheel', blockPan, { passive: false, capture: true });
      const prevBodyTouch = document.body.style.touchAction;
      document.body.style.touchAction = 'none';
      ex1PanLockCleanupRef.current = () => {
        document.removeEventListener('touchmove', blockPan, true);
        document.removeEventListener('wheel', blockPan, true);
        document.body.style.touchAction = prevBodyTouch;
      };
    }, []);

    const ensureEx1AliveSession = useCallback(async () => {
      try {
        const api = window.SF_EX1_RADIAL;
        if (!api?.postSession || !SF_API_BASE) return null;

        let session = ex1AliveSessionRef.current || api.readAliveSession?.() || null;
        // Hold product: never resume a legacy Alive mid-session as the intelligence space.
        try {
          if (
            session &&
            session.phase &&
            session.phase !== 'idle' &&
            session.phase !== 'standby' &&
            typeof api.isHoldIntelligenceSpace === 'function' &&
            !api.isHoldIntelligenceSpace(session)
          ) {
            api.clearAliveSession?.();
            ex1AliveSessionRef.current = null;
            session = null;
          }
        } catch (_) {
          /* soft */
        }
        // Re-enter when idle/missing so tag scan stays fresh; keep in-progress phases
        if (session && session.phase && session.phase !== 'idle' && session.phase !== 'standby') {
          ex1AliveSessionRef.current = session;
          const man = api.readAliveManifest?.() || null;
          if (man) setEx1AliveManifest(man);
          try {
            const reuseLine =
              typeof session.companion_line === 'string' && session.companion_line.trim()
                ? session.companion_line.trim()
                : null;
            if (reuseLine) setEx1AliveCompanionLine(reuseLine);
            if (typeof api.memoryChipsFromSession === 'function') {
              setEx1MemoryChips(api.memoryChipsFromSession(session));
            }
            if (typeof api.focusCardFromSession === 'function') {
              setEx1FocusCard(api.focusCardFromSession(session));
            }
            if (typeof api.spacePromptFromSession === 'function') {
              setEx1SpacePrompt(api.spacePromptFromSession(session));
            }
            if (typeof api.hisTvFromSession === 'function') {
              setEx1HisTv(api.hisTvFromSession(session));
            }
          } catch (_) {
            /* soft */
          }
          return session;
        }

        if (ex1AliveEnterInFlightRef.current) {
          return ex1AliveEnterInFlightRef.current;
        }

        const wx = nowRecoWeatherContext;
        const coords =
          liveUserCoords &&
          typeof liveUserCoords.lat === 'number' &&
          typeof liveUserCoords.lng === 'number'
            ? { lat: liveUserCoords.lat, lng: liveUserCoords.lng }
            : null;
        // #155 P1 (UNLOCK_UI_GOLD): pass NOW preview traveler tags when already known.
        // Server still hydrates PERSONALITY_KV if empty (#155 P0) — client is additive.
        const personality_tags = Array.isArray(nowRecoTravelerTags)
          ? [
              ...new Set(
                nowRecoTravelerTags
                  .map((t) => String(t || '').toLowerCase().trim())
                  .filter(Boolean)
              )
            ]
          : [];

        // SF device-local wall-time (same semantics as micro-action preview).
        const deviceClock =
          (typeof window !== 'undefined' &&
            window.SF_EX1_DEVICE_CLOCK &&
            typeof window.SF_EX1_DEVICE_CLOCK.buildEx1DeviceLocalClockFields === 'function' &&
            window.SF_EX1_DEVICE_CLOCK.buildEx1DeviceLocalClockFields(new Date())) ||
          (() => {
            const now = new Date();
            const o = {
              client_day_key: formatClientDayKeyLocal(now),
              client_local_hhmm: formatClientLocalHHMM(now)
            };
            try {
              const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
              if (tz) o.client_tz = tz;
            } catch (_) { /* soft */ }
            return o;
          })();

        const enter = {
          coords,
          hold_mode: 'intelligence_space',
          weather: wx
            ? {
                condition: wx.condition || wx.summary_line || null,
                temp_c: wx.temp_c ?? null,
                pop: wx.pop ?? wx.pop_now ?? null,
                summary_line: wx.summary_line || null
              }
            : null,
          rain_pop: wx?.pop ?? wx?.pop_now ?? null,
          // Capacity truth: never send placeholder fill_ratio (0.5 Day tank scaled half-tank).
          // Soft energy omitted → server DEFAULT_CAPACITY / Bazi / reported path.
          has_next_stop: !!(nowNextCard?.title && asText(nowNextCard?.icon) !== '◌'),
          ...deviceClock,
          ...(personality_tags.length ? { personality_tags } : {})
        };

        const sessionBody = {
          action: 'enter',
          hold_mode: 'intelligence_space',
          enter,
          coords,
          ...deviceClock,
          ...(personality_tags.length ? { personality_tags } : {})
        };

        const p = api
          .postSession(SF_API_BASE, sessionBody)
          .then((data) => {
            ex1HoldDenyCodeRef.current = null;
            if (data?.ok && data.session) {
              ex1AliveSessionRef.current = data.session;
              setEx1AliveManifest(data.manifest || null);
              // Display-only: server verbalisation; omit/null = keep prior (mid-session) or no line.
              try {
                const line =
                  typeof data.companion_line === 'string' && data.companion_line.trim()
                    ? data.companion_line.trim()
                    : null;
                if (line) {
                  if (data.session) data.session.companion_line = line;
                  setEx1AliveCompanionLine(line);
                }
              } catch (_) {
                /* soft — do not wipe prior companion on parse fail */
              }
              return data.session;
            }
            // Phase 5.3 — kill switch / soft deny (never invent SOP six menu).
            try {
              const code = typeof data?.code === 'string' ? data.code : null;
              if (code === 'ERR_EX1_FLAG_OFF') {
                ex1HoldDenyCodeRef.current = 'ERR_EX1_FLAG_OFF';
              } else if (code) {
                ex1HoldDenyCodeRef.current = code;
              }
            } catch (_) {
              /* soft */
            }
            return null;
          })
          .catch(() => {
            ex1HoldDenyCodeRef.current = 'ERR_EX1_ENTER_SOFT';
            return null;
          })
          .finally(() => {
            ex1AliveEnterInFlightRef.current = null;
          });
        ex1AliveEnterInFlightRef.current = p;
        return p;
      } catch (e) {
        return null;
      }
    }, [
      SF_API_BASE,
      liveUserCoords,
      nowRecoWeatherContext,
      nowNextCard?.title,
      nowNextCard?.icon,
      nowRecoTravelerTags
    ]);

    const openEx1RadialFromNowOrb = useCallback(
      (evt) => {
        const btn = nuAskNowOrbBtnRef.current;
        let x = evt.clientX;
        let y = evt.clientY;
        try {
          if (btn?.getBoundingClientRect) {
            const r = btn.getBoundingClientRect();
            x = r.left + r.width / 2;
            y = r.top + r.height / 2;
          }
        } catch (e) {
          /* keep event coords */
        }
        ex1HoldArmedRef.current = true;
        ex1OrbClickSkipRef.current = true;
        // Phase 1: release orb capture so thumb-up does not own the session;
        // overlay stays open (persistent space) and accepts taps after release.
        try {
          const orbBtn = nuAskNowOrbBtnRef.current;
          if (orbBtn?.hasPointerCapture?.(evt.pointerId)) {
            orbBtn.releasePointerCapture(evt.pointerId);
          }
        } catch (_) { /* ignore */ }
        setEx1RadialAnchor({ x, y });
        setEx1RadialHint('');
        setEx1FieldStatus('');
        setEx1FocusCard(null);
        setEx1SpacePrompt(null);
        setEx1HisTv(null);
        // Open immediately in loading; reveal seats + companion together when enter settles (no late flash).
        // Keep prior companion_line across re-hold until session echoes a line (mid-session continuity).
        setEx1RadialSeatsReady(false);
        const enterStartedMs = Date.now();
        void ensureEx1AliveSession()
          .then((session) => {
            try {
              if (!session) {
                // Fail closed — never invent companion / SOP menu (no-fake-fallback).
                // Integrated conclude plate (not mid-field status over a second option slot).
                setEx1AliveManifest(null);
                setEx1AliveCompanionLine(null);
                setEx1SpacePrompt(null);
                setEx1FieldStatus('');
                try {
                  const api = window.SF_EX1_RADIAL;
                  setEx1HisTv(
                    typeof api?.hisConcludePlate === 'function'
                      ? api.hisConcludePlate('unavailable', [])
                      : {
                          mode: 'conclude',
                          empty_label: "Check-in isn't available right now.",
                          conclude_prompt: 'What would you like to do next?',
                          conclude_options: [{ id: 'done', label: "I'm good", primary: true }]
                        }
                  );
                } catch (_) {
                  setEx1HisTv({
                    mode: 'conclude',
                    empty_label: "Check-in isn't available right now.",
                    conclude_prompt: 'What would you like to do next?',
                    conclude_options: [{ id: 'done', label: "I'm good", primary: true }]
                  });
                }
                window.uiLogger?.uiLog?.('[Ex.1] radial hold enter fail-closed', {
                  deny: ex1HoldDenyCodeRef.current || 'none',
                  enter_ms: Date.now() - enterStartedMs
                });
                return;
              }
              const line =
                typeof session?.companion_line === 'string' && session.companion_line.trim()
                  ? session.companion_line.trim()
                  : null;
              if (line) setEx1AliveCompanionLine(line);
              try {
                const api = window.SF_EX1_RADIAL;
                if (typeof api?.memoryChipsFromSession === 'function') {
                  setEx1MemoryChips(api.memoryChipsFromSession(session));
                }
                if (typeof api?.focusCardFromSession === 'function') {
                  setEx1FocusCard(api.focusCardFromSession(session));
                }
                if (typeof api?.spacePromptFromSession === 'function') {
                  setEx1SpacePrompt(api.spacePromptFromSession(session));
                }
                if (typeof api?.hisTvFromSession === 'function') {
                  setEx1HisTv(api.hisTvFromSession(session));
                }
              } catch (_) {
                /* soft */
              }
              window.uiLogger?.uiLog?.('[Ex.1] radial alive enter', {
                ok: !!session,
                phase: session?.phase || null,
                tag_count: session?.tag_scan?.tags?.length || 0,
                personality_tags_count: Array.isArray(nowRecoTravelerTags)
                  ? nowRecoTravelerTags.length
                  : 0,
                personality_applied: session?.personality_applied === true,
                companion: !!line,
                enter_ms: Date.now() - enterStartedMs,
                day_itin:
                  session?.day_itinerary_present === false
                    ? 0
                    : session?.day_itinerary_present === true
                      ? 1
                      : null
              });
            } catch (e2) {
              /* no-op */
            }
          })
          .catch(() => {
            setEx1AliveManifest(null);
            setEx1AliveCompanionLine(null);
            setEx1SpacePrompt(null);
            setEx1FieldStatus('');
            try {
              const api = window.SF_EX1_RADIAL;
              setEx1HisTv(
                typeof api?.hisConcludePlate === 'function'
                  ? api.hisConcludePlate('unavailable', [])
                  : {
                      mode: 'conclude',
                      empty_label: "Check-in isn't available right now.",
                      conclude_prompt: 'What would you like to do next?',
                      conclude_options: [{ id: 'done', label: "I'm good", primary: true }]
                    }
              );
            } catch (_) {
              setEx1HisTv({
                mode: 'conclude',
                empty_label: "Check-in isn't available right now.",
                conclude_prompt: 'What would you like to do next?',
                conclude_options: [{ id: 'done', label: "I'm good", primary: true }]
              });
            }
            try {
              window.uiLogger?.uiLog?.('[Ex.1] radial hold enter fail-closed', {
                deny: 'catch',
                enter_ms: Date.now() - enterStartedMs
              });
            } catch (_) {
              /* no-op */
            }
          })
          .finally(() => {
            setEx1RadialSeatsReady(true);
          });
        setEx1RadialOpen(true);
        try {
          window.uiLogger?.uiLog?.('[Ex.1] radial overlay open (NOW orb)');
        } catch (e2) {
          /* no-op */
        }
      },
      [ensureEx1AliveSession, nowRecoTravelerTags]
    );

    const handleNowOrbPointerDown = useCallback(
      (evt) => {
        if (evt.button != null && evt.button !== 0) return;
        if (nowRecoLoading) return;
        ex1HoldArmedRef.current = false;
        ex1OrbClickSkipRef.current = false;
        clearEx1HoldTimer();
        // Capture + pan lock during the 420ms arm so corridor/TV cannot steal the gesture
        // (without this, thumb slide pans the page and radial never expands).
        try {
          evt.currentTarget?.setPointerCapture?.(evt.pointerId);
        } catch (_) { /* ignore */ }
        if (evt.cancelable) {
          try {
            evt.preventDefault();
          } catch (_) { /* ignore */ }
        }
        installEx1PanLock();
        const pointerId = evt.pointerId;
        const clientX = evt.clientX;
        const clientY = evt.clientY;
        ex1HoldTimerRef.current = setTimeout(() => {
          ex1HoldTimerRef.current = null;
          openEx1RadialFromNowOrb({ pointerId, clientX, clientY });
        }, 420);
      },
      [clearEx1HoldTimer, installEx1PanLock, nowRecoLoading, openEx1RadialFromNowOrb]
    );

    const handleNowOrbPointerUp = useCallback(() => {
      clearEx1HoldTimer();
      // Early release (tap <420ms): drop pan lock. If radial opened, overlay owns finish.
      if (!ex1HoldArmedRef.current && !ex1RadialOpen) releaseEx1PanLock();
    }, [clearEx1HoldTimer, ex1RadialOpen, releaseEx1PanLock]);

    const handleNowOrbPointerCancel = useCallback(() => {
      clearEx1HoldTimer();
      if (!ex1RadialOpen) {
        ex1HoldArmedRef.current = false;
        releaseEx1PanLock();
      }
    }, [clearEx1HoldTimer, ex1RadialOpen, releaseEx1PanLock]);

    const handleNowOrbClick = useCallback(
      (evt) => {
        if (ex1HoldArmedRef.current || ex1RadialOpen || ex1OrbClickSkipRef.current) {
          evt?.preventDefault?.();
          evt?.stopPropagation?.();
          ex1OrbClickSkipRef.current = false;
          return;
        }
        refreshNowRecommendations();
      },
      [ex1RadialOpen, refreshNowRecommendations]
    );

    const handleEx1RadialCancel = useCallback((opts) => {
      const skipLeaveLog = opts && opts.skipLeaveLog === true;
      if (!skipLeaveLog) {
        try {
          window.uiLogger?.uiLog?.('[Ex.1] HIS leave', {
            leave_reason: 'back',
            phase: ex1AliveSessionRef.current?.phase || null
          });
        } catch (_) {
          /* no-op */
        }
      }
      ex1HoldArmedRef.current = false;
      ex1HoldGovernorBusyRef.current = false;
      setEx1RadialOpen(false);
      setEx1RadialAnchor(null);
      // Keep companion_line for mid-session re-hold (conversation continuity).
      setEx1RadialSeatsReady(false);
      setEx1FocusCard(null);
      setEx1FieldStatus('');
      setEx1SpacePrompt(null);
        setEx1HisTv(null);
      releaseEx1PanLock();
    }, [releaseEx1PanLock]);

    /** Hold-only governor fail copy — never surface raw NOW / i18n patch strip on HIS TV. */
    const holdGovernorFailCopy = useCallback((raw) => {
      const s = String(raw || '').trim();
      if (!s) return "Couldn't update your plan — try again or Not now.";
      if (/schedule or preview changed|行程或预览|refresh NOW|刷新 NOW/i.test(s)) {
        return "Your plan changed — try again or Not now.";
      }
      if (/couldn't match|Couldn't match/i.test(s)) {
        return "Couldn't match this place to your live options yet.";
      }
      if (s.length > 90) return "Couldn't update your plan — try again or Not now.";
      return s;
    }, []);

    /** A10: HIS TV conclude exits (Add / Not now / standby). */
    const handleEx1Conclude = useCallback(
      (optId) => {
        const id = String(optId || '').trim();
        const api = typeof window !== 'undefined' ? window.SF_EX1_RADIAL : null;

        if (id === 'keep_exploring') {
          const session = ex1AliveSessionRef.current || api?.readAliveSession?.() || null;
          if (!session || typeof api?.postSession !== 'function') {
            setEx1FieldStatus('No other places right now.');
            return;
          }
          setEx1FieldStatus('Looking for another…');
          setEx1RadialSeatsReady(false);
          void api
            .postSession(SF_API_BASE, {
              action: 'event',
              hold_mode: 'intelligence_space',
              session,
              event: { value: 'another', layer: 'L2' }
            })
            .then((data) => {
              if (!data?.ok || !data.session) {
                setEx1FieldStatus('No other places right now.');
                setEx1RadialSeatsReady(true);
                const steps =
                  (Array.isArray(session.path_steps) && session.path_steps) || [];
                if (typeof api.hisConcludePlate === 'function') {
                  setEx1HisTv(
                    api.hisConcludePlate('dismissed', steps, { canKeepExploring: false })
                  );
                }
                try {
                  window.uiLogger?.uiLog?.('[Ex.1] HIS leave', {
                    leave_reason: 'conclude_keep_exploring_fail',
                    phase: session.phase || null
                  });
                } catch (_) {
                  /* no-op */
                }
                return;
              }
              ex1AliveSessionRef.current = data.session;
              setEx1AliveManifest(data.manifest || null);
              setEx1FieldStatus('');
              setEx1RadialHint('');
              try {
                if (typeof api.spacePromptFromSession === 'function') {
                  setEx1SpacePrompt(api.spacePromptFromSession(data.session));
                }
                if (typeof api.focusCardFromSession === 'function') {
                  setEx1FocusCard(api.focusCardFromSession(data.session));
                }
                if (typeof api.memoryChipsFromSession === 'function') {
                  setEx1MemoryChips(api.memoryChipsFromSession(data.session));
                }
                if (typeof api.hisTvFromSession === 'function') {
                  setEx1HisTv(api.hisTvFromSession(data.session));
                }
                const releaseLine =
                  (typeof data.companion_line === 'string' && data.companion_line.trim()) ||
                  (typeof data.session?.companion_line === 'string' &&
                    data.session.companion_line.trim()) ||
                  null;
                if (releaseLine) setEx1AliveCompanionLine(releaseLine);
              } catch (_) {
                /* soft */
              }
              setEx1RadialSeatsReady(true);
            })
            .catch(() => {
              setEx1FieldStatus('No other places right now.');
              setEx1RadialSeatsReady(true);
            });
          return;
        }

        // Valued-empty agency: re-ask mood only — never invent places.
        if (id === 'retry_feel') {
          const session = ex1AliveSessionRef.current || api?.readAliveSession?.() || null;
          if (!session || typeof api?.postSession !== 'function') {
            setEx1FieldStatus("Couldn't restart — try again from NOW.");
            return;
          }
          setEx1FieldStatus('Okay…');
          setEx1RadialSeatsReady(false);
          void api
            .postSession(SF_API_BASE, {
              action: 'event',
              hold_mode: 'intelligence_space',
              session,
              event: { value: 'retry_feel', layer: 'L2' }
            })
            .then((data) => {
              if (!data?.ok || !data.session) {
                setEx1FieldStatus("Couldn't restart — keep what you noted.");
                setEx1RadialSeatsReady(true);
                return;
              }
              ex1AliveSessionRef.current = data.session;
              setEx1AliveManifest(data.manifest || null);
              setEx1FieldStatus('');
              setEx1RadialHint('');
              try {
                if (typeof api.spacePromptFromSession === 'function') {
                  setEx1SpacePrompt(api.spacePromptFromSession(data.session));
                }
                if (typeof api.focusCardFromSession === 'function') {
                  setEx1FocusCard(api.focusCardFromSession(data.session));
                }
                if (typeof api.memoryChipsFromSession === 'function') {
                  setEx1MemoryChips(api.memoryChipsFromSession(data.session));
                }
                if (typeof api.hisTvFromSession === 'function') {
                  setEx1HisTv(api.hisTvFromSession(data.session));
                }
                const releaseLine =
                  (typeof data.companion_line === 'string' && data.companion_line.trim()) ||
                  (typeof data.session?.companion_line === 'string' &&
                    data.session.companion_line.trim()) ||
                  (typeof data.session?.traveller_ack === 'string' &&
                    data.session.traveller_ack.trim()) ||
                  null;
                if (releaseLine) setEx1AliveCompanionLine(releaseLine);
                window.uiLogger?.uiLog?.('[Ex.1] HIS retry_feel', {
                  phase: data.session.phase || null
                });
              } catch (_) {
                /* soft */
              }
              setEx1RadialSeatsReady(true);
            })
            .catch(() => {
              setEx1FieldStatus("Couldn't restart — keep what you noted.");
              setEx1RadialSeatsReady(true);
            });
          return;
        }

        const leaveReason =
          id === 'see_flow_plan'
            ? 'conclude_flow_plan'
            : id === 'back_to_now'
              ? 'conclude_back_to_now'
              : id === 'done'
                ? 'conclude_done'
                : 'conclude_done';
        const leaveValue =
          id === 'see_flow_plan'
            ? 'see_flow_plan'
            : id === 'back_to_now'
              ? 'back_to_now'
              : 'done';
        try {
          window.uiLogger?.uiLog?.('[Ex.1] HIS leave', {
            leave_reason: leaveReason,
            phase: ex1AliveSessionRef.current?.phase || null
          });
        } catch (_) {
          /* no-op */
        }

        // F10 — stamp conclude exit before clear (fail soft).
        // skip_alive_cache: late response must not write STANDBY over a fresh enter.
        const leaveSession = ex1AliveSessionRef.current || api?.readAliveSession?.() || null;
        if (leaveSession && typeof api?.postSession === 'function' && SF_API_BASE) {
          try {
            void api
              .postSession(SF_API_BASE, {
                action: 'event',
                hold_mode: 'intelligence_space',
                session: leaveSession,
                release: { value: leaveValue, layer: 'L2' },
                event: { value: leaveValue, layer: 'L2' },
                skip_alive_cache: true
              })
              .catch(() => {
                /* soft — never block leave */
              });
          } catch (_) {
            /* soft */
          }
        }

        if (id === 'see_flow_plan') {
          api?.clearAliveSession?.();
          ex1AliveSessionRef.current = null;
          handleEx1RadialCancel({ skipLeaveLog: true });
          void refreshNowRecommendations();
          corridorNavigate('explore');
          return;
        }

        // done / back_to_now / unknown → leave HIS; refresh NOW after plan change.
        api?.clearAliveSession?.();
        ex1AliveSessionRef.current = null;
        handleEx1RadialCancel({ skipLeaveLog: true });
        void refreshNowRecommendations();
      },
      [SF_API_BASE, handleEx1RadialCancel, refreshNowRecommendations]
    );

    // Freeze nested NuAsk scrollers while radial is open (body overflow alone is not enough).
    useEffect(() => {
      if (!ex1RadialOpen) return undefined;
      const els = [nowSectionScrollRef.current, scrollerRef.current].filter(Boolean);
      const prev = els.map((el) => ({
        el,
        overflowY: el.style.overflowY,
        overflowX: el.style.overflowX,
        touchAction: el.style.touchAction
      }));
      els.forEach((el) => {
        el.style.overflowY = 'hidden';
        el.style.overflowX = 'hidden';
        el.style.touchAction = 'none';
      });
      return () => {
        prev.forEach(({ el, overflowY, overflowX, touchAction }) => {
          el.style.overflowY = overflowY;
          el.style.overflowX = overflowX;
          el.style.touchAction = touchAction;
        });
      };
    }, [ex1RadialOpen]);

    const handleEx1RadialSelect = useCallback(
      (payload) => {
        try {
          window.SF_EX1_RADIAL?.appendReported?.(payload);
        } catch (e) {
          /* ignore */
        }

        const extras = {
          gesture: 'hold',
          client_day_key: formatClientDayKeyLocal(new Date()),
          micro_action_id: nowRecoMicroActionId || null,
          coords:
            liveUserCoords &&
            typeof liveUserCoords.lat === 'number' &&
            typeof liveUserCoords.lng === 'number'
              ? { lat: liveUserCoords.lat, lng: liveUserCoords.lng }
              : null
        };

        // Durable: LEARNING_KV via POST /api/ex1/reported
        try {
          const post =
            window.SF_EX1_RADIAL?.postReported?.(SF_API_BASE, payload, extras) ||
            Promise.resolve({ ok: false, skipped: true });
          post
            .then((data) => {
              try {
                window.uiLogger?.uiLog?.('[Ex.1] radial reported → LEARNING_KV', {
                  wedge_id: payload.wedge_id,
                  value: payload.value,
                  ok: !!(data && data.ok),
                  learning_kv_updated: !!(data && data.learning_kv_updated),
                  feedback_collected: !!(data && data.feedback_collected),
                  classified: data?.classified || null
                });
              } catch (eLog) {
                /* no-op */
              }
            })
            .catch(() => {
              /* offline / flag-off — session cache still holds report */
            });
        } catch (ePost) {
          /* ignore */
        }

        // Alive / Hold intelligence space: session event advances phase without UI close.
        let hintLabel = `${payload.wedge_id}: ${payload.value}`;
        try {
          const api = window.SF_EX1_RADIAL;
          const session = ex1AliveSessionRef.current || api?.readAliveSession?.() || null;
          const holdSpace = !!(api?.isHoldIntelligenceSpace?.(session));
          const wedgeList =
            (ex1AliveManifest && api?.manifestToWedges?.(ex1AliveManifest)) || [];
          const seat = wedgeList.find((w) => w.id === payload.wedge_id);
          if (session && api?.shouldReleaseAliveSession?.(session, payload) && api?.postSession) {
            const releaseValue = api.normalizeAliveReleaseValue?.(payload) || payload.value;
            // A18: keep loading until the *next* stage (seats + Top) is applied.
            if (holdSpace) setEx1RadialSeatsReady(false);
            void api
              .postSession(SF_API_BASE, {
                // Phase 2+: event alias; release kept for legacy Smart Brain tips.
                action: holdSpace ? 'event' : 'release',
                hold_mode: holdSpace ? 'intelligence_space' : undefined,
                session,
                release: {
                  value: releaseValue,
                  slot_id: seat?.slot_id || null,
                  layer: payload.layer || 'L2'
                },
                event: holdSpace
                  ? {
                      value: releaseValue,
                      slot_id: seat?.slot_id || null,
                      layer: payload.layer || 'L2'
                    }
                  : undefined
              })
              .then((data) => {
                if (!data?.ok || !data.session) {
                  try {
                    window.uiLogger?.uiLog?.('[Ex.1] radial session release soft-fail', {
                      reason: data?.reason || data?.code || 'fail'
                    });
                  } catch (e3) {
                    /* no-op */
                  }
                  if (holdSpace) {
                    setEx1FieldStatus("That choice didn't fit — try another.");
                    setEx1RadialSeatsReady(true);
                  }
                  return;
                }
                ex1AliveSessionRef.current = data.session;
                setEx1AliveManifest(data.manifest || null);
                // W3: entering COMMIT must not keep prior NOW/governor amber under Add-now.
                const nextPhase = String(data.session.phase || '').toLowerCase();
                if (nextPhase === 'commit' || nextPhase === 'tv_shown') {
                  setEx1FieldStatus('');
                }
                try {
                  const releaseLine =
                    (typeof data.companion_line === 'string' && data.companion_line.trim()) ||
                    (typeof data.session?.companion_line === 'string' &&
                      data.session.companion_line.trim()) ||
                    null;
                  if (releaseLine) {
                    data.session.companion_line = releaseLine;
                    setEx1AliveCompanionLine(releaseLine);
                  } else if (typeof api.travellerAckFromSession === 'function') {
                    const ack = api.travellerAckFromSession(data.session);
                    if (ack) setEx1AliveCompanionLine(ack);
                  } else if (
                    typeof data.session?.traveller_ack === 'string' &&
                    data.session.traveller_ack.trim()
                  ) {
                    setEx1AliveCompanionLine(data.session.traveller_ack.trim());
                  }
                  if (typeof api.memoryChipsFromSession === 'function') {
                    setEx1MemoryChips(api.memoryChipsFromSession(data.session));
                  }
                  if (typeof api.focusCardFromSession === 'function') {
                    setEx1FocusCard(api.focusCardFromSession(data.session));
                  }
                  if (typeof api.spacePromptFromSession === 'function') {
                    setEx1SpacePrompt(api.spacePromptFromSession(data.session));
                  }
                  if (typeof api.hisTvFromSession === 'function') {
                    setEx1HisTv(api.hisTvFromSession(data.session));
                  }
                  if (holdSpace) setEx1RadialSeatsReady(true);
                  // W6 Q1 ops: non-PII progression snapshot (roles/counts only).
                  try {
                    const mem = data.session.memory || {};
                    window.uiLogger?.uiLog?.('[Ex.1] radial session event', {
                      phase: data.session.phase,
                      intent: data.intent?.type || data.session?.last_intent?.type || null,
                      hold_space: holdSpace,
                      tv: data.session.tv_poi?.poi_id ? '1' : null,
                      ui_close: data.ui_close === true,
                      ctx_mood: mem.mood ? 1 : 0,
                      ctx_protect: mem.protect ? 1 : 0,
                      ctx_interrupt: mem.interrupt ? 1 : 0,
                      explore_tags: Array.isArray(data.session.explore_tags)
                        ? data.session.explore_tags.length
                        : 0,
                      path_roles: (Array.isArray(data.session.path_steps)
                        ? data.session.path_steps.map((s) => s.role).filter(Boolean)
                        : []
                      ).join('>')
                    });
                  } catch (_) {
                    /* no-op */
                  }
                } catch (_) {
                  /* soft */
                }

                const tv = data.session.tv_poi;
                const intentType = data.intent?.type || data.session?.last_intent?.type;

                // Phase 3.6: do NOT mid-session dump to NOW TV / refresh pool.
                // Quiet bind only when committing (Phase 4) so Governor has an option_id.
                if (holdSpace) {
                  // Never surface wedge_id:value / machinery plain_why as "Noted · …"
                  setEx1RadialHint('');
                  if (intentType === 'commit_dismiss') {
                    // F8: 2nd+ dismiss opens trust_nudge radial — stay in seats (no conclude yet).
                    const trustOpen =
                      data.intent?.trust_nudge_open === true ||
                      String(data.session?.phase || '').toLowerCase() === 'trust_nudge';
                    if (trustOpen) {
                      ex1AliveSessionRef.current = data.session;
                      setEx1AliveManifest(data.manifest || null);
                      setEx1FieldStatus('');
                      ex1HoldGovernorBusyRef.current = false;
                      setEx1RadialSeatsReady(true);
                      try {
                        if (typeof api.spacePromptFromSession === 'function') {
                          setEx1SpacePrompt(api.spacePromptFromSession(data.session));
                        }
                        if (typeof api.hisTvFromSession === 'function') {
                          setEx1HisTv(api.hisTvFromSession(data.session));
                        }
                        window.uiLogger?.uiLog?.('[Ex.1] HIS trust_nudge open', {
                          dismiss_count:
                            data.intent?.dismiss_count ||
                            data.session?.commit_dismiss_count ||
                            null
                        });
                      } catch (_) {
                        /* soft */
                      }
                      return;
                    }
                    // A10 v1: conclude plate — keep session for Keep exploring; clear governor noise.
                    const steps =
                      (Array.isArray(data.session.path_steps) && data.session.path_steps) ||
                      (ex1HisTv && Array.isArray(ex1HisTv.steps) && ex1HisTv.steps) ||
                      [];
                    const canKeepExploring =
                      Array.isArray(data.session.resolved_pois) &&
                      data.session.resolved_pois.length >= 2;
                    ex1AliveSessionRef.current = data.session;
                    setEx1AliveManifest(null);
                    setEx1FocusCard(null);
                    setEx1SpacePrompt(null);
                    setEx1FieldStatus('');
                    ex1HoldGovernorBusyRef.current = false;
                    const plate =
                      typeof api.hisConcludePlate === 'function'
                        ? api.hisConcludePlate('dismissed', steps, { canKeepExploring })
                        : {
                            mode: 'conclude',
                            steps,
                            empty_label: "Nothing added — that's fine.",
                            conclude_kind: 'dismissed',
                            conclude_prompt: 'What would you like to do?',
                            conclude_options: canKeepExploring
                              ? [
                                  { id: 'keep_exploring', label: 'Keep exploring', primary: true },
                                  { id: 'done', label: "I'm good" }
                                ]
                              : [{ id: 'done', label: "I'm good", primary: true }]
                          };
                    setEx1HisTv(plate);
                    setEx1RadialSeatsReady(true);
                    return;
                  }
                  if (intentType === 'trust_nudge_feedback') {
                    // F8 — preference collected → A10 conclude (same as dismiss).
                    const steps =
                      (Array.isArray(data.session.path_steps) && data.session.path_steps) ||
                      (ex1HisTv && Array.isArray(ex1HisTv.steps) && ex1HisTv.steps) ||
                      [];
                    const canKeepExploring =
                      Array.isArray(data.session.resolved_pois) &&
                      data.session.resolved_pois.length >= 2;
                    ex1AliveSessionRef.current = data.session;
                    setEx1AliveManifest(null);
                    setEx1FocusCard(null);
                    setEx1SpacePrompt(null);
                    setEx1FieldStatus('');
                    ex1HoldGovernorBusyRef.current = false;
                    const plate =
                      typeof api.hisConcludePlate === 'function'
                        ? api.hisConcludePlate('dismissed', steps, { canKeepExploring })
                        : {
                            mode: 'conclude',
                            steps,
                            empty_label: "Nothing added — that's fine.",
                            conclude_kind: 'dismissed',
                            conclude_prompt: 'What would you like to do?',
                            conclude_options: canKeepExploring
                              ? [
                                  { id: 'keep_exploring', label: 'Keep exploring', primary: true },
                                  { id: 'done', label: "I'm good" }
                                ]
                              : [{ id: 'done', label: "I'm good", primary: true }]
                          };
                    setEx1HisTv(plate);
                    setEx1RadialSeatsReady(true);
                    return;
                  }
                  if (intentType === 'why_not_feedback') {
                    // A22 — reason/skip collected → A10 conclude (Add already succeeded).
                    const steps =
                      (Array.isArray(data.session.path_steps) && data.session.path_steps) ||
                      (ex1HisTv && Array.isArray(ex1HisTv.steps) && ex1HisTv.steps) ||
                      [];
                    setEx1FieldStatus('');
                    api.clearAliveSession?.();
                    ex1AliveSessionRef.current = null;
                    setEx1AliveManifest(null);
                    setEx1FocusCard(null);
                    setEx1SpacePrompt(null);
                    ex1HoldGovernorBusyRef.current = false;
                    const plate =
                      typeof api.hisConcludePlate === 'function'
                        ? api.hisConcludePlate('added', steps, { hasTodayPlan: true })
                        : {
                            mode: 'conclude',
                            steps,
                            empty_label: 'Added to your plan.',
                            conclude_kind: 'added',
                            conclude_prompt: 'What next?',
                            conclude_options: [
                              {
                                id: 'see_flow_plan',
                                label: 'See Flow Plan',
                                primary: true
                              },
                              { id: 'back_to_now', label: 'Back to NOW' }
                            ]
                          };
                    setEx1HisTv(plate);
                    setEx1RadialSeatsReady(true);
                    return;
                  }
                  if (intentType === 'governor_add' || intentType === 'governor_replace') {
                    if (ex1HoldGovernorBusyRef.current) return;
                    ex1HoldGovernorBusyRef.current = true;
                    setEx1RadialSeatsReady(true);
                    setEx1FieldStatus('Checking the rest of your plan…');
                    const mode = intentType === 'governor_add' ? 'add' : 'replace';
                    let optionOverride = null;
                    try {
                      if (tv && typeof api.bindTvPoiToOptions === 'function') {
                        // Exact poi_id only — fuzzy is labeled catalog fallback, not Hold truth.
                        const hit = api.bindTvPoiToOptions(nowRecoOptions, tv, {
                          allowFuzzy: false
                        });
                        if (hit?.ok && hit.mode === 'exact' && hit.option) {
                          optionOverride = hit.option;
                          setSelectedRecoKey(getRecoKey(hit.option, hit.index));
                        }
                      }
                    } catch (_) {
                      /* soft */
                    }
                    void (async () => {
                      try {
                        if (!optionOverride) {
                          setEx1FieldStatus(
                            "Couldn't match this place to your live options yet."
                          );
                          ex1HoldGovernorBusyRef.current = false;
                          return;
                        }
                        const submitFn = ex1SubmitRecoRef.current;
                        if (typeof submitFn !== 'function') {
                          setEx1FieldStatus("Couldn't update your plan — try again.");
                          ex1HoldGovernorBusyRef.current = false;
                          return;
                        }
                        const result = await submitFn(mode, {
                          holdSpace: true,
                          optionOverride
                        });
                        if (result && result.ok) {
                          const pathSteps =
                            (ex1AliveSessionRef.current &&
                              Array.isArray(ex1AliveSessionRef.current.path_steps) &&
                              ex1AliveSessionRef.current.path_steps) ||
                            (Array.isArray(data.session.path_steps) && data.session.path_steps) ||
                            [];
                          // Named-POI continuity stamp (Accept → next Hold open feel). Fail soft.
                          try {
                            const title =
                              (tv && (tv.title || tv.name)) ||
                              (optionOverride && (optionOverride.title || optionOverride.name)) ||
                              null;
                            const poiId =
                              (tv && tv.poi_id) ||
                              (optionOverride &&
                                (optionOverride.poi_id || optionOverride.id)) ||
                              null;
                            if (title || poiId) {
                              void api.postReported?.(
                                SF_API_BASE,
                                { wedge_id: 'his_continuity_accept', value: 'accepted', layer: 'L2' },
                                {
                                  gesture: 'hold',
                                  client_day_key: formatClientDayKeyLocal(new Date()),
                                  poi_id: poiId,
                                  poi_title: title,
                                  theme_label: 'nearby',
                                  still_active: true
                                }
                              );
                            }
                          } catch (_) {
                            /* soft */
                          }
                          // A22 — after Add only: open why-not when alternate pending; else A10 conclude.
                          const sessKeep =
                            ex1AliveSessionRef.current || data.session || null;
                          const whyPending =
                            mode === 'add' &&
                            sessKeep &&
                            sessKeep.why_not_pending &&
                            sessKeep.why_not_pending.pending === true &&
                            sessKeep.why_not_pending.alternate_title;
                          if (whyPending && typeof api.postSession === 'function') {
                            setEx1FieldStatus('');
                            setEx1RadialSeatsReady(false);
                            try {
                              const whyData = await api.postSession(SF_API_BASE, {
                                action: 'event',
                                hold_mode: 'intelligence_space',
                                session: sessKeep,
                                release: { value: 'open_why_not', layer: 'L2' },
                                event: { value: 'open_why_not', layer: 'L2' }
                              });
                              if (
                                whyData?.ok &&
                                whyData.session &&
                                String(whyData.session.phase || '').toLowerCase() === 'why_not'
                              ) {
                                ex1AliveSessionRef.current = whyData.session;
                                setEx1AliveManifest(whyData.manifest || null);
                                try {
                                  if (typeof api.spacePromptFromSession === 'function') {
                                    setEx1SpacePrompt(
                                      api.spacePromptFromSession(whyData.session)
                                    );
                                  } else if (
                                    whyData.manifest?.interaction_contract?.space_prompt_hint
                                  ) {
                                    setEx1SpacePrompt(
                                      whyData.manifest.interaction_contract.space_prompt_hint
                                    );
                                  }
                                  if (typeof api.memoryChipsFromSession === 'function') {
                                    setEx1MemoryChips(
                                      api.memoryChipsFromSession(whyData.session)
                                    );
                                  }
                                  const releaseLine =
                                    (typeof whyData.companion_line === 'string' &&
                                      whyData.companion_line.trim()) ||
                                    null;
                                  if (releaseLine) setEx1AliveCompanionLine(releaseLine);
                                } catch (_) {
                                  /* soft */
                                }
                                // Keep path chips on HIS TV (no conclude yet).
                                if (typeof api.hisTvFromSession === 'function') {
                                  try {
                                    setEx1HisTv(api.hisTvFromSession(whyData.session));
                                  } catch (_) {
                                    /* soft */
                                  }
                                }
                                ex1HoldGovernorBusyRef.current = false;
                                setEx1RadialSeatsReady(true);
                                return;
                              }
                            } catch (_) {
                              /* fall through to conclude */
                            }
                          }
                          // Clear governor / prior NOW-error status before conclude (A10).
                          setEx1FieldStatus('');
                          api.clearAliveSession?.();
                          ex1AliveSessionRef.current = null;
                          setEx1AliveManifest(null);
                          setEx1FocusCard(null);
                          setEx1SpacePrompt(null);
                          ex1HoldGovernorBusyRef.current = false;
                          // Successful Add/Replace → there is a today plan to show.
                          const plate =
                            typeof api.hisConcludePlate === 'function'
                              ? api.hisConcludePlate(
                                  mode === 'replace' ? 'replaced' : 'added',
                                  pathSteps,
                                  { hasTodayPlan: true }
                                )
                              : {
                                  mode: 'conclude',
                                  steps: pathSteps,
                                  empty_label:
                                    mode === 'replace'
                                      ? 'Replaced your next stop.'
                                      : 'Added to your plan.',
                                  conclude_kind: mode === 'replace' ? 'replaced' : 'added',
                                  conclude_prompt: 'What next?',
                                  conclude_options: [
                                    {
                                      id: 'see_flow_plan',
                                      label: 'See Flow Plan',
                                      primary: true
                                    },
                                    { id: 'back_to_now', label: 'Back to NOW' }
                                  ]
                                };
                          setEx1HisTv(plate);
                        } else {
                          setEx1FieldStatus(
                            holdGovernorFailCopy(
                              (result && result.error) ||
                                "Couldn't update your plan — try again or Not now."
                            )
                          );
                          ex1HoldGovernorBusyRef.current = false;
                        }
                      } catch (eGov) {
                        setEx1FieldStatus(
                          holdGovernorFailCopy("Couldn't update your plan — try again.")
                        );
                        ex1HoldGovernorBusyRef.current = false;
                      }
                    })();
                    return;
                  }
                  if (intentType === 'standby_silence') {
                    setEx1RadialHint("I'll be here when you need me");
                  }
                } else {
                  // Legacy Alive path (should be rare — enter is intelligence_space).
                  if (tv?.title || tv?.poi_id) {
                    setEx1RadialHint(`TV · ${tv.title || tv.poi_id}`);
                    try {
                      const matchIdx =
                        typeof api.matchTvPoiToOptions === 'function'
                          ? api.matchTvPoiToOptions(nowRecoOptions, tv)
                          : -1;
                      if (matchIdx >= 0) {
                        setSelectedRecoKey(getRecoKey(nowRecoOptions[matchIdx], matchIdx));
                        ex1PendingTvPoiRef.current = null;
                      } else {
                        ex1PendingTvPoiRef.current = tv;
                        void refreshNowRecommendations();
                      }
                    } catch (eMatch) {
                      ex1PendingTvPoiRef.current = tv;
                    }
                  } else if (data.session.plain_why) {
                    setEx1RadialHint(data.session.plain_why);
                  }
                  if (intentType === 'standby_silence') {
                    setEx1RadialHint("I'll be here when you need me");
                  }
                }
              })
              .catch(() => {
                if (holdSpace) {
                  setEx1RadialSeatsReady(true);
                  setEx1FieldStatus("That choice didn't fit — try another.");
                }
                /* soft — reported path still ran */
              });
          }
        } catch (eAlive) {
          /* ignore */
        }

        // Phase 1–4: keep intelligence space open after select (no close-on-select).
        setEx1RadialHint(hintLabel);
        try {
          window.uiLogger?.uiLog?.('[Ex.1] radial select (session + LEARNING_KV)', {
            wedge_id: payload.wedge_id,
            value: payload.value,
            hold_persistent: true
          });
        } catch (e2) {
          /* no-op */
        }
      },
      [
        SF_API_BASE,
        liveUserCoords,
        nowRecoMicroActionId,
        ex1AliveManifest,
        ex1MemoryChips,
        ex1HisTv,
        nowRecoOptions,
        refreshNowRecommendations,
        handleEx1RadialCancel,
        holdGovernorFailCopy
      ]
    );

    useEffect(() => {
      if (!ex1RadialHint) return undefined;
      const t = setTimeout(() => setEx1RadialHint(''), 4500);
      return () => clearTimeout(t);
    }, [ex1RadialHint]);

    // After preview refresh, finish TV↔TOP marry if radial left a pending POI
    useEffect(() => {
      const tv = ex1PendingTvPoiRef.current;
      if (!tv || !nowRecoOptions.length) return;
      try {
        const api = window.SF_EX1_RADIAL;
        const matchIdx =
          typeof api?.matchTvPoiToOptions === 'function'
            ? api.matchTvPoiToOptions(nowRecoOptions, tv)
            : -1;
        if (matchIdx >= 0) {
          setSelectedRecoKey(getRecoKey(nowRecoOptions[matchIdx], matchIdx));
          ex1PendingTvPoiRef.current = null;
          setEx1RadialHint(`TV · ${tv.title || tv.poi_id || 'matched'}`);
        }
      } catch (e) {
        /* soft */
      }
    }, [nowRecoOptions]);

    const ex1RadialWedges = useMemo(() => {
      // Hold loading: empty wedges until Alive enter settles (avoids flash then swap).
      if (!ex1RadialSeatsReady) return [];
      try {
        const api = window.SF_EX1_RADIAL;
        if (ex1AliveManifest && api?.manifestToWedges) {
          const alive = api.manifestToWedges(ex1AliveManifest);
          if (alive && alive.length) return alive;
        }
        // Fail closed: empty seats. Never SOP six as fake Ex.1 Hold menu.
        return [];
      } catch (e) {
        return [];
      }
    }, [ex1RadialOpen, ex1AliveManifest, ex1RadialSeatsReady]);

    /** Presentation-only: reveal with seats (never late pop after loading). Never feeds Ex.1 / Governor. */
    const ex1CompanionLine =
      ex1RadialOpen && ex1RadialSeatsReady ? ex1AliveCompanionLine : null;

    useEffect(() => {
      return () => {
        cleanupNuAskSttCapture();
        clearEx1HoldTimer();
        releaseEx1PanLock();
      };
    }, [cleanupNuAskSttCapture, clearEx1HoldTimer, releaseEx1PanLock]);

    useEffect(() => {
      return () => {
        if (nuAskLatestHideTimerRef.current) clearTimeout(nuAskLatestHideTimerRef.current);
        if (nuAskLatestClearTimerRef.current) clearTimeout(nuAskLatestClearTimerRef.current);
      };
    }, []);

    useLayoutEffect(() => {
      const updateDockMetrics = () => {
        const measure = () => {
          let nextH = 0;
          try {
            const el = nuAskNowDockRef.current;
            if (el?.getBoundingClientRect) nextH = Math.max(0, Math.ceil(el.getBoundingClientRect().height || 0));
          } catch (e) {
            nextH = 0;
          }
          setNuAskNowDockHeight((prev) => (prev === nextH ? prev : nextH));
        };
        requestAnimationFrame(() => {
          requestAnimationFrame(measure);
        });
      };

      updateDockMetrics();
      window.addEventListener('resize', updateDockMetrics);
      let ro;
      if (typeof ResizeObserver !== 'undefined' && nuAskNowDockRef.current) {
        ro = new ResizeObserver(updateDockMetrics);
        ro.observe(nuAskNowDockRef.current);
      }
      return () => {
        window.removeEventListener('resize', updateDockMetrics);
        if (ro) ro.disconnect();
      };
    }, [
      panel,
      nuAskLatestMounted,
      nuAskLatestVisible,
      nuAskInlineError,
      nuAskInlineTyping,
      nuAskSttPhase,
      nuAskInlineInput,
      nowTvAskOpen,
      nowHotbarDetail,
      nowNearbyOpen,
      nowWeatherPlanOpen,
      nowUseMyTimeOpen
    ]);

    useEffect(() => {
      const wasLoading = prevNowRecoLoadingRef.current;
      prevNowRecoLoadingRef.current = nowRecoLoading;
      if (!wasLoading || nowRecoLoading || panel !== 1) return;
      requestAnimationFrame(() => {
        requestAnimationFrame(() => snapNowRecoScrollToTop());
      });
    }, [nowRecoLoading, panel, snapNowRecoScrollToTop]);

    useEffect(() => {
      // Set RIGHT as soon as Tap NOW starts loading so the first ready frame
      // is recommendation, not a flash of LEFT. RIGHT is hidden while
      // nowRecoLoading (showNowRecoTopStack is false); fray covers LEFT.
      if (nowRecoLoading) {
        setNowTvInnerPane('right');
      }
      if (wasNowRecoLoadingForRevealRef.current && !nowRecoLoading && nowRecoSyncPhase === 'ready') {
        setTopRecoRevealKey((k) => k + 1);
        setNowTvInnerPane('right');
      }
      wasNowRecoLoadingForRevealRef.current = nowRecoLoading;
    }, [nowRecoLoading, nowRecoSyncPhase]);

    useEffect(() => {
      if (!showLocationPrompt) return undefined;
      const timer = setTimeout(() => setShowLocationPrompt(false), 3000);
      return () => clearTimeout(timer);
    }, [showLocationPrompt]);

    useEffect(() => {
      try {
        const raw = localStorage.getItem(EN_ROUTE_LOCK_STORAGE_KEY);
        if (!raw) return;
        const parsed = JSON.parse(raw);
        const until = Number(parsed?.until_ms || 0);
        const poi = asText(parsed?.poi_name || '').trim();
        const poiId = normalizePoiIdKey(parsed?.poi_id || '');
        /** Do not use Number(null) — that is 0 and would fake Null Island coords. */
        const parseCoord = (v) => {
          if (v == null || v === '') return null;
          const n = typeof v === 'number' ? v : Number(v);
          return Number.isFinite(n) ? n : null;
        };
        const lat = parseCoord(parsed?.lat);
        const lng = parseCoord(parsed?.lng);
        if (until > Date.now()) {
          setNowEnRouteLockUntilMs(until);
          setNowEnRouteLockPoiName(poi);
          setNowEnRouteLockPoiId(poiId);
          setNowEnRouteLockLat(lat);
          setNowEnRouteLockLng(lng);
          if (parsed?.undo_available === true) {
            setNowUndoAvailable(true);
          }
        } else {
          localStorage.removeItem(EN_ROUTE_LOCK_STORAGE_KEY);
        }
      } catch {}
    }, [normalizePoiIdKey]);

    useEffect(() => {
      if (!nowEnRouteLockUntilMs) return undefined;
      const ms = nowEnRouteLockUntilMs - Date.now();
      if (ms <= 0) {
        setNowEnRouteLockUntilMs(0);
        setNowEnRouteLockPoiName('');
        setNowEnRouteLockPoiId('');
        setNowEnRouteLockLat(null);
        setNowEnRouteLockLng(null);
        return undefined;
      }
      const t = setTimeout(() => {
        setNowEnRouteLockUntilMs(0);
        setNowEnRouteLockPoiName('');
        setNowEnRouteLockPoiId('');
        setNowEnRouteLockLat(null);
        setNowEnRouteLockLng(null);
      }, ms + 20);
      return () => clearTimeout(t);
    }, [nowEnRouteLockUntilMs]);

    useEffect(() => {
      if (!(nowEnRouteLockUntilMs > Date.now())) return undefined;
      const t = setInterval(() => setNowLockTickMs(Date.now()), 1000);
      return () => clearInterval(t);
    }, [nowEnRouteLockUntilMs]);

    useEffect(() => {
      try {
        if (nowEnRouteLockUntilMs > Date.now()) {
          const payload = {
            until_ms: nowEnRouteLockUntilMs,
            poi_name: nowEnRouteLockPoiName || '',
            poi_id: nowEnRouteLockPoiId || '',
            undo_available: !!nowUndoAvailable
          };
          if (nowEnRouteLockLat != null && nowEnRouteLockLng != null) {
            payload.lat = nowEnRouteLockLat;
            payload.lng = nowEnRouteLockLng;
          }
          localStorage.setItem(EN_ROUTE_LOCK_STORAGE_KEY, JSON.stringify(payload));
        } else {
          localStorage.removeItem(EN_ROUTE_LOCK_STORAGE_KEY);
        }
      } catch {}
    }, [
      nowEnRouteLockUntilMs,
      nowEnRouteLockPoiName,
      nowEnRouteLockPoiId,
      nowEnRouteLockLat,
      nowEnRouteLockLng,
      nowUndoAvailable
    ]);

    useEffect(() => {
      const syncProfile = () => setProfile(window.SylvanFlowState?.getProfile?.() || null);
      syncProfile();
      if (window.SylvanFlowState?.on) {
        window.SylvanFlowState.on('profile', syncProfile);
        return () => window.SylvanFlowState.off('profile', syncProfile);
      }
      return undefined;
    }, []);

    useEffect(() => {
      let cancelled = false;
      if (!profile) {
        setProfilePhotoUrl(null);
        return undefined;
      }
      void (async () => {
        const url =
          window.SylvanProfilePhoto && typeof window.SylvanProfilePhoto.hydrate === 'function'
            ? await window.SylvanProfilePhoto.hydrate(profile, SF_API_BASE)
            : null;
        if (!cancelled) setProfilePhotoUrl(url);
      })();
      return () => {
        cancelled = true;
      };
    }, [profile?.user_key, profile?.profile_photo_r2_key, profile?.profile_photo_url, profile?.photo_url]);

    useEffect(() => {
      let cancelled = false;
      async function loadPastCorridorData() {
        let activities = [];
        let resolvedTripKey = null;
        let mutationTripKey = null;
        if (SF_API_BASE && window.SylvanFlowAuth?.authenticatedFetch) {
          try {
            let tripKey = null;
            const pickCorridorTripKey = (list) => {
              if (!list || list.length === 0) return null;
              const today = list.find((it) => it && (it.is_today_plan === true || it.is_today_plan === 'true'));
              const first = list[0];
              return (today || first).trip_key || (today || first).Trip_Key || null;
            };
            const pickTodayPlanTripKeyOnly = (list) => {
              if (!list || list.length === 0) return null;
              const today = list.find((it) => it && (it.is_today_plan === true || it.is_today_plan === 'true'));
              if (!today) return null;
              return today.trip_key || today.Trip_Key || null;
            };
            const cachedList = window.SylvanFlowState?.getItineraries?.();
            if (cachedList && cachedList.length > 0) {
              tripKey = pickCorridorTripKey(cachedList);
              mutationTripKey = pickTodayPlanTripKeyOnly(cachedList);
            }
            if (!tripKey) {
              const res = await window.SylvanFlowAuth.authenticatedFetch(
                `${SF_API_BASE}/api/itineraries?client_day_key=${encodeURIComponent(formatClientDayKeyLocal(new Date()))}`
              );
              window.SylvanFlowAuth.handleAuthResponse(res);
              if (res.ok && !cancelled) {
                const data = await res.json();
                const itins = data.itineraries || [];
                if (itins.length > 0) {
                  tripKey = pickCorridorTripKey(itins);
                  mutationTripKey = pickTodayPlanTripKeyOnly(itins);
                  if (window.SylvanFlowState?.setItineraries) window.SylvanFlowState.setItineraries(itins);
                }
              }
            }
            resolvedTripKey = tripKey;
            if (tripKey && !cancelled) {
              const res2 = await window.SylvanFlowAuth.authenticatedFetch(
                `${SF_API_BASE}/api/itineraries/${encodeURIComponent(tripKey)}`
              );
              window.SylvanFlowAuth.handleAuthResponse(res2);
              if (res2.ok && !cancelled) {
                const detail = await res2.json();
                const itinerary = detail.itinerary;
                if (itinerary) {
                  activities = flattenItineraryActivities(itinerary);
                }
              }
            }
          } catch (e) {
            if (!cancelled && window.uiLogger) {
              window.uiLogger.uiWarn('[NuAsk] past corridor load failed:', e?.message || e);
            }
          }
        }
        if (activities.length === 0) {
          mutationTripKey = null;
        }
        if (!cancelled) {
          corridorHydratedRef.current = true;
          setCorridorTripKey(resolvedTripKey);
          setCorridorMutationTripKey(mutationTripKey);
          setCorridorActivities(activities);
        }
      }
      loadPastCorridorData();
      const onItins = () => {
        if (!cancelled) loadPastCorridorData();
      };
      if (window.SylvanFlowState?.on) {
        window.SylvanFlowState.on('itineraries', onItins);
      }
      return () => {
        cancelled = true;
        if (window.SylvanFlowState?.off) window.SylvanFlowState.off('itineraries', onItins);
      };
    }, [corridorReloadTick]);

    useEffect(() => {
      const el = scrollerRef.current;
      if (!el) return undefined;

      const scrollToNow = () => {
        const w = el.clientWidth;
        if (w > 0) el.scrollLeft = w;
      };
      scrollToNow();
      requestAnimationFrame(() => requestAnimationFrame(scrollToNow));

      let settleTimer = null;
      let settling = false;

      const settleToNearestPanel = () => {
        const cw = el.clientWidth || 1;
        if (cw < 8) return;
        const i = Math.max(0, Math.min(2, Math.round(el.scrollLeft / cw)));
        const target = i * cw;
        setPanel(i);
        if (Math.abs(el.scrollLeft - target) > 2) {
          settling = true;
          if (typeof el.scrollTo === 'function') {
            el.scrollTo({ left: target, behavior: 'smooth' });
          } else {
            el.scrollLeft = target;
          }
          window.setTimeout(() => {
            settling = false;
          }, 320);
        }
      };

      const onScroll = () => {
        if (settling) return;
        if (settleTimer) clearTimeout(settleTimer);
        // Defer panel state until swipe settles — mid-swipe setPanel remounts A2 chrome
        // and can leave snap-x stuck between PAST / NOW / NEXT.
        settleTimer = window.setTimeout(settleToNearestPanel, 90);
      };

      const onScrollEnd = () => {
        if (settleTimer) clearTimeout(settleTimer);
        settleToNearestPanel();
      };

      el.addEventListener('scroll', onScroll, { passive: true });
      el.addEventListener('scrollend', onScrollEnd);
      return () => {
        if (settleTimer) clearTimeout(settleTimer);
        el.removeEventListener('scroll', onScroll);
        el.removeEventListener('scrollend', onScrollEnd);
      };
    }, []);

    const SpotlightTourCmp = typeof window !== 'undefined' ? window.SpotlightTour : null;
    const STS = typeof window !== 'undefined' ? window.SpotlightTourStorage : null;

    const beginCorridorTour = useCallback((force) => {
      setCorridorTourForce(!!force);
      setCorridorTourAwaitOpen(true);
    }, []);

    const closeCorridorTour = useCallback(
      (meta) => {
        setCorridorTourOpen(false);
        setCorridorTourForce(false);
        setCorridorTourAwaitOpen(false);
        const uk = profile?.user_key || (STS && STS.readUserKey());
        if (uk && STS && meta && meta.dontShowAgain) {
          STS.setNuaskCorridorTourSuppressed(uk, true);
        }
        setCorridorTourDontShow(false);
      },
      [profile?.user_key]
    );

    const corridorTourSteps = useMemo(
      () => [
        {
          resolveTarget: () => tourPanelNavRef.current,
          title: corridorTourLabels.corridorTitle,
          body: corridorTourLabels.corridorBody
        },
        {
          resolveTarget: () => tourNowRailRef.current,
          title: corridorTourLabels.nowTitle,
          body: corridorTourLabels.nowBody
        }
      ],
      [corridorTourLabels]
    );

    useEffect(() => {
      let cancelled = false;
      const lang = profile?.preferred_language || resolveNuAskPreferredLanguageCode() || 'en';
      const load = async (key, fb) => {
        if (typeof window.getSysMessage !== 'function') return fb;
        try {
          const v = await window.getSysMessage(key, lang);
          return v && String(v).trim() ? String(v).trim() : fb;
        } catch (_) {
          return fb;
        }
      };
      (async () => {
        const [skip, next, done, dontShowAgain, corridorTitle, corridorBody, nowTitle, nowBody] =
          await Promise.all([
            load('ui:nuask_tour_skip', 'Skip'),
            load('ui:nuask_tour_next', 'Next'),
            load('ui:nuask_tour_done', 'Done'),
            load('ui:nuask_tour_dont_show_again', corridorTourLabels.dontShowAgain),
            load('ui:nuask_tour_corridor_title', corridorTourLabels.corridorTitle),
            load('ui:nuask_tour_corridor_body', corridorTourLabels.corridorBody),
            load('ui:nuask_tour_now_title', corridorTourLabels.nowTitle),
            load('ui:nuask_tour_now_body', corridorTourLabels.nowBody)
          ]);
        if (cancelled) return;
        setCorridorTourLabels({
          skip,
          next,
          done,
          dontShowAgain,
          corridorTitle,
          corridorBody,
          nowTitle,
          nowBody
        });
      })();
      return () => {
        cancelled = true;
      };
    }, [profile?.preferred_language, profile?.user_key]);

    useEffect(() => {
      if (!STS) return undefined;
      const pending = STS.consumePendingTour(STS.TOUR_IDS.NUASK_CORRIDOR);
      if (pending) beginCorridorTour(!!pending.force);
      const onTour = (ev) => {
        const d = ev.detail;
        if (d && d.tourId === STS.TOUR_IDS.NUASK_CORRIDOR) {
          beginCorridorTour(!!d.force);
        }
      };
      window.addEventListener(STS.TOUR_EVENT, onTour);
      return () => window.removeEventListener(STS.TOUR_EVENT, onTour);
    }, [beginCorridorTour]);

    useEffect(() => {
      if (!corridorTourAwaitOpen) return undefined;
      const t = window.setTimeout(() => {
        setCorridorTourOpen(true);
        setCorridorTourAwaitOpen(false);
      }, 200);
      return () => window.clearTimeout(t);
    }, [corridorTourAwaitOpen]);

    useEffect(() => {
      if (corridorTourForce) return;
      if (!STS) return undefined;
      const uk = profile?.user_key || STS.readUserKey();
      if (!uk) return undefined;
      if (STS.isNuaskCorridorTourSuppressed(uk)) return undefined;
      const t = window.setTimeout(() => {
        if (showLocationPrompt || nowRecoError) return;
        setCorridorTourOpen(true);
      }, 500);
      return () => window.clearTimeout(t);
    }, [profile?.user_key, corridorTourForce, showLocationPrompt, nowRecoError]);

    const travelKmStraightToNext = useMemo(() => {
      if (!liveUserCoords || !nowNextCoords) return null;
      return haversineKm(
        liveUserCoords.lat,
        liveUserCoords.lng,
        nowNextCoords.lat,
        nowNextCoords.lng
      );
    }, [liveUserCoords, nowNextCoords]);

    const travelFromHereToNextMins = useMemo(() => {
      if (travelKmStraightToNext == null) return null;
      return roughUrbanDriveMinutes(travelKmStraightToNext);
    }, [travelKmStraightToNext]);

    const travelLeaveAlert = useMemo(() => {
      if (!hasActiveTripContext) return null;
      if (travelFromHereToNextMins == null || !Number.isFinite(Number(nowClock.minsUntilNextStart)) || nowClock.minsUntilNextStart <= 0 || !nowNextCoords) return null;
      const travelMins = travelFromHereToNextMins;
      const windowMins = nowClock.minsUntilNextStart;
      const nextTitle = asText(nowNextCard.title) || (nuAskUiStrings.yourNextStop || 'your next stop');
      if (travelMins >= windowMins) {
        return {
          tone: 'urgent',
          title: nuAskUiStrings.travelAlertUrgentTitle || 'Start travelling to your next stop now',
          body: (
            nuAskUiStrings.travelAlertUrgentBodyTemplate ||
            'About {travel_mins} min straight-line travel to “{next_title}”, but only about {window_mins} min until that activity — you should be on your way (estimates, not live traffic).'
          )
            .replace('{travel_mins}', String(travelMins))
            .replace('{next_title}', nextTitle)
            .replace('{window_mins}', String(windowMins))
        };
      }
      if (travelMins >= windowMins - 2) {
        return {
          tone: 'tight',
          title: nuAskUiStrings.travelAlertTightTitle || 'Tight — consider leaving now',
          body: (nuAskUiStrings.travelAlertTightBodyTemplate || '~{travel_mins} min travel to “{next_title}” vs ~{window_mins} min left. Buffer is thin.')
            .replace('{travel_mins}', String(travelMins))
            .replace('{next_title}', nextTitle)
            .replace('{window_mins}', String(windowMins))
        };
      }
      return null;
    }, [hasActiveTripContext, travelFromHereToNextMins, nowClock.minsUntilNextStart, nowNextCoords, nowNextCard.title, nuAskUiStrings]);

    travelLeaveAlertRef.current = travelLeaveAlert;

    /** No sidetrip POIs — travel time to next stop consumes (or exceeds) the remaining window. */
    const travelOnlyMode = travelLeaveAlert != null;

    /** Same null/empty guard as en-route localStorage restore — Number(null) is 0 (Null Island). */
    const nowEnRouteLockCoords = useMemo(() => {
      if (nowEnRouteLockLat == null || nowEnRouteLockLat === '') return null;
      if (nowEnRouteLockLng == null || nowEnRouteLockLng === '') return null;
      const lat = typeof nowEnRouteLockLat === 'number' ? nowEnRouteLockLat : Number(nowEnRouteLockLat);
      const lng = typeof nowEnRouteLockLng === 'number' ? nowEnRouteLockLng : Number(nowEnRouteLockLng);
      if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
      return { lat, lng };
    }, [nowEnRouteLockLat, nowEnRouteLockLng]);

    const enRouteLockActive = nowEnRouteLockUntilMs > Date.now();

    /** Decide→Go TV mode (content inside existing TOP rim). Hotbar shells still win upstream. */
    const nowTvMode = useMemo(() => {
      if (enRouteLockActive && nowEnRouteLockCoords) return 'go_added';
      if (travelLeaveAlert) return 'go_next';
      return 'decide';
    }, [enRouteLockActive, nowEnRouteLockCoords, travelLeaveAlert]);

    const showNowGoCard = nowTvMode === 'go_added' || nowTvMode === 'go_next';

    /** CN → Gaode; GLOBAL → Google. Prefer preview region (user); else live coords. */
    const mapsRegion = useMemo(
      () => resolveMapsRegion(nowMapsRegion, liveUserCoords),
      [nowMapsRegion, liveUserCoords]
    );

    const nextStopDirectionsUrl = useMemo(() => {
      if (!nowNextCoords) return '';
      return buildExternalDirectionsUrl(
        { ...nowNextCoords, name: asText(nowNextCard?.title) },
        liveUserCoords,
        mapsRegion
      );
    }, [nowNextCoords, nowNextCard?.title, liveUserCoords, mapsRegion]);

    /**
     * NEXT-only live future activities from itinerary (not DEMO, not nowNextCard ghosts).
     * Shared NOW/Go/HIS consumers keep using nowNextCard / nextAfterCard unchanged.
     */
    const nextLiveFuture = useMemo(() => {
      if (!hasActiveTripContext) return { first: null, second: null };
      const now = new Date();
      const future = (corridorActivities || [])
        .map((a) => {
          const dt = getActivityDateTime(a);
          return dt && dt.getTime() > now.getTime() ? { ...a, __dt: dt } : null;
        })
        .filter(Boolean)
        .sort((a, b) => a.__dt.getTime() - b.__dt.getTime());
      return { first: future[0] || null, second: future[1] || null };
    }, [hasActiveTripContext, corridorActivities]);
    const nextLiveFirst = nextLiveFuture.first;
    const nextLiveSecond = nextLiveFuture.second;
    const nextLiveFirstIdentity = resolveOccurrenceIdentity(nextLiveFirst);
    const nextLiveSecondIdentity = resolveOccurrenceIdentity(nextLiveSecond);
    const nextLiveFirstSameDayPeers = Array.isArray(corridorActivities)
      ? corridorActivities.filter(
          (a) => asText(a?.__dayKey) === asText(nextLiveFirst?.__dayKey)
        )
      : [];
    const nextLiveSecondSameDayPeers = Array.isArray(corridorActivities)
      ? corridorActivities.filter(
          (a) => asText(a?.__dayKey) === asText(nextLiveSecond?.__dayKey)
        )
      : [];
    const nextLiveFirstProviderMatchCount =
      nextLiveFirstIdentity.type !== 'none' && nextLiveFirstIdentity.type !== 'activity_key'
        ? nextLiveFirstSameDayPeers.filter(
            (a) => {
              const raw = a?.[nextLiveFirstIdentity.type];
              return typeof raw === 'string' && raw.trim() === nextLiveFirstIdentity.value;
            }
          ).length
        : 0;
    const nextLiveSecondProviderMatchCount =
      nextLiveSecondIdentity.type !== 'none' && nextLiveSecondIdentity.type !== 'activity_key'
        ? nextLiveSecondSameDayPeers.filter(
            (a) => {
              const raw = a?.[nextLiveSecondIdentity.type];
              return typeof raw === 'string' && raw.trim() === nextLiveSecondIdentity.value;
            }
          ).length
        : 0;
    const nextLiveFirstOccurrenceToken =
      nextLiveFirstIdentity.value &&
      (nextLiveFirstIdentity.type === 'activity_key' || nextLiveFirstProviderMatchCount === 1)
        ? `${nextLiveFirstIdentity.type}:${nextLiveFirstIdentity.value}`
        : '';
    const nextLiveSecondOccurrenceToken =
      nextLiveSecondIdentity.value &&
      (nextLiveSecondIdentity.type === 'activity_key' || nextLiveSecondProviderMatchCount === 1)
        ? `${nextLiveSecondIdentity.type}:${nextLiveSecondIdentity.value}`
        : '';
    const nextSelectedOccurrenceMatchCount =
      Number(Boolean(nextSelectedOccurrenceToken && nextSelectedOccurrenceToken === nextLiveFirstOccurrenceToken)) +
      Number(Boolean(nextSelectedOccurrenceToken && nextSelectedOccurrenceToken === nextLiveSecondOccurrenceToken));
    const nextSelectedSlotForRender =
      nextSelectedOccurrenceMatchCount === 1 &&
      nextSelectedOccurrenceToken === nextLiveSecondOccurrenceToken
        ? 'fa2'
        : 'fa1';
    useEffect(() => {
      const firstIsUnique =
        Boolean(nextLiveFirstOccurrenceToken) &&
        (!nextLiveSecondOccurrenceToken || nextLiveFirstOccurrenceToken !== nextLiveSecondOccurrenceToken);
      const resolvedToken =
        nextSelectedOccurrenceMatchCount === 1
          ? nextSelectedOccurrenceToken
          : firstIsUnique
            ? nextLiveFirstOccurrenceToken
            : '';
      if (nextSelectedSlot !== nextSelectedSlotForRender) setNextSelectedSlot(nextSelectedSlotForRender);
      if (nextSelectedOccurrenceToken !== resolvedToken) setNextSelectedOccurrenceToken(resolvedToken);
    }, [
      nextLiveFirstOccurrenceToken,
      nextLiveSecondOccurrenceToken,
      nextSelectedOccurrenceMatchCount,
      nextSelectedOccurrenceToken,
      nextSelectedSlot,
      nextSelectedSlotForRender
    ]);
    const nextSelectedActivity =
      nextSelectedSlotForRender === 'fa2' && nextLiveSecond ? nextLiveSecond : nextLiveFirst || null;
    const nextLiveTitle = asText(nextLiveFirst?.title) || asText(nextLiveFirst?.Title) || '';
    const nextLiveTime = nextLiveFirst ? formatActivityTimeLabel(nextLiveFirst) : '';
    const nextLiveSub = nextLiveFirst
      ? asText(nextLiveFirst.activity_type) ||
        asText(nextLiveFirst.Activity_Type) ||
        (getActivityLocationLabel(nextLiveFirst).split(',')[0] || '')
      : '';
    const nextLiveIcon = nextLiveFirst ? pickActivityIcon(nextLiveFirst, 1) : '◌';
    const nextLiveAfterTitle = asText(nextLiveSecond?.title) || asText(nextLiveSecond?.Title) || '';
    const nextLiveAfterTime = nextLiveSecond ? formatActivityTimeLabel(nextLiveSecond) : '';
    const nextLiveAfterSub = nextLiveSecond
      ? asText(nextLiveSecond.activity_type) ||
        asText(nextLiveSecond.Activity_Type) ||
        (getActivityLocationLabel(nextLiveSecond).split(',')[0] || '')
      : '';
    const nextLiveAfterIcon = nextLiveSecond ? pickActivityIcon(nextLiveSecond, 2) : '◌';

    const nextSelectedTitle = asText(nextSelectedActivity?.title) || asText(nextSelectedActivity?.Title) || '';
    const nextSelectedTime = nextSelectedActivity ? formatActivityTimeLabel(nextSelectedActivity) : '';
    const nextSelectedSub = nextSelectedActivity
      ? asText(nextSelectedActivity.activity_type) ||
        asText(nextSelectedActivity.Activity_Type) ||
        (getActivityLocationLabel(nextSelectedActivity).split(',')[0] || '')
      : '';
    const nextSelectedIcon = nextSelectedActivity
      ? pickActivityIcon(nextSelectedActivity, nextSelectedSlotForRender === 'fa2' ? 2 : 1)
      : '◌';
    const nextSelectedImageUrl = nextSelectedActivity ? getActivityImageUrl(nextSelectedActivity) : '';
    const nextSelectedCoords = nextSelectedActivity ? extractActivityCoords(nextSelectedActivity) : null;
    const nextSelectedBody = nextSelectedActivity
      ? asText(nextSelectedActivity.rich_description) ||
        asText(nextSelectedActivity.Rich_Description) ||
        asText(nextSelectedActivity.description) ||
        asText(nextSelectedActivity.Description) ||
        ''
      : '';
    const nextSelectedDetailTarget =
      nextSelectedActivity &&
      corridorTripKey &&
      nextSelectedActivity.__dayKey != null &&
      nextSelectedActivity.__activityIndex != null
        ? {
            tripKey: corridorTripKey,
            dayKey: nextSelectedActivity.__dayKey,
            activityIndex: nextSelectedActivity.__activityIndex,
            activity: nextSelectedActivity
          }
        : null;

    const nextLiveDirectionsUrl = useMemo(() => {
      if (!nextSelectedCoords) return '';
      return buildExternalDirectionsUrl(
        { ...nextSelectedCoords, name: nextSelectedTitle },
        liveUserCoords,
        mapsRegion
      );
    }, [nextSelectedCoords, nextSelectedTitle, liveUserCoords, mapsRegion]);

    /**
     * NEXT hero travel meta — semantic gate (UNLOCK_UI_GOLD scoped).
     * FREE TIME / physical / unknown → omit.
     * Journey → only itinerary-owned duration_min / distance_km (never phone GPS).
     * Maps / directions keep separate coordinate contracts.
     */
    const nextHighlightDistanceLine = useMemo(() => {
      const api =
        (typeof globalThis !== 'undefined' && globalThis.NuAskNextTravelMeta) ||
        (typeof window !== 'undefined' && window.NuAskNextTravelMeta) ||
        null;
      if (!api || typeof api.buildNextHighlightTravelMeta !== 'function') return '';
      const built = api.buildNextHighlightTravelMeta({
        activity: nextSelectedActivity,
        strings: {
          nextHighlightJourneyDurationTemplate:
            nuAskUiStrings.nextHighlightJourneyDurationTemplate || '{mins} min',
          nextHighlightJourneyDistanceTemplate:
            nuAskUiStrings.nextHighlightJourneyDistanceTemplate || '{km} km',
          nextHighlightJourneyMetaTemplate:
            nuAskUiStrings.nextHighlightJourneyMetaTemplate || '{mins} min · {km} km'
        }
      });
      return built && built.text ? String(built.text) : '';
    }, [nextSelectedActivity, nuAskUiStrings]);

    const nextPanelMapImageUrl = useMemo(() => {
      if (!nextSelectedCoords) return '';
      const lab = encodeURIComponent(mapLabelLetter(nextSelectedTitle));
      return `${SMART_BRAIN_MEDIA_BASE}/map-image?lat=${encodeURIComponent(String(nextSelectedCoords.lat))}&lng=${encodeURIComponent(String(nextSelectedCoords.lng))}&label=${lab}&zoom=14&size=1200x700`;
    }, [nextSelectedCoords, nextSelectedTitle]);

    const nextHighlightThumbMapUrl = useMemo(() => {
      if (!nextSelectedCoords) return '';
      const lab = encodeURIComponent(mapLabelLetter(nextSelectedTitle));
      return `${SMART_BRAIN_MEDIA_BASE}/map-image?lat=${encodeURIComponent(String(nextSelectedCoords.lat))}&lng=${encodeURIComponent(String(nextSelectedCoords.lng))}&label=${lab}&zoom=15&size=400x300`;
    }, [nextSelectedCoords, nextSelectedTitle]);

    const pastMapPoints = useMemo(() => {
      return pastCardTargets
        .map((t, idx) => {
          const coords = extractActivityCoords(t?.activity);
          if (!coords) return null;
          return {
            lat: coords.lat,
            lng: coords.lng,
            label: String(idx + 1),
            title: asText(t?.activity?.title) || asText(t?.activity?.Title) || `Stop ${idx + 1}`
          };
        })
        .filter(Boolean);
    }, [pastCardTargets]);

    const standbyPastCoords = useMemo(() => {
      const d = new Date();
      const seed = Number(`${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}`);
      const lat = 1.28 + ((seed % 17) - 8) * 0.0022;
      const lng = 103.85 + (((Math.floor(seed / 10)) % 19) - 9) * 0.0022;
      return { lat, lng };
    }, []);

    const pastMiniMapMode = useMemo(() => {
      if (!hasActiveTripContext) return 'standby_random';
      if (pastMapPoints.length > 0) return 'active_with_pins';
      return 'active_context';
    }, [hasActiveTripContext, pastMapPoints]);

    const pastMiniMapFocus = useMemo(() => {
      if (pastMiniMapMode === 'standby_random') return standbyPastCoords;
      if (pastMapPoints.length > 0) return pastMapPoints[pastMapPoints.length - 1];
      if (nowNextCoords) return nowNextCoords;
      if (liveUserCoords) return liveUserCoords;
      return standbyPastCoords;
    }, [pastMiniMapMode, standbyPastCoords, pastMapPoints, nowNextCoords, liveUserCoords]);

    const pastMiniMapImageUrl = useMemo(() => {
      if (pastMiniMapMode === 'active_with_pins' && pastMapPoints.length > 1) {
        const markersParam = pastMapPoints
          .map((p) => `${p.lat},${p.lng},${mapLabelLetter(p.label)}`)
          .join(';');
        const pathParam = pastMapPoints.map((p) => `${p.lat},${p.lng}`).join(';');
        return `${SMART_BRAIN_MEDIA_BASE}/map-image?markers=${encodeURIComponent(markersParam)}&path=${encodeURIComponent(pathParam)}&zoom=14&size=640x360&scale=2&maptype=roadmap`;
      }
      const focus = pastMiniMapMode === 'active_with_pins' ? pastMapPoints[0] : pastMiniMapFocus;
      if (!focus) return '';
      const lab = encodeURIComponent(
        pastMiniMapMode === 'standby_random' ? 'S' : mapLabelLetter(asText(focus.title) || 'P')
      );
      return `${SMART_BRAIN_MEDIA_BASE}/map-image?lat=${encodeURIComponent(String(focus.lat))}&lng=${encodeURIComponent(String(focus.lng))}&label=${lab}&zoom=14&size=640x360&scale=2&maptype=roadmap`;
    }, [pastMiniMapMode, pastMapPoints, pastMiniMapFocus]);
    const pastMiniMapSrc = pastMiniMapImgFailed ? '' : pastMiniMapImageUrl;

    const pastMiniMapLinkUrl = useMemo(() => {
      const useGaode = mapsRegion === 'CN';
      if (pastMiniMapMode === 'active_with_pins' && pastMapPoints.length > 1) {
        const first = pastMapPoints[0];
        const last = pastMapPoints[pastMapPoints.length - 1];
        const via = pastMapPoints.slice(1, -1);
        if (useGaode) {
          return `https://uri.amap.com/navigation?from=${encodeURIComponent(`${first.lng},${first.lat},Start`)}&to=${encodeURIComponent(`${last.lng},${last.lat},End`)}&mode=car&policy=1&src=sylvanflow&coordinate=gaode&callnative=1`;
        }
        const waypoints = via.map((p) => `${p.lat},${p.lng}`).join('|');
        const q = new URLSearchParams({
          api: '1',
          origin: `${first.lat},${first.lng}`,
          destination: `${last.lat},${last.lng}`,
          travelmode: 'driving'
        });
        if (waypoints) q.set('waypoints', waypoints);
        return `https://www.google.com/maps/dir/?${q.toString()}`;
      }
      if (!pastMiniMapFocus) return '';
      if (useGaode) {
        return `https://uri.amap.com/marker?position=${encodeURIComponent(`${pastMiniMapFocus.lng},${pastMiniMapFocus.lat}`)}&name=${encodeURIComponent(asText(pastMiniMapFocus.title) || 'Map point')}&src=sylvanflow&coordinate=gaode&callnative=1`;
      }
      return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${pastMiniMapFocus.lat},${pastMiniMapFocus.lng}`)}`;
    }, [pastMiniMapMode, pastMapPoints, pastMiniMapFocus, mapsRegion]);

    useEffect(() => {
      setPastMiniMapImgFailed(false);
    }, [pastMiniMapImageUrl]);

    const nowRecoPersistActive = nowRecoPersistUntilMs > Date.now();

    /** Orbit + TOP recommendation card: only after first sync completes; hidden again while a refresh is in flight. */
    const showNowRecoTopStack = nowRecoSyncPhase === 'ready' && !nowRecoLoading;
    /** NOW corridor strip + expand arrow — after itinerary hydrate (not recommendation completion). */
    const showNowCorridorChrome = corridorDataReady;
    const nowCiNowOpenDayKey = panel === 1 ? formatClientDayKeyLocal(new Date()) : '';
    const nowCiNowOpenLatchKey =
      panel === 1 && nowCiNowOpenDayKey && nowCiTemporalRole
        ? nowCiEvaluateLatchKey(nowCiNowOpenDayKey, nowCiTemporalRole)
        : '';
    const nowCiNowOpenPending =
      Boolean(nowCiNowOpenLatchKey) &&
      nowCiEvaluateDayKeyRef.current !== nowCiNowOpenLatchKey &&
      nowCiNowOpenAttemptKey !== nowCiNowOpenLatchKey;
    /** #175: fray until NOW hydrate + current-latch CI attempt settle (also during go_next) or Tap NOW preview. */
    const nowEnterTheFrayHold =
      nowRecoLoading || (panel === 1 && (!corridorDataReady || nowCiNowOpenPending));



    useEffect(() => {
      if (nowRecoSyncPhase === 'ready') setNowRecoEverReady(true);
    }, [nowRecoSyncPhase]);

    /** Background CI evaluate — after paint. NOW or NEXT. Does not trigger preview. NOW TV Enter the fray holds until this attempt key settles. */
    useEffect(() => {
      if (panel !== 1 && panel !== 2) return undefined;
      if (!corridorDataReady) return undefined;

      const now = new Date();
      const clientDayKey = formatClientDayKeyLocal(now);
      if (!clientDayKey) return undefined;
      if (!nowCiTemporalRole) return undefined;
      const latchKey = nowCiEvaluateLatchKey(clientDayKey, nowCiTemporalRole);
      const settleNowOpenAttempt = () => {
        setNowCiNowOpenAttemptKey(latchKey);
      };
      if (nowCiEvaluateDayKeyRef.current === latchKey) {
        settleNowOpenAttempt();
        return undefined;
      }
      if (nowCiEvaluateInFlightRef.current) return undefined;
      if (!SF_API_BASE || typeof window.SylvanFlowAuth?.authenticatedFetch !== 'function') {
        settleNowOpenAttempt();
        return undefined;
      }

      nowCiEvaluateInFlightRef.current = true;

      const clientLocalHhmm = formatClientLocalHHMM(now);
      const body = {
        client_day_key: clientDayKey,
        client_local_hhmm: clientLocalHhmm
      };
      if (panel === 1) {
        body.ambient_context_requested = true;
      }
      const lat = Number(liveUserCoords?.lat);
      const lng = Number(liveUserCoords?.lng);
      if (
        Number.isFinite(lat) &&
        Number.isFinite(lng) &&
        lat >= -90 &&
        lat <= 90 &&
        lng >= -180 &&
        lng <= 180
      ) {
        body.coords = { lat, lng };
      }

      void (async () => {
        try {
          const res = await window.SylvanFlowAuth.authenticatedFetch(
            `${SF_API_BASE}/api/context-intelligence/evaluate`,
            {
              method: 'POST',
              headers: {
              'Content-Type': 'application/json',
              ...(panel === 1 && nowTraceRidRef.current ? { 'x-request-id': nowTraceRidRef.current } : {})
            },
              body: JSON.stringify(body)
            }
          );
          if (typeof window.SylvanFlowAuth.handleAuthResponse === 'function') {
            window.SylvanFlowAuth.handleAuthResponse(res);
          }
          const data = await res.json().catch(() => null);
          if (data && (data.ok === true || ciEvaluateHasTrustedCurrentWeather(data))) {
            nowCiEvaluateDayKeyRef.current = latchKey;
            nowCiEvaluateResultRef.current = data;
            if (panel === 1 && (data.request_id || data.requestId)) {
              nowTraceRidRef.current = data.request_id || data.requestId;
            }
            if (window.NowTraceObservability && typeof window.NowTraceObservability.logNowTrace === 'function') {
              window.NowTraceObservability.logNowTrace(nowTraceRidRef.current, 'context_result', {
                ok: data.ok === true ? 1 : 0,
                prepare: (data.now_obs && data.now_obs.context_prepare_status) || data.preparation_state || 'unknown'
              });
            }
            setNowCiEvaluateResult({
              result: data,
              client_day_key: clientDayKey,
              client_local_hhmm: clientLocalHhmm
            });
          }
        } catch {
          /* silent network/parse — never toast or alter NOW chrome */
        } finally {
          nowCiEvaluateInFlightRef.current = false;
          settleNowOpenAttempt();
        }
      })();
    }, [panel, corridorDataReady, nowCiTemporalRole]);

    /** One NOW-only ambient evaluate after Branch C quiet / non-ambient when coords are available. */
    useEffect(() => {
      if (panel !== 1) return undefined;
      if (!corridorDataReady) return undefined;
      if (!SF_API_BASE || typeof window.SylvanFlowAuth?.authenticatedFetch !== 'function') return undefined;
      const prior = nowCiEvaluateResult?.result;
      if (!prior || prior.ok !== true) return undefined;
      if (prior.relevant_day_reason !== 'no_relevant_itinerary') return undefined;
      if (prior.preparation_state === 'ambient') return undefined;
      const lat = Number(liveUserCoords?.lat);
      const lng = Number(liveUserCoords?.lng);
      if (
        !Number.isFinite(lat) ||
        !Number.isFinite(lng) ||
        lat < -90 ||
        lat > 90 ||
        lng < -180 ||
        lng > 180
      ) {
        return undefined;
      }
      const now = new Date();
      const clientDayKey = formatClientDayKeyLocal(now);
      if (!clientDayKey) return undefined;
      if (nowCiAmbientGpsRetryDayKeyRef.current === clientDayKey) return undefined;
      if (nowCiEvaluateInFlightRef.current) return undefined;

      nowCiAmbientGpsRetryDayKeyRef.current = clientDayKey;
      nowCiEvaluateInFlightRef.current = true;

      const clientLocalHhmm = formatClientLocalHHMM(now);
      const body = {
        client_day_key: clientDayKey,
        client_local_hhmm: clientLocalHhmm,
        coords: { lat, lng },
        ambient_context_requested: true
      };

      void (async () => {
        try {
          const res = await window.SylvanFlowAuth.authenticatedFetch(
            `${SF_API_BASE}/api/context-intelligence/evaluate`,
            {
              method: 'POST',
              headers: {
              'Content-Type': 'application/json',
              ...(panel === 1 && nowTraceRidRef.current ? { 'x-request-id': nowTraceRidRef.current } : {})
            },
              body: JSON.stringify(body)
            }
          );
          if (typeof window.SylvanFlowAuth.handleAuthResponse === 'function') {
            window.SylvanFlowAuth.handleAuthResponse(res);
          }
          const data = await res.json().catch(() => null);
          if (data && data.ok === true) {
            nowCiEvaluateResultRef.current = data;
            if (panel === 1 && (data.request_id || data.requestId)) {
              nowTraceRidRef.current = data.request_id || data.requestId;
            }
            if (window.NowTraceObservability && typeof window.NowTraceObservability.logNowTrace === 'function') {
              window.NowTraceObservability.logNowTrace(nowTraceRidRef.current, 'context_result', {
                ok: data.ok === true ? 1 : 0,
                prepare: (data.now_obs && data.now_obs.context_prepare_status) || data.preparation_state || 'unknown'
              });
            }
            setNowCiEvaluateResult({
              result: data,
              client_day_key: clientDayKey,
              client_local_hhmm: clientLocalHhmm
            });
          }
        } catch {
          /* silent — keep prior quiet Branch C result; do not invent context */
        } finally {
          nowCiEvaluateInFlightRef.current = false;
        }
      })();
    }, [panel, corridorDataReady, liveUserCoords, nowCiEvaluateResult]);

    const nowCiOccupancyActivity =
      nowCurrentStop && nowCurrentStop.detailTarget && nowCurrentStop.detailTarget.activity
        ? nowCurrentStop.detailTarget.activity
        : null;
    const nowCiOccupancyPeers = useMemo(() => {
      const day =
        (nowCiOccupancyActivity && asText(nowCiOccupancyActivity.__dayKey)) ||
        formatClientDayKeyLocal(new Date());
      if (!day || !Array.isArray(corridorActivities)) return [];
      return corridorActivities.filter((a) => asText(a && a.__dayKey) === day);
    }, [corridorActivities, nowCiOccupancyActivity]);
    const nowCiOwnerKey = nowCiSituationOwnerKey(
      formatClientDayKeyLocal(new Date()),
      nowCiOccupancyActivity,
      nowCiEvaluateResult && nowCiEvaluateResult.result && nowCiEvaluateResult.result.culture_locality_key
    );
    const nowCiOwnerKeyRef = useRef(nowCiOwnerKey);
    useEffect(() => {
      if (nowCiOwnerKeyRef.current === nowCiOwnerKey) return;
      nowCiOwnerKeyRef.current = nowCiOwnerKey;
      setNowCiKeptEnrichment([]);
    }, [nowCiOwnerKey]);
    const nowCiDerivedLines = useMemo(
      () =>
        deriveNowCiVisibleLines(
          nowCiEvaluateResult,
          nowCiOccupancyActivity,
          nowCiOccupancyPeers,
          liveUserCoords,
          nuAskUiStrings
        ),
      [nowCiEvaluateResult, nowCiOccupancyActivity, nowCiOccupancyPeers, liveUserCoords, nuAskUiStrings]
    );
    const nowCiWeatherLine = useMemo(
      () =>
        formatTrustedCurrentWeatherLine(
          nowCiEvaluateResult &&
            nowCiEvaluateResult.result &&
            nowCiEvaluateResult.result.weather &&
            nowCiEvaluateResult.result.weather.current,
          nuAskUiStrings
        ),
      [nowCiEvaluateResult, nuAskUiStrings]
    );
    useEffect(() => {
      const enrichment = [];
      for (let i = 0; i < nowCiDerivedLines.length; i++) {
        const text = String(nowCiDerivedLines[i] || '').trim();
        if (!text || text === nowCiWeatherLine) continue;
        enrichment.push(text);
      }
      if (enrichment.length > 0) setNowCiKeptEnrichment(enrichment);
    }, [nowCiDerivedLines, nowCiWeatherLine, nowCiOwnerKey]);
    const nowCiVisibleLines = useMemo(() => {
      const retained = retainNowCiEnrichmentLines(
        nowCiDerivedLines,
        nowCiKeptEnrichment,
        nowCiWeatherLine
      );
      const currentWx =
        nowCiEvaluateResult &&
        nowCiEvaluateResult.result &&
        nowCiEvaluateResult.result.weather &&
        nowCiEvaluateResult.result.weather.current;
      return filterNowCiLinesAgainstWeather(retained, currentWx, nowCiWeatherLine);
    }, [nowCiDerivedLines, nowCiKeptEnrichment, nowCiWeatherLine, nowCiEvaluateResult]);
    const nowCurrentStopMapUrl = useMemo(() => {
      const coords = nowCurrentStop && nowCurrentStop.coords;
      if (!coords) return '';
      const lab = encodeURIComponent(mapLabelLetter(nowCurrentStop.title));
      return `${SMART_BRAIN_MEDIA_BASE}/map-image?lat=${encodeURIComponent(String(coords.lat))}&lng=${encodeURIComponent(String(coords.lng))}&label=${lab}&zoom=14&size=1200x700`;
    }, [nowCurrentStop]);
    useEffect(() => {
      if (nowTvMode !== 'go_next') return;
      if (nowCiVisibleLines.length !== 0) return;
      const result = nowCiEvaluateResult && nowCiEvaluateResult.result;
      let reason = 'not_called';
      if (result && typeof result === 'object') {
        if (result.ok !== true) reason = result.error_code || result.code || 'evaluate_not_ok';
        else if (result.preparation && result.preparation.nothing_material === true) reason = 'nothing_material';
        else if (result.preparation == null) reason = 'no_preparation';
        else reason = 'derived_empty';
      }
      console.info(`[NOW-CI] go_next nowCiVisibleLines.length=0 reason=${reason}`);
    }, [nowTvMode, nowCiVisibleLines.length, nowCiEvaluateResult]);
    const hasKnownWindowEnd = hasUpcomingActivity === true;
    const showAvailableWindowRange = hasKnownWindowEnd && Number(nowClock.mins) > 0;
    const deviceDayKey = formatClientDayKeyLocal(new Date());
    const nextSelectedDayKey = asText(nextSelectedActivity?.__dayKey) || '';
    const nextPreparationLanguage = normalizeNuAskUiLang(
      profile?.preferred_language || resolveNuAskPreferredLanguageCode() || 'en'
    );
    nextPreparationLanguageRef.current = nextPreparationLanguage;
    nextSelectedDayKeyRef.current = nextSelectedDayKey;

    useEffect(() => {
      if (panel !== 2) return undefined;
      if (!corridorDataReady) return undefined;
      if (!nextSelectedDayKey || !deviceDayKey) return undefined;
      // Do not spoof evaluate client_day_key
      if (!SF_API_BASE || typeof window.SylvanFlowAuth?.authenticatedFetch !== 'function') return undefined;
      const cacheKey = `${nextSelectedDayKey}:${nextPreparationLanguage}`;
      if (nextOtherDayPrepRef.current[cacheKey]) {
        setNextOtherDayPrep(nextOtherDayPrepRef.current[cacheKey]);
        return undefined;
      }
      if (nextOtherDayInFlightRef.current[cacheKey]) return undefined;
      nextOtherDayInFlightRef.current[cacheKey] = true;
      const dayKey = nextSelectedDayKey;
      const requestedLanguage = nextPreparationLanguage;
      void (async () => {
        try {
          const getRes = await window.SylvanFlowAuth.authenticatedFetch(
            `${SF_API_BASE}/api/context-intelligence/briefing?client_day_key=${encodeURIComponent(dayKey)}`
          );
          if (typeof window.SylvanFlowAuth.handleAuthResponse === 'function') {
            window.SylvanFlowAuth.handleAuthResponse(getRes);
          }
          const getData = await getRes.json().catch(() => null);
          if (
            nextPreparationLanguageRef.current !== requestedLanguage ||
            nextSelectedDayKeyRef.current !== dayKey
          ) {
            return;
          }
          if (
            getData &&
            getData.ok === true &&
            getData.state === 'fresh' &&
            getData.preparation &&
            normalizeNuAskUiLang(getData.language) === requestedLanguage
          ) {
            const packet = {
              dayKey,
              preparation: getData.preparation,
              request_id: getData.request_id || getData.requestId || null,
              source: 'briefing',
              preferred_language: requestedLanguage
            };
            nextOtherDayPrepRef.current[cacheKey] = packet;
            setNextOtherDayPrep(packet);
            return;
          }
          const prepRes = await window.SylvanFlowAuth.authenticatedFetch(
            `${SF_API_BASE}/api/context-intelligence/prepare`,
            {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ client_day_key: dayKey })
            }
          );
          if (typeof window.SylvanFlowAuth.handleAuthResponse === 'function') {
            window.SylvanFlowAuth.handleAuthResponse(prepRes);
          }
          const prepData = await prepRes.json().catch(() => null);
          if (
            nextPreparationLanguageRef.current !== requestedLanguage ||
            nextSelectedDayKeyRef.current !== dayKey
          ) {
            return;
          }
          if (
            prepData &&
            prepData.ok === true &&
            prepData.preparation &&
            normalizeNuAskUiLang(prepData.language) === requestedLanguage
          ) {
            const packet = {
              dayKey,
              preparation: prepData.preparation,
              request_id: prepData.request_id || prepData.requestId || null,
              source: 'prepare',
              preferred_language: requestedLanguage
            };
            nextOtherDayPrepRef.current[cacheKey] = packet;
            setNextOtherDayPrep(packet);
          }
        } catch {
          /* silent — Before You Go stays quiet */
        } finally {
          nextOtherDayInFlightRef.current[cacheKey] = false;
        }
      })();
      return undefined;
    }, [panel, corridorDataReady, nextSelectedDayKey, deviceDayKey, nextPreparationLanguage]);

    const nextSelectedPrepPacket = useMemo(() => {
      if (!nextSelectedDayKey) return null;
      if (
        nextOtherDayPrep &&
        nextOtherDayPrep.dayKey === nextSelectedDayKey &&
        nextOtherDayPrep.preferred_language === nextPreparationLanguage
      ) {
        return nextOtherDayPrep;
      }
      return nextOtherDayPrepRef.current[`${nextSelectedDayKey}:${nextPreparationLanguage}`] || null;
    }, [nextSelectedDayKey, nextPreparationLanguage, nextOtherDayPrep]);

    const nextBeforeYouGoBind = useMemo(() => {
      const peers = Array.isArray(corridorActivities)
        ? corridorActivities.filter((a) => asText(a?.__dayKey) === nextSelectedDayKey)
        : [];
      return matchNextSelectedPreparation(
        nextSelectedPrepPacket?.preparation,
        nextSelectedActivity,
        peers
      );
    }, [nextSelectedPrepPacket, nextSelectedActivity, corridorActivities, nextSelectedDayKey]);

    useEffect(() => {
      if (panel !== 2) return;
      logContextPrepBind({
        rid: nextSelectedPrepPacket?.request_id,
        dayKey: nextSelectedDayKey || deviceDayKey,
        slot: nextSelectedSlotForRender,
        bind: nextBeforeYouGoBind
      });
    }, [panel, nextSelectedDayKey, deviceDayKey, nextSelectedSlotForRender, nextBeforeYouGoBind, nextSelectedPrepPacket]);

    const openNextSelectedActivityDetail = useCallback(() => {
      const target = nextSelectedDetailTarget;
      if (!target || !window.appState?.set) return;
      window.appState.set({
        view: 'activity-detail',
        activityParams: {
          tripKey: target.tripKey,
          dayKey: target.dayKey,
          activityIndex: target.activityIndex,
          lang: resolveNuAskPreferredLanguageCode(),
          selectedActivity: target.activity
        }
      });
    }, [nextSelectedDetailTarget]);

    const openNowCurrentActivityDetail = useCallback(() => {
      const target = nowCurrentStop && nowCurrentStop.detailTarget;
      if (!target || !window.appState?.set) return;
      window.appState.set({
        view: 'activity-detail',
        activityParams: {
          tripKey: target.tripKey,
          dayKey: target.dayKey,
          activityIndex: target.activityIndex,
          lang: resolveNuAskPreferredLanguageCode(),
          selectedActivity: target.activity
        }
      });
    }, [nowCurrentStop]);
    /**
     * Scroll clearance for the fixed bottom dock (hotbar + inline Ask + twin nav).
     * Use measured height + gap — do not cap low or long WHY / alternatives copy ends under the dock.
     */
    const dockMeasuredH = nuAskNowDockHeight > 12 ? nuAskNowDockHeight : 0;
    const nuAskDockBottomPadPx = Math.max(
      36,
      dockMeasuredH > 0 ? dockMeasuredH + 6 : 96
    );

    const nuAskDockSectionBottomPadStyle = useMemo(
      () => ({
        paddingBottom: `calc(${nuAskDockBottomPadPx}px + env(safe-area-inset-bottom, 0px))`
      }),
      [nuAskDockBottomPadPx]
    );

    /** #131 phase 2: collapse TOP reco details when sync fails (location / warming error). */
    useEffect(() => {
      if (nowRecoError) setTopRecoExpanded(false);
    }, [nowRecoError]);

    /** #131: defer spotlight while NOW shows location/reco error — avoids coach over TOP plate (STB demo). */
    useEffect(() => {
      if (!nowRecoError && !showLocationPrompt) return undefined;
      setCorridorTourOpen(false);
      return undefined;
    }, [nowRecoError, showLocationPrompt]);

    useEffect(() => {
      if (panel !== 1) setNowTvAskOpen(false);
    }, [panel]);

    useEffect(() => {
      if (panel !== 1) {
        setNowHotkeysOpen(false);
        setNowHotkeysLeaving(false);
      }
    }, [panel]);

    useEffect(() => {
      if (panel !== 1) setNowCorridorExpanded(false);
    }, [panel]);

    useEffect(() => {
      if (!nowHotkeysOpen && !nowHotkeysLeaving) return undefined;
      const onKey = (e) => {
        if (e.key === 'Escape') closeNowHotkeys();
      };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [nowHotkeysOpen, nowHotkeysLeaving, closeNowHotkeys]);

    /** Pick a NOW hotkey — collapse A2 Tools sheet first, then existing openers. */
    const activateNowHotkey = useCallback(
      (h) => {
        if (!h || !h.key) return;
        closeNowHotkeys();
        if (h.key === 'emergency') {
          void startEmergencyFromNowHotbar();
          return;
        }
        if (h.key === 'nearby') {
          void openNearbyFromNowHotbar();
          return;
        }
        if (h.key === 'improve') {
          void openWeatherDaySuggestFromNowHotbar();
          return;
        }
        if (h.key === 'time') {
          void openUseMyTimeFromNowHotbar();
          return;
        }
        if (h.target) corridorNavigate(h.target);
      },
      [
        closeNowHotkeys,
        openNearbyFromNowHotbar,
        openUseMyTimeFromNowHotbar,
        openWeatherDaySuggestFromNowHotbar,
        startEmergencyFromNowHotbar
      ]
    );

    const goToPanel = useCallback((index) => {
      const el = scrollerRef.current;
      if (!el) return;
      const w = el.clientWidth || 1;
      const i = Math.max(0, Math.min(2, Math.floor(Number(index)) || 0));
      setPanel(i);
      if (typeof el.scrollTo === 'function') {
        el.scrollTo({ left: i * w, behavior: 'smooth' });
      } else {
        el.scrollLeft = i * w;
      }
    }, []);

    const corridorPanelHeader = useMemo(() => {
      if (panel === 0) {
        return { title: nuAskUiStrings.panelPastTitle, subtitle: nuAskUiStrings.pastSubtitle };
      }
      if (panel === 1) {
        return { title: nuAskUiStrings.panelNowTitle, subtitle: nuAskUiStrings.panelNowSubtitle };
      }
      return { title: nuAskUiStrings.panelNextTitle, subtitle: nuAskUiStrings.panelNextSubtitle };
    }, [panel, nuAskUiStrings]);

    const header = corridorPanelHeader;
    const panelSubtitle = panel === 0 ? nuAskUiStrings.pastSubtitle : header.subtitle;
    const displayName = profile?.name || profile?.user_name || 'User';
    const pastRealCount = useMemo(() => pastCardTargets.filter(Boolean).length, [pastCardTargets]);
    const showPastDetailBlocks = hasActiveTripContext && pastRealCount > 0;
    const nextStandbyMode = !hasActiveTripContext || !nextLiveFirst;
    const nextHonestEmptyCopy = hasActiveTripContext
      ? (nuAskUiStrings.noUpcomingItinerary || 'No upcoming activity yet')
      : (nuAskUiStrings.errNoTrip || 'No active trip linked');
    const nowPrevPlaceholder = !hasActiveTripContext || asText(nowPrevCard?.icon) === '◌';
    const nowNextPlaceholder = !hasActiveTripContext || !hasUpcomingActivity || asText(nowNextCard?.icon) === '◌';
    const enRouteRemainingLabel = useMemo(() => {
      const remainMs = Math.max(0, nowEnRouteLockUntilMs - nowLockTickMs);
      const totalSec = Math.floor(remainMs / 1000);
      const m = String(Math.floor(totalSec / 60)).padStart(2, '0');
      const s = String(totalSec % 60).padStart(2, '0');
      return `${m}:${s}`;
    }, [nowEnRouteLockUntilMs, nowLockTickMs]);
    const pastHeroTitle = useMemo(() => {
      if (!hasActiveTripContext) return `${nuAskUiStrings.panelPastTitle || 'PAST'} · STANDBY`;
      if (pastRealCount <= 1) return nuAskUiStrings.journeyMoments || (nuAskUiStrings.panelPastTitle || 'PAST');
      return `${nuAskUiStrings.greatStart || 'Great start!'} 🥳`;
    }, [hasActiveTripContext, pastRealCount, nuAskUiStrings]);
    const pastHeroBody = useMemo(() => {
      if (!hasActiveTripContext) return nuAskUiStrings.errNoTrip || 'No active trip linked';
      const tpl = nuAskUiStrings.placesExploredTemplate || "You've explored {count} places.";
      return String(tpl).replace('{count}', String(Math.max(0, placesActivityCount)));
    }, [hasActiveTripContext, placesActivityCount, nuAskUiStrings]);
    const hasLiveReco = nowRecoOptions.length > 0 && (nowRecoPersistActive || !travelOnlyMode);
    /** POI hours footnote must not depend on SYS hydration alone — other NOW lines use `|| '…'` fallbacks. */
    const poiHoursVerifyDisclaimerResolved = useMemo(() => {
      const s = nuAskUiStrings.poiHoursVerifyDisclaimer;
      if (typeof s === 'string' && s.trim()) return s.trim();
      try {
        const d =
          typeof window !== 'undefined' && window.NUASK_UI_DEFAULTS?.poiHoursVerifyDisclaimer;
        if (typeof d === 'string' && d.trim()) return d.trim();
      } catch (e) {
        /* noop */
      }
      return 'Hours and listings can be wrong or out of date. Before you go, confirm the place is still open and still operating (not permanently closed).';
    }, [nuAskUiStrings.poiHoursVerifyDisclaimer]);
    const availableWindowBridgeText = useMemo(() => {
      if (!hasActiveTripContext) return '';
      const tpl = nuAskUiStrings.nowAvailableWindowBridgeTemplate;
      if (!tpl || String(tpl).trim() === '') return '';
      const nextTitle = asText(nowNextCard?.title);
      if (!nextTitle || !nowClock.endLabel) return '';
      const windowMins = formatDurationMins(nowClock.mins);
      return String(tpl)
        .replace(/\{window_mins\}/g, windowMins)
        .replace(/\{next_title\}/g, nextTitle)
        .replace(/\{end_time\}/g, nowClock.endLabel);
    }, [
      hasActiveTripContext,
      nuAskUiStrings.nowAvailableWindowBridgeTemplate,
      nowClock.mins,
      nowClock.endLabel,
      nowNextCard?.title
    ]);
    const addPostStartWindowActive = useMemo(() => {
      if (!hasActiveTripContext || !Number.isFinite(nowCorridorPrevStartMs) || nowCorridorPrevStartMs <= 0) {
        return false;
      }
      const t = nowLockTickMs;
      return t >= nowCorridorPrevStartMs && t < nowCorridorPrevStartMs + NUASK_ACTIVITY_POST_START_ADD_LOCK_MS;
    }, [hasActiveTripContext, nowCorridorPrevStartMs, nowLockTickMs]);
    const nowOrbTapLocked = addPostStartWindowActive === true || nowTvMode === 'go_next';
    const showAvailableWindowChrome = addPostStartWindowActive !== true;

    // #185: leave NOW closes the RID session so re-entry mints a fresh RID.
    useEffect(() => {
      if (panel === 1) return undefined;
      if (nowTraceRidRef.current) {
        nowTraceRidRef.current = null;
        nowTraceFinalEmittedRef.current = false;
        nowTraceFinalFingerprintRef.current = null;
      }
      return undefined;
    }, [panel]);

    useEffect(() => {
      if (panel !== 1) return undefined;
      const NTO = window.NowTraceObservability;
      if (!NTO || typeof NTO.logNowTrace !== 'function') return undefined;
      if (!nowTraceRidRef.current) {
        nowTraceRidRef.current = NTO.generateNowTraceRid ? NTO.generateNowTraceRid() : ('now-' + Date.now());
        nowTraceFinalEmittedRef.current = false;
        nowTraceFinalFingerprintRef.current = null;
        NTO.logNowTrace(nowTraceRidRef.current, 'now_open', { build: 'sfRev108' });
      }
      if (corridorDataReady) {
        NTO.logNowTrace(nowTraceRidRef.current, 'itinerary_hydrated', { ok: 1 });
      }
      if (nowCiTemporalRole) {
        NTO.logNowTrace(nowTraceRidRef.current, 'temporal_roles', { role: String(nowCiTemporalRole).slice(0, 32) });
      }
      // CURRENT write-up contract: nowCurrentStop.body only (never invent writeUp/description).
      const hasCurrent = Boolean(nowCurrentStop && nowCurrentStop.title);
      const hasWriteup = Boolean(
        hasCurrent && nowCurrentStop && typeof nowCurrentStop.body === 'string' && nowCurrentStop.body.trim()
      );
      const hasImage = Boolean(hasCurrent && nowCurrentStop && nowCurrentStop.imageUrl);
      NTO.logNowTrace(nowTraceRidRef.current, 'current_materialized', {
        present: hasCurrent ? 1 : 0,
        writeup: hasWriteup ? 1 : 0,
        image: hasImage ? 1 : 0
      });
      // AW / today / region: canonical runtime only — never invent.
      const todayTripSelected = Boolean(corridorMutationTripKey);
      const awPresent = Boolean(hasActiveTripContext);
      const awVisible = Boolean(awPresent && showAvailableWindowChrome);
      let awHideReason = 'none';
      if (!awPresent) awHideReason = 'no_trip_context';
      else if (!awVisible) awHideReason = addPostStartWindowActive ? 'post_start_lock' : 'unknown';
      NTO.logNowTrace(nowTraceRidRef.current, 'available_window', {
        present: awPresent ? 1 : 0,
        visible: awVisible ? 1 : 0,
        reason: awHideReason
      });
      const gpsOk = Number.isFinite(Number(liveUserCoords?.lat)) && Number.isFinite(Number(liveUserCoords?.lng));
      NTO.logNowTrace(nowTraceRidRef.current, 'location_ready', { gps: gpsOk ? 1 : 0 });
      const storedRegion = String(nowMapsRegion || '').trim().toUpperCase();
      const region =
        storedRegion === 'CN' || storedRegion === 'GLOBAL'
          ? storedRegion
          : gpsOk
            ? mapsRegion
            : 'unknown';
      if (!nowEnterTheFrayHold) {
        NTO.logNowTrace(nowTraceRidRef.current, 'fray_settled', { settled: 1 });
        const evalResult = nowCiEvaluateResult?.result || nowCiEvaluateResultRef?.current || null;
        const obs = (evalResult && evalResult.now_obs) || {};
        const sections = [];
        if (hasCurrent) sections.push('current');
        if (awVisible) sections.push('available_window');
        if ((nowCiVisibleLines || []).length) sections.push('context_intelligence');
        const prepareStatus = obs.context_prepare_status || (evalResult && evalResult.ok ? 'ok' : 'unknown');
        const fingerprint = [
          hasCurrent ? 1 : 0,
          hasWriteup ? 1 : 0,
          hasImage ? 1 : 0,
          awPresent ? 1 : 0,
          awVisible ? 1 : 0,
          awHideReason,
          (nowCiVisibleLines || []).length,
          prepareStatus,
          obs.provider_model || 'none',
          Number(obs.search_calls) || 0,
          sections.join(',')
        ].join('|');
        // Emit FINAL only when settled; re-emit if late CI changes the outcome fingerprint.
        if (nowTraceFinalFingerprintRef.current !== fingerprint && typeof NTO.logNowTraceFinal === 'function') {
          nowTraceFinalFingerprintRef.current = fingerprint;
          nowTraceFinalEmittedRef.current = true;
          const finalInput = {
            rid: nowTraceRidRef.current,
            build: 'sfRev108',
            itinerary_hydrated: Boolean(corridorDataReady),
            today_trip_selected: todayTripSelected,
            current_present: hasCurrent,
            current_writeup_present: hasWriteup,
            current_image_present: hasImage,
            available_window_present: awPresent,
            available_window_visible: awVisible,
            available_window_hide_reason: awHideReason,
            gps_available: gpsOk,
            region,
            reverse_geocode_status: obs.reverse_geocode || 'none',
            locality_established: Number(obs.locality) === 1,
            culture_cache: obs.culture_cache || 'not_applicable',
            culture_records_count: Number(obs.culture_records) || 0,
            context_prepare_status: prepareStatus,
            requested_model: obs.requested_model || 'none',
            provider_model: obs.provider_model || 'none',
            search_call_count: Number(obs.search_calls) || 0,
            provider_latency_ms: Number(obs.provider_latency_ms) || 0,
            context_intelligence_visible_line_count: (nowCiVisibleLines || []).length,
            fray_hold_settled: true,
            final_visible_sections: sections
          };
          const summary = NTO.logNowTraceFinal(finalInput);
          NTO.logNowTrace(nowTraceRidRef.current, 'render_outcome', {
            blank: sections.length ? 0 : 1,
            visible: sections.join(',') || 'none'
          });
          // Backend-searchable FINAL via existing authenticated /api/chat/event (log-only type).
          void logNuAskChatEvent('now_trace_final', 'now_trace_final', {
            rid: nowTraceRidRef.current,
            build: 'sfRev108',
            itinerary_hydrated: Boolean(corridorDataReady),
            today_trip_selected: todayTripSelected,
            current_present: hasCurrent,
            current_writeup_present: hasWriteup,
            current_image_present: hasImage,
            available_window_present: awPresent,
            available_window_visible: awVisible,
            available_window_hide_reason: awHideReason,
            gps_available: gpsOk,
            region,
            reverse_geocode_status: obs.reverse_geocode || 'none',
            locality_established: Number(obs.locality) === 1,
            culture_cache: obs.culture_cache || 'not_applicable',
            culture_records_count: Number(obs.culture_records) || 0,
            context_prepare_status: prepareStatus,
            requested_model: obs.requested_model || 'none',
            provider_model: obs.provider_model || 'none',
            search_call_count: Number(obs.search_calls) || 0,
            provider_latency_ms: Number(obs.provider_latency_ms) || 0,
            context_intelligence_visible_line_count: (nowCiVisibleLines || []).length,
            fray_hold_settled: true,
            final_visible_sections: sections.join(','),
            blank: summary && summary.blank ? 1 : 0,
            reasons: (summary && summary.reasons) || 'none'
          });
        }
      }
      return undefined;
    }, [panel, corridorDataReady, nowCiTemporalRole, nowCurrentStop, showAvailableWindowChrome, addPostStartWindowActive, hasActiveTripContext, corridorMutationTripKey, liveUserCoords, nowMapsRegion, mapsRegion, nowEnterTheFrayHold, nowCiEvaluateResult, nowCiVisibleLines, logNuAskChatEvent]);


    const patchMicroDisabledReason = useMemo(() => {
      if (nowRecoActionLoading) return nuAskUiStrings.patchDisabledLoading || '';
      if (enRouteLockActive) return nuAskUiStrings.enRouteReplaceOnlyBody || '';
      if (!hasLiveReco) return nuAskUiStrings.patchDisabledNoReco || '';
      if (addPostStartWindowActive) return nuAskUiStrings.addHerePostStartLockHint || '';
      return '';
    }, [nowRecoActionLoading, enRouteLockActive, hasLiveReco, addPostStartWindowActive, nuAskUiStrings]);
    const replaceNextDisabledReason = useMemo(() => {
      if (nowRecoActionLoading) return nuAskUiStrings.patchDisabledLoading || '';
      if (!hasLiveReco) return nuAskUiStrings.patchDisabledNoReco || '';
      if (nowNextPlaceholder) return nuAskUiStrings.replaceNextNoFutureActivity || 'No next activity to replace yet.';
      if (nowNextHardBooking) {
        return nuAskUiStrings.patchHardBookingProtected || nuAskUiStrings.replaceNextHardBookingHint || '';
      }
      return '';
    }, [nowRecoActionLoading, hasLiveReco, nowNextPlaceholder, nowNextHardBooking, nuAskUiStrings]);
    const replaceNextDisabled =
      !hasLiveReco || !!nowRecoActionLoading || nowNextPlaceholder || nowNextHardBooking;
    /** Mode B (go_added): left = Undo; Replace Next stays visible but forced disabled. */
    const goAddedActionRow = nowTvMode === 'go_added';
    /** Decide third slot needs select-response flag; Mode B enables Undo with today trip key (server decides). */
    const showUndoAction = nowUndoAvailable && !!corridorMutationTripKey;
    const showUndoActionGoAdded = goAddedActionRow && !!corridorMutationTripKey;
    const replaceNextForcedOff = goAddedActionRow || replaceNextDisabled;
    const replaceNextForcedOffReason = goAddedActionRow
      ? nuAskUiStrings.enRouteReplaceOnlyBody ||
        'Changed your mind? Use Undo. Replace Next is paused for about 30 minutes.'
      : replaceNextDisabledReason;
    const showUndoThirdSlot = showUndoAction && !goAddedActionRow;
    const undoLeftDisabled = !(showUndoActionGoAdded || showUndoAction) || !!nowRecoActionLoading;
    const undoLeftTitle = !corridorMutationTripKey
      ? nuAskUiStrings.undoFailed || 'Could not undo the last add.'
      : nuAskUiStrings.undoLastAddHint || 'Remove the last Add Here from today\'s plan';
    const fixedBookingBadgeEl =
      nowNextHardBooking && !nowNextPlaceholder ? (
        <span className="inline-block rounded border border-amber-500/35 bg-amber-950/50 px-1 py-px text-[9px] font-bold uppercase tracking-wide text-amber-200/95">
          {nuAskUiStrings.fixedBookingBadge || 'Fixed'}
        </span>
      ) : null;

    const nowGoCardModel = useMemo(() => {
      if (nowTvMode === 'go_added' && nowEnRouteLockCoords) {
        const poi =
          nowEnRouteLockPoiName ||
          asText(nowNextCard?.title) ||
          (nuAskUiStrings.panelNowTitle || 'NOW');
        const km =
          liveUserCoords != null
            ? haversineKm(
                liveUserCoords.lat,
                liveUserCoords.lng,
                nowEnRouteLockCoords.lat,
                nowEnRouteLockCoords.lng
              )
            : null;
        const mins = km != null && Number.isFinite(Number(km)) ? roughUrbanDriveMinutes(Number(km)) : null;
        return {
          variant: 'added',
          title: poi,
          coords: nowEnRouteLockCoords,
          headerLabel: nuAskUiStrings.goOnTheWay || 'On the way',
          badgeLabel: enRouteRemainingLabel || (nuAskUiStrings.goNow || 'Go now'),
          alertTone: 'amber',
          alertTitle: (nuAskUiStrings.enRouteReplaceOnlyTitle || 'On the way to {poi}.').replace(
            /\{poi\}/g,
            poi
          ),
          alertBody:
            nuAskUiStrings.enRouteReplaceOnlyBody ||
            'Changed your mind? Use Undo. Replace Next is paused for about 30 minutes.',
          etaMins: mins,
          kmStraight: km,
          directionsUrl: buildExternalDirectionsUrl(
            { ...nowEnRouteLockCoords, name: poi },
            liveUserCoords,
            mapsRegion
          ),
          timeSub: (nuAskUiStrings.enRouteCountdownTemplate || 'About {time} left on this Go lock').replace(
            /\{time\}/g,
            enRouteRemainingLabel || '—'
          )
        };
      }
      if (nowTvMode === 'go_next' && travelLeaveAlert) {
        return {
          variant: 'next',
          title: asText(nowNextCard.title),
          coords: nowNextCoords,
          headerLabel: nuAskUiStrings.yourNextStop || 'Your next stop',
          badgeLabel: nuAskUiStrings.goNow || 'Go now',
          alertTone: travelLeaveAlert.tone === 'urgent' ? 'urgent' : 'amber',
          alertTitle: travelLeaveAlert.title,
          alertBody: travelLeaveAlert.body,
          etaMins: travelFromHereToNextMins,
          kmStraight: travelKmStraightToNext,
          directionsUrl: nextStopDirectionsUrl,
          timeSub: `${nowNextCard.time} · ${nuAskUiStrings.untilWord || 'until'} ${nowClock.endLabel}`
        };
      }
      return null;
    }, [
      nowTvMode,
      nowEnRouteLockCoords,
      nowEnRouteLockPoiName,
      nowNextCard,
      nuAskUiStrings,
      enRouteRemainingLabel,
      liveUserCoords,
      travelLeaveAlert,
      nowNextCoords,
      travelFromHereToNextMins,
      travelKmStraightToNext,
      nextStopDirectionsUrl,
      nowClock.endLabel,
      mapsRegion
    ]);

    /** En-route Go still takes the whole plate. Leave-soon paints on LEFT (not a plate replace). */
    const showNowEnRouteGo = nowTvMode === 'go_added' && !!nowGoCardModel;
    const showNowLeaveSoonOnLeft = nowTvMode === 'go_next' && !!travelLeaveAlert;
    /** Pin LEFT/RIGHT A2 arrow to the plate (not the inner scroller). Hide while exclusive shells own the TV. */
    const showNowInnerPaneArrow =
      showNowRecoTopStack &&
      !showNowEnRouteGo &&
      !nowEnterTheFrayHold &&
      !nowHotbarDetail &&
      !nowNearbyOpen &&
      !nowWeatherPlanOpen &&
      !nowUseMyTimeOpen &&
      !nowTvAskOpen;

    const resolveNuAskRecoTierBadge = (opt) => {
      const tier = opt?.recommendation_tier;
      if (tier === 'strong_fit') return nuAskUiStrings.strongFitDefault || 'STRONG FIT';
      if (tier === 'personalized') return nuAskUiStrings.recoPersonalizedBadge || 'PERSONALIZED';
      if (tier === 'alternative') return nuAskUiStrings.recoNextBestBadge || 'NEXT BEST';
      return '';
    };
    const selectedRecoIdx = nowRecoOptions.findIndex((opt, idx) => getRecoKey(opt, idx) === selectedRecoKey);
    const topReco = (selectedRecoIdx >= 0 ? nowRecoOptions[selectedRecoIdx] : nowRecoOptions[0]) || null;
    const addHereDisabled =
      !hasLiveReco || !!nowRecoActionLoading || enRouteLockActive || addPostStartWindowActive;
    const addHereDisabledBannerLines = useMemo(() => {
      if (!addHereDisabled) return null;
      if (nowRecoActionLoading) {
        const line = nuAskUiStrings.patchDisabledLoading || 'Wait for the current action to finish.';
        return line.trim() ? [line] : null;
      }
      if (enRouteLockActive) {
        const poi =
          nowEnRouteLockPoiName ||
          topReco?.poi?.name ||
          asText(nowNextCard?.title) ||
          (nuAskUiStrings.panelNowTitle || 'NOW');
        const titleLine = (nuAskUiStrings.enRouteReplaceOnlyTitle || 'On the way to {poi}.')
          .replace(/\{poi\}/g, poi);
        const body = nuAskUiStrings.enRouteReplaceOnlyBody || '';
        const countdown = (nuAskUiStrings.enRouteCountdownTemplate || 'About {time} left on this Go lock').replace(
          /\{time\}/g,
          enRouteRemainingLabel
        );
        return [titleLine, body, countdown].map((s) => String(s || '').trim()).filter(Boolean);
      }
      if (!hasLiveReco) {
        const line = nuAskUiStrings.patchDisabledNoReco || '';
        return line.trim() ? [line] : null;
      }
      if (addPostStartWindowActive) {
        const line =
          nuAskUiStrings.addHerePostStartLockHint ||
          'During the first 30 minutes after your current activity starts, Add here stays off—use Replace next, or wait until the half-hour mark if time allows.';
        return line.trim() ? [line] : null;
      }
      return null;
    }, [
      addHereDisabled,
      nowRecoActionLoading,
      enRouteLockActive,
      hasLiveReco,
      addPostStartWindowActive,
      nuAskUiStrings,
      nowEnRouteLockPoiName,
      topReco?.poi?.name,
      nowNextCard?.title,
      enRouteRemainingLabel
    ]);
    const strongFitIdx = nowRecoOptions.findIndex((opt) => opt?.recommendation_tier === 'strong_fit');
    const rankedTopReco = (strongFitIdx >= 0 ? nowRecoOptions[strongFitIdx] : nowRecoOptions[0]) || null;
    const rankedTopKey = rankedTopReco != null
      ? getRecoKey(rankedTopReco, strongFitIdx >= 0 ? strongFitIdx : 0)
      : null;
    const altRecos = nowRecoOptions
      .map((opt, fullIdx) => ({ opt, fullIdx }))
      .filter(({ fullIdx }) => (selectedRecoIdx >= 0 ? fullIdx !== selectedRecoIdx : fullIdx !== 0))
      .slice(0, 2);
    const topRecoFirstPhoto =
      typeof topReco?.poi?.photos?.[0] === 'string'
        ? topReco?.poi?.photos?.[0]
        : (topReco?.poi?.photos?.[0]?.url || topReco?.poi?.photos?.[0]?.src || '');
    const topRecoPhotoCandidates = useMemo(() => {
      const rows = [
        resolveRecoMediaImageUrl(topReco?.poi),
        normalizeRecoImageUrl(topRecoFirstPhoto),
        normalizeRecoImageUrl(topReco?.poi?.photo_url),
        normalizeRecoImageUrl(topReco?.poi?.image_url),
        Number.isFinite(Number(topReco?.poi?.lat)) && Number.isFinite(Number(topReco?.poi?.lng))
          ? `${SMART_BRAIN_MEDIA_BASE}/map-image?lat=${encodeURIComponent(String(topReco?.poi?.lat))}&lng=${encodeURIComponent(String(topReco?.poi?.lng))}&label=${encodeURIComponent((topReco?.poi?.name || 'P').slice(0, 1))}&zoom=15&size=1200x700`
          : '',
        normalizeRecoImageUrl(nowNextImageUrl),
        nowNextCoords &&
        Number.isFinite(Number(nowNextCoords.lat)) &&
        Number.isFinite(Number(nowNextCoords.lng))
          ? `${SMART_BRAIN_MEDIA_BASE}/map-image?lat=${encodeURIComponent(String(nowNextCoords.lat))}&lng=${encodeURIComponent(String(nowNextCoords.lng))}&label=${encodeURIComponent((asText(nowNextCard?.title) || 'N').slice(0, 1))}&zoom=15&size=1200x700`
          : ''
      ];
      const seen = new Set();
      const out = [];
      for (const u of rows) {
        if (!u || typeof u !== 'string') continue;
        const t = u.trim();
        if (!t || seen.has(t)) continue;
        seen.add(t);
        out.push(t);
      }
      return out;
    }, [topReco, topRecoFirstPhoto, nowNextImageUrl, nowNextCoords, nowNextCard?.title]);
    const topRecoPhotoUrl = topRecoPhotoCandidates[topRecoImageIdx] || '';
    const topRecoName =
      topReco?.poi?.name || topReco?.summary || nuAskUiStrings.noLiveRecommendationYet || 'No live recommendation yet';
    const topRecoDistance = formatDistanceLabel(topReco?.poi?.distance_km) || '';
    const topRecoReviewCount = topReco?.poi?.review_count ?? topReco?.poi?.user_ratings_total ?? 0;
    const topRecoRating = Number.isFinite(Number(topReco?.poi?.rating))
      ? `${Number(topReco?.poi?.rating).toFixed(1)} (${topRecoReviewCount} ${nuAskUiStrings.reviewsSuffix || 'reviews'})`
      : '';
    const topRecoDesc =
      topReco?.poi?.rich_description ||
      topReco?.poi?.description ||
      nuAskUiStrings.recoRefreshHint;
    const topRecoCategory = topReco?.poi?.category || topReco?.microAction?.category || '';
    const displayRecoIdxForTag = selectedRecoIdx >= 0 ? selectedRecoIdx : 0;
    const displayRecoKeyForTag = topReco ? getRecoKey(topReco, displayRecoIdxForTag) : null;
    const isTopRecoRankedFirst =
      hasLiveReco &&
      rankedTopKey != null &&
      displayRecoKeyForTag != null &&
      displayRecoKeyForTag === rankedTopKey;
    /** Match `topRecoTag`: badge can show STRONG FIT from rank when `recommendation_tier` is absent. */
    const topRecoHudCaption = useMemo(() => {
      const tier =
        topReco?.recommendation_tier ||
        (topReco && isTopRecoRankedFirst ? 'strong_fit' : null);
      if (tier === 'strong_fit') {
        return (
          nuAskUiStrings.recoTierCaptionStrongFit ||
          'Best overall catalog match for right now.'
        );
      }
      if (tier === 'personalized') {
        return (
          nuAskUiStrings.recoTierCaptionPersonalized ||
          'Boosted for your tags—not just nearby.'
        );
      }
      if (tier === 'alternative') {
        return (
          nuAskUiStrings.recoTierCaptionAlternative ||
          'Strong next choice—same vetting as the top two.'
        );
      }
      return nuAskUiStrings.catalogRankedTimeFeasible || 'Catalog-ranked · Fits your window';
    }, [
      topReco?.recommendation_tier,
      topReco,
      isTopRecoRankedFirst,
      nuAskUiStrings.recoTierCaptionStrongFit,
      nuAskUiStrings.recoTierCaptionPersonalized,
      nuAskUiStrings.recoTierCaptionAlternative,
      nuAskUiStrings.catalogRankedTimeFeasible
    ]);
    const topRecoApiLabel = topReco?.label && String(topReco.label).trim();
    const tierBadgeLabel = topReco ? resolveNuAskRecoTierBadge(topReco) : '';
    const topRecoTag = topReco
      ? tierBadgeLabel ||
        (isTopRecoRankedFirst
          ? topRecoApiLabel || nuAskUiStrings.strongFitDefault || DEMO.reco.tag
          : topRecoApiLabel || nuAskUiStrings.recoAlternativeBadge || 'Alternative')
      : '';
    const nuAskExplainBangBtnClass =
      'inline-flex h-[17px] min-w-[17px] shrink-0 items-center justify-center rounded-full border border-amber-400/55 bg-gradient-to-b from-amber-500/28 to-amber-950/45 px-[3px] text-[13px] font-black leading-none text-amber-50 shadow-[0_0_10px_rgba(251,191,36,0.28)] ring-1 ring-amber-300/20 active:scale-[0.94]';
    const nuAskExplainBubbleBoxClass =
      'w-[min(19.5rem,calc(100vw-2.25rem))] max-w-[calc(100%-0.5rem)] rounded-xl border border-cyan-400/45 bg-[rgba(5,14,16,0.97)] px-3 py-2.5 shadow-[0_16px_48px_rgba(0,0,0,0.65)] backdrop-blur-md';

    const recoWhyFmt = (template, vars) => {
      let out = template;
      for (const [k, v] of Object.entries(vars || {})) {
        out = out.replace(new RegExp(`\{${k}\}`, 'g'), String(v));
      }
      return out;
    };

    /** Full ranking/engine trace — admin UI only. Underlying preview fields stay in state for metrics. */
    const recoWhyLinesAdmin = (() => {
      if (!hasLiveReco || !topReco) return [];
      const opt = topReco;
      const poi = opt.poi || {};
      const lines = [];
      if (nowRecoWeatherContext && typeof nowRecoWeatherContext === 'object') {
        const w = nowRecoWeatherContext;
        if (w.summary_line) {
          lines.push(
            recoWhyFmt(nuAskUiStrings.whyWeatherSummaryTemplate || 'Weather: {summary}.', {
              summary: w.summary_line
            })
          );
        } else {
          const tempLabel = w.temp_c != null ? `${w.temp_c}°C` : '—';
          const c = w.condition != null ? String(w.condition) : '—';
          const p = w.pop != null ? `${w.pop}%` : '—';
          lines.push(
            recoWhyFmt(
              nuAskUiStrings.whyWeatherPopTemplate ||
                'Weather: {temp} · {condition} · {pop} {rain_word} (POP).',
              { temp: tempLabel, condition: c, pop: p, rain_word: nuAskUiStrings.weatherRainWord || 'rain' }
            )
          );
        }
        if (nowRecoWeatherContext.severe_outdoor_avoidance) {
          lines.push(
            nuAskUiStrings.whyHeavyRainDownrank ||
              'Heavy rain / high POP — open-air & landmark-outdoor picks are strongly down-ranked so they should not appear as #1 when sheltered alternatives exist.'
          );
        } else if (w.favors_indoor) {
          lines.push(
            nuAskUiStrings.whyRainIndoorBoost ||
              'Rain or elevated POP — ranking boosts indoor/sheltered picks and penalizes open-air from name, category, and map types (same pipeline as Smart Brain scoring).'
          );
        } else {
          lines.push(
            nuAskUiStrings.whyConditionsOutdoorOk ||
              'Conditions look OK for outdoor-style picks — no strong weather down-rank from POP/condition alone.'
          );
        }
      }
      if (asText(opt.poi_exposure)) {
        lines.push(
          recoWhyFmt(
            nuAskUiStrings.whyExposureGuessTemplate || 'Exposure guess for this POI: {exposure} (from catalog text & types).',
            { exposure: asText(opt.poi_exposure) }
          )
        );
      }
      if (opt.shelter_likely === true) {
        lines.push(
          nuAskUiStrings.whyShelterSignal ||
            'Shelter signal: name/category reads indoor-friendly (heuristic).'
        );
      } else if (opt.shelter_likely === false && nowRecoWeatherContext?.favors_indoor) {
        lines.push(
          nuAskUiStrings.whyNotIndoorCaution ||
            'Does not read as clearly indoor — in wet weather double-check cover or visit length.'
        );
      }
      const mr = asText(opt.match_reason);
      if (mr)
        lines.push(
          recoWhyFmt(nuAskUiStrings.whyIntentFitTemplate || 'Intent fit: {reason}.', {
            reason: mr
          })
        );
      const tt = joinTagList(nowRecoTravelerTags, 10);
      if (tt)
        lines.push(
          recoWhyFmt(
            nuAskUiStrings.whyTravelerTagsTemplate ||
              'Traveler / personality tags considered: {tags}.',
            { tags: tt }
          )
        );
      const ct = joinTagList(poi.category_tags, 6);
      const vt = joinTagList(poi.vibe_tags, 6);
      const st = joinTagList(poi.suitability_tags, 6);
      if (ct)
        lines.push(
          recoWhyFmt(nuAskUiStrings.whyPoiCategoryTagsTemplate || 'POI category tags: {tags}.', {
            tags: ct
          })
        );
      if (vt)
        lines.push(
          recoWhyFmt(nuAskUiStrings.whyVibeTagsTemplate || 'Behaviour / vibe tags: {tags}.', {
            tags: vt
          })
        );
      if (st)
        lines.push(
          recoWhyFmt(nuAskUiStrings.whySuitabilityTagsTemplate || 'Suitability tags: {tags}.', {
            tags: st
          })
        );
      const lr = asText(opt.learning_reason);
      if (lr) {
        lines.push(
          recoWhyFmt(nuAskUiStrings.whyLearningBoostTemplate || 'Learning boost: {reason}.', {
            reason: lr
          })
        );
      } else if (opt.score_breakdown && typeof opt.score_breakdown === 'object') {
        const b = opt.score_breakdown.base;
        const lb = opt.score_breakdown.learning_boost;
        const parts = [];
        if (b != null && Number.isFinite(Number(b))) parts.push(`base ${Number(b).toFixed(2)}`);
        if (lb != null && Number.isFinite(Number(lb))) parts.push(`learning +${(Number(lb) * 100).toFixed(0)}%`);
        if (parts.length)
          lines.push(
            recoWhyFmt(nuAskUiStrings.whyRankingSignalTemplate || 'Ranking signal: {parts}.', {
              parts: parts.join(', ')
            })
          );
      }
      const dk = Number(poi.distance_km);
      if (Number.isFinite(dk)) {
        const drive = roughUrbanDriveMinutes(dk);
        lines.push(
          recoWhyFmt(
            nuAskUiStrings.whyDistanceFromLocationTemplate ||
              'Straight-line distance from your location: {distance} (~{drive} min at urban pace, estimate).',
            { distance: formatDistanceLabel(dk), drive }
          )
        );
      }
      if (nowClock.mins > 0) {
        lines.push(
          recoWhyFmt(nuAskUiStrings.whyFreeWindowTemplate || 'Free window before your next commitment: {window}.', {
            window: formatDurationMins(nowClock.mins)
          })
        );
      }
      const plat = Number(poi.lat);
      const plng = Number(poi.lng);
      if (nowNextCoords && Number.isFinite(plat) && Number.isFinite(plng)) {
        const legKm = haversineKm(plat, plng, nowNextCoords.lat, nowNextCoords.lng);
        if (legKm != null) {
          const dm = roughUrbanDriveMinutes(legKm);
          lines.push(
            recoWhyFmt(
              nuAskUiStrings.whyPoiToNextTemplate ||
                'Rough straight-line from this POI to your next stop ({next_title}): {distance} (~{drive} min, estimate).',
              { next_title: asText(nowNextCard.title), distance: formatDistanceLabel(legKm), drive: dm }
            )
          );
        }
      } else if (asText(nowNextCard.title)) {
        lines.push(
          recoWhyFmt(
            nuAskUiStrings.whyNextStopNoCoordsTemplate ||
              'Next itinerary stop: {next_title}{time_suffix}. Precise route time needs coordinates on that activity.',
            {
              next_title: asText(nowNextCard.title),
              time_suffix: nowNextCard.time ? ` (${nowNextCard.time})` : ''
            }
          )
        );
      }
      return lines;
    })();

    /** Short product-facing why lines — no enums, down-rank jargon, tag dumps, or score internals. */
    const recoWhyLinesTraveler = (() => {
      if (!hasLiveReco || !topReco) return [];
      const opt = topReco;
      const poi = opt.poi || {};
      const lines = [];
      if (nowRecoWeatherContext && typeof nowRecoWeatherContext === 'object') {
        const w = nowRecoWeatherContext;
        const tempLabel = w.temp_c != null ? `${w.temp_c}°C` : '—';
        const conditionLabel =
          w.condition != null
            ? String(w.condition).replace(/_/g, ' ').trim() || '—'
            : '—';
        const popLabel = w.pop != null ? `${w.pop}%` : '—';
        const weatherVars = {
          temp: tempLabel,
          condition: conditionLabel,
          pop: popLabel,
          rain_word: nuAskUiStrings.weatherRainWord || 'rain'
        };
        if (w.severe_outdoor_avoidance) {
          lines.push(
            recoWhyFmt(
              nuAskUiStrings.whyTravelerWeatherHeavy ||
                'Rough weather ({temp} · {condition} · {pop} {rain_word}) — this pick accounts for cover and outdoor exposure.',
              weatherVars
            )
          );
        } else if (w.favors_indoor) {
          lines.push(
            recoWhyFmt(
              nuAskUiStrings.whyTravelerWeatherSheltered ||
                'About {temp} · {condition} · {pop} {rain_word} — favors a more sheltered stop.',
              weatherVars
            )
          );
        } else {
          lines.push(
            recoWhyFmt(
              nuAskUiStrings.whyTravelerWeatherOk ||
                'About {temp} · {condition} — suits this kind of place.',
              weatherVars
            )
          );
        }
      }
      if (Array.isArray(nowRecoTravelerTags) && nowRecoTravelerTags.length > 0) {
        lines.push(
          nuAskUiStrings.whyTravelerPrefMatch || 'Aligned with your travel preferences.'
        );
      }
      const mr = asText(opt.match_reason);
      if (mr) {
        lines.push(
          nuAskUiStrings.whyTravelerIntentFit || "Fits what you're looking for right now."
        );
      }
      const dk = Number(poi.distance_km);
      if (Number.isFinite(dk)) {
        const drive = roughUrbanDriveMinutes(dk);
        lines.push(
          recoWhyFmt(
            nuAskUiStrings.whyTravelerDistanceTemplate ||
              'About {distance} from you (~{drive} min at urban pace).',
            { distance: formatDistanceLabel(dk), drive }
          )
        );
      }
      if (nowClock.mins > 0) {
        lines.push(
          recoWhyFmt(
            nuAskUiStrings.whyTravelerWindowTemplate ||
              'Fits your free window before the next stop ({window}).',
            { window: formatDurationMins(nowClock.mins) }
          )
        );
      }
      if (asText(nowNextCard.title)) {
        lines.push(
          recoWhyFmt(
            nuAskUiStrings.whyTravelerNextStopTemplate || 'Works with your next stop: {next_title}.',
            { next_title: asText(nowNextCard.title) }
          )
        );
      }
      return lines.slice(0, 5);
    })();

    const recoWhyLines = adminWhyUnlocked ? recoWhyLinesAdmin : recoWhyLinesTraveler;

    useEffect(() => {
      setTopRecoExpanded(true);
      setExpandedAltRecoKey(null);
      setTopRecoImageIdx(0);
      setTopRecoImageExhausted(false);
    }, [nowRecoOptions]);

    useEffect(() => {
      // Reset image candidate pointer when selected recommendation changes.
      setTopRecoImageIdx(0);
      setTopRecoImageExhausted(false);
    }, [selectedRecoKey, topReco?.id, topReco?.poi?.name]);

    const submitRecoSelection = async (modeLabel, opts = {}) => {
      const holdSpace = opts.holdSpace === true;
      const reco = opts.optionOverride || topReco;
      setNowRecoActionError('');
      setNowRecoActionInfo('');
      if (!holdSpace) setNowRecoProposal(null);

      if (modeLabel === 'add') {
        const t = Date.now();
        if (
          hasActiveTripContext &&
          nowCorridorPrevStartMs > 0 &&
          t >= nowCorridorPrevStartMs &&
          t < nowCorridorPrevStartMs + NUASK_ACTIVITY_POST_START_ADD_LOCK_MS
        ) {
          return holdSpace ? { ok: false, error: 'Add is paused just after your last start.' } : undefined;
        }
      }
      if (modeLabel === 'add' && nowEnRouteLockUntilMs > Date.now()) {
        const msg = (nuAskUiStrings.enRouteReplaceOnlyTitle || 'On the way to {poi}.')
          .replace(/\{poi\}/g, nowEnRouteLockPoiName || asText(nowNextCard?.title) || (nuAskUiStrings.panelNowTitle || 'NOW'));
        if (holdSpace) return { ok: false, error: msg };
        setNowRecoActionInfo(msg);
        return;
      }
      if (modeLabel === 'replace' && nowTvMode === 'go_added') {
        const msg =
          nuAskUiStrings.enRouteReplaceOnlyBody ||
          'Changed your mind? Use Undo. Replace Next is paused for about 30 minutes.';
        if (holdSpace) return { ok: false, error: msg };
        setNowRecoActionInfo(msg);
        return;
      }
      if (modeLabel === 'replace' && nowNextPlaceholder) {
        const msg =
          nuAskUiStrings.replaceNextNoFutureActivity || 'No next activity to replace yet.';
        if (holdSpace) return { ok: false, error: msg };
        setNowRecoActionError(msg);
        return;
      }

      if (!reco?.id) {
        const msg = nuAskUiStrings.noLiveRecommendationYet || '';
        if (holdSpace) return { ok: false, error: msg || 'No live place to add.' };
        setNowRecoActionError(msg);
        return;
      }
      if (!nowRecoMicroActionId) {
        const msg = nuAskUiStrings.patchDisabledNoReco || '';
        if (holdSpace) return { ok: false, error: msg || 'Recommendations not ready.' };
        setNowRecoActionError(msg);
        return;
      }
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        const msg = nuAskUiStrings.errApiUnavailable || '';
        if (holdSpace) return { ok: false, error: msg || 'API unavailable.' };
        setNowRecoActionError(msg);
        return;
      }

      setNowRecoActionLoading(modeLabel);
      try {
        const now = new Date();
        const clientDayKey = formatClientDayKeyLocal(now);
        const clientLocalHHMM = formatClientLocalHHMM(now);
        const selectedOptionId = reco?.id || null;
        const selectedPoiName = reco?.poi?.name || reco?.title || null;
        if (modeLabel === 'add') {
          void logNuAskChatEvent(
            'nuask_action_add_here',
            'NuAsk action: Add Here triggered.',
            {
              panel,
              mode: modeLabel,
              selected_option_id: selectedOptionId,
              selected_poi_name: selectedPoiName,
              client_day_key: clientDayKey,
              client_local_hhmm: clientLocalHHMM,
              hold_space: holdSpace || undefined
            }
          );
        } else if (modeLabel === 'replace') {
          void logNuAskChatEvent(
            'nuask_action_replace_next',
            'NuAsk action: Replace Next triggered.',
            {
              panel,
              mode: modeLabel,
              selected_option_id: selectedOptionId,
              selected_poi_name: selectedPoiName,
              client_day_key: clientDayKey,
              client_local_hhmm: clientLocalHHMM,
              hold_space: holdSpace || undefined
            }
          );
        }

        // Propose-only UI for ordinary Add Here — skip inside Hold (Governor-in-space).
        if (modeLabel === 'add' && liveUserCoords && reco?.poi && !holdSpace) {
          try {
            const proposeRes = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/micro-action/propose-slot`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                coords: liveUserCoords,
                client_day_key: clientDayKey,
                client_local_hhmm: clientLocalHHMM,
                poi: reco.poi
              })
            });
            window.SylvanFlowAuth.handleAuthResponse(proposeRes);
            const proposeData = await proposeRes.json().catch(() => null);
            if (proposeRes.ok && proposeData?.ok && proposeData?.proposal?.proposed_start_hhmm) {
              setNowRecoProposal(proposeData.proposal);
              setNowRecoActionInfo(
                `Suggested time: ${proposeData.proposal.proposed_start_hhmm}. Confirm to add.`
              );
              return;
            }
            if (proposeRes.ok && proposeData?.state === 'NO_FEASIBLE_SLOT') {
              const slotAfterCommitment =
                proposeData?.reason_code === 'SLOT_AFTER_COMMITMENT' ||
                (typeof proposeData?.reason === 'string' &&
                  proposeData.reason.toLowerCase().includes('commitment'));
              setNowRecoActionError(
                slotAfterCommitment
                  ? (nuAskUiStrings.proposeSlotAfterCommitment || nuAskUiStrings.noFeasibleWindow)
                  : proposeData?.reason || nuAskUiStrings.noFeasibleWindow
              );
              return;
            }
          } catch {}
        }

        const body = {
          micro_action_id: nowRecoMicroActionId,
          option_id: reco.id,
          client_day_key: clientDayKey,
          client_local_hhmm: clientLocalHHMM,
          patch_mode: modeLabel === 'replace' ? 'replace_next' : 'add',
          ...(holdSpace && modeLabel === 'add' ? { slot_confirmed: true } : {}),
          ...(liveUserCoords ? { coords: liveUserCoords } : {}),
          ...(corridorMutationTripKey ? { trip_key: corridorMutationTripKey } : {})
        };
        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/micro-action/select`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body)
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();
        const patchErr = resolveNuAskMicroActionPatchError(data, nuAskUiStrings);
        if (!res.ok || !data?.ok) {
          const err =
            patchErr || data?.message || data?.error || 'Unable to apply recommendation';
          if (holdSpace) {
            return {
              ok: false,
              error: /schedule or preview changed|行程或预览|refresh NOW|刷新 NOW/i.test(String(err))
                ? 'Your plan changed — try again or Not now.'
                : String(err).length > 90
                  ? "Couldn't update your plan — try again or Not now."
                  : err
            };
          }
          setNowRecoActionError(err);
          return;
        }
        if (patchErr) {
          if (holdSpace) {
            return {
              ok: false,
              error: /schedule or preview changed|行程或预览|refresh NOW|刷新 NOW/i.test(
                String(patchErr)
              )
                ? 'Your plan changed — try again or Not now.'
                : String(patchErr).length > 90
                  ? "Couldn't update your plan — try again or Not now."
                  : patchErr
            };
          }
          setNowRecoActionError(patchErr);
          return;
        }
        if (modeLabel === 'add') {
          const responseTripKey =
            typeof data?.trip_key === 'string' && data.trip_key.trim() ? data.trip_key.trim() : null;
          if (responseTripKey) {
            setCorridorMutationTripKey(responseTripKey);
          }
          if (data?.undo_available && (responseTripKey || corridorMutationTripKey)) {
            setNowUndoAvailable(true);
          }
          engageEnRouteLock(
            reco?.poi?.name || reco?.summary || '',
            reco?.poi?.poi_id ||
              reco?.poi?.id ||
              reco?.poi?.catalog_poi_id ||
              reco?.poi?.place_id ||
              reco?.poi?.google_place_id ||
              reco?.id,
            reco?.poi
          );
          setCorridorReloadTick((t) => t + 1);
          if (!holdSpace) {
            setNowRecoActionInfo(
              (nuAskUiStrings.enRouteReplaceOnlyTitle || 'On the way to {poi}.')
                .replace(/\{poi\}/g, asText(reco?.poi?.name || reco?.summary || nowEnRouteLockPoiName || ''))
            );
          }
        } else {
          setNowUndoAvailable(false);
          if (!holdSpace) {
            setNowRecoActionInfo(nuAskUiStrings.recommendationAppliedReplaceNext || 'Recommendation applied (replace-next request sent).');
          }
        }
        if (holdSpace) return { ok: true };
      } catch (e) {
        const err = e?.message || 'Unable to apply recommendation';
        if (holdSpace) return { ok: false, error: err };
        setNowRecoActionError(err);
      } finally {
        setNowRecoActionLoading('');
      }
    };
    ex1SubmitRecoRef.current = submitRecoSelection;

    const confirmRecoProposal = async (action) => {
      if (!nowRecoProposal?.proposed_start_hhmm) return;
      setNowRecoActionError('');
      setNowRecoActionInfo('');

      const hhmm = action === 'edit'
        ? (window.prompt('Pick a time (HH:MM, 24-hour).', nowRecoProposal.proposed_start_hhmm) || '').trim()
        : nowRecoProposal.proposed_start_hhmm;

      if (!/^\d{2}:\d{2}$/.test(hhmm)) {
        setNowRecoActionError('Invalid time. Please use HH:MM (24-hour).');
        return;
      }

      setNowRecoActionLoading('add');
      try {
        const now = new Date();
        const body = {
          micro_action_id: nowRecoMicroActionId,
          option_id: topReco?.id,
          client_day_key: formatClientDayKeyLocal(now),
          client_local_hhmm: hhmm,
          patch_mode: 'add',
          slot_confirmed: true,
          ...(liveUserCoords ? { coords: liveUserCoords } : {}),
          ...(corridorMutationTripKey ? { trip_key: corridorMutationTripKey } : {})
        };
        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/micro-action/select`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body)
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();
        const patchErr = resolveNuAskMicroActionPatchError(data, nuAskUiStrings);
        if (!res.ok || !data?.ok) {
          setNowRecoActionError(
            patchErr || data?.message || data?.error || 'Unable to apply recommendation'
          );
          return;
        }
        if (patchErr) {
          setNowRecoActionError(patchErr);
          return;
        }
        setNowRecoProposal(null);
        const responseTripKey =
          typeof data?.trip_key === 'string' && data.trip_key.trim() ? data.trip_key.trim() : null;
        if (responseTripKey) {
          setCorridorMutationTripKey(responseTripKey);
        }
        if (data?.undo_available && (responseTripKey || corridorMutationTripKey)) {
          setNowUndoAvailable(true);
        }
        engageEnRouteLock(
          topReco?.poi?.name || topReco?.summary || '',
          topReco?.poi?.poi_id ||
            topReco?.poi?.id ||
            topReco?.poi?.catalog_poi_id ||
            topReco?.poi?.place_id ||
            topReco?.poi?.google_place_id ||
            topReco?.id,
          topReco?.poi
        );
        setCorridorReloadTick((t) => t + 1);
        setNowRecoActionInfo(
          (nuAskUiStrings.addedToItineraryAtTemplate || 'Added to your itinerary at {time}.').replace(/\{time\}/g, hhmm)
        );
        void logNuAskChatEvent(
          'nuask_action_add_here_confirmed',
          `NuAsk action: Add Here confirmed at ${hhmm}.`,
          {
            panel,
            action,
            selected_option_id: topReco?.id || null,
            selected_poi_name: topReco?.poi?.name || topReco?.title || null,
            client_day_key: formatClientDayKeyLocal(now),
            confirmed_hhmm: hhmm
          }
        );
      } catch (e) {
        setNowRecoActionError(e?.message || 'Unable to apply recommendation');
      } finally {
        setNowRecoActionLoading('');
      }
    };

    const undoLastMicroActionAdd = async () => {
      if (!corridorMutationTripKey || nowRecoActionLoading) return;
      setNowRecoActionError('');
      setNowRecoActionInfo('');
      setNowRecoActionLoading('undo');
      try {
        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/micro-action/undo`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ trip_key: corridorMutationTripKey })
        });
        window.SylvanFlowAuth.handleAuthResponse(res);
        const data = await res.json();
        if (!res.ok || !data?.ok || !data?.undone) {
          if (data?.state === 'NOT_FOUND_STATE') {
            setNowUndoAvailable(false);
          }
          setNowRecoActionError(
            data?.reason ||
              data?.message ||
              nuAskUiStrings.undoFailed ||
              'Could not undo the last add.'
          );
          return;
        }
        setNowEnRouteLockUntilMs(0);
        setNowEnRouteLockPoiName('');
        setNowEnRouteLockPoiId('');
        setNowEnRouteLockLat(null);
        setNowEnRouteLockLng(null);
        setNowUndoAvailable(false);
        if (data?.trip_torn_down) {
          setCorridorMutationTripKey(null);
          setCorridorTripKey(null);
          setCorridorActivities([]);
          try {
            if (window.SylvanFlowState?.setItineraries) {
              const prev = window.SylvanFlowState.getItineraries?.() || [];
              const gone = corridorMutationTripKey;
              window.SylvanFlowState.setItineraries(
                Array.isArray(prev) ? prev.filter((it) => (it?.trip_key || it?.Trip_Key) !== gone) : []
              );
            }
          } catch {}
        }
        setNowRecoActionInfo(
          nuAskUiStrings.undoLastAddSuccess || 'Removed the last add from your plan.'
        );
        setCorridorReloadTick((t) => t + 1);
        void logNuAskChatEvent(
          'nuask_action_undo_last_add',
          'NuAsk action: Undo last micro-action add.',
          { panel, trip_key: corridorMutationTripKey }
        );
      } catch (e) {
        setNowRecoActionError(e?.message || nuAskUiStrings.undoFailed || 'Could not undo the last add.');
      } finally {
        setNowRecoActionLoading('');
      }
    };

    // Phase 4: Governor-in-space — Add/Replace runs inside Hold select (no post-close fire-and-forget).

    return (
        <div
          className={NUASK_LAYOUT.shell}
          style={{ fontFamily: FONT, backgroundColor: C.bg }}
        >
        {/* Header — reference: menu · large caps title · SF */}
        <header className="z-20 flex-shrink-0 border-b border-white/[0.06] px-4 pb-0 pt-[max(0.75rem,env(safe-area-inset-top))]">
          <div className="flex items-center justify-between gap-3">
            <button
              type="button"
              onClick={() => window.dispatchEvent(new CustomEvent('sylvanflow-open-drawer'))}
              className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg active:bg-white/10"
              aria-label="Menu"
            >
              <img
                src={
                  (typeof window !== 'undefined' &&
                    window.SF_TAB_GLYPH_SRC &&
                    window.SF_TAB_GLYPH_SRC.nuask) ||
                  '/assets/nav-nuask-premium.webp?v=9'
                }
                alt=""
                width={40}
                height={40}
                className="pointer-events-none h-10 w-10 object-contain"
                decoding="async"
                draggable={false}
              />
            </button>
            <div className="min-w-0 flex-1 text-center">
              <h1
                className="text-[22px] font-bold uppercase leading-tight tracking-[0.08em]"
                style={{ color: C.accent }}
              >
                {header.title}
              </h1>
              <p className="mt-1 text-[13px] font-normal leading-snug text-[#9CA3AF]">{panelSubtitle}</p>
            </div>
            <button
              type="button"
              onClick={() => corridorNavigate('power-up')}
              className="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full ring-2 ring-[#2DE2C5] ring-offset-2 ring-offset-black"
              style={{
                backgroundColor: profilePhotoUrl ? '#0a0a0a' : C.accent,
                boxShadow: `0 0 22px ${C.accentMuted}`
              }}
              aria-label="Profile"
            >
              {profilePhotoUrl ? (
                <img
                  src={profilePhotoUrl}
                  alt={displayName}
                  className="h-full w-full object-cover"
                  onError={(e) => {
                    e.target.style.display = 'none';
                    const par = e.target.parentElement;
                    if (par) par.style.backgroundColor = C.accent;
                    const fb = e.target.nextElementSibling;
                    if (fb) fb.style.display = 'flex';
                  }}
                />
              ) : null}
              <span
                className="flex h-full w-full items-center justify-center text-[11px] font-bold tracking-tight text-black"
                style={{ display: profilePhotoUrl ? 'none' : 'flex' }}
              >
                {(displayName || 'U')[0].toUpperCase()}
              </span>
            </button>
          </div>
        </header>

        <nav
          ref={tourPanelNavRef}
          className="z-20 flex-shrink-0 border-b border-white/[0.06] px-3 pb-2 pt-1"
          aria-label={nuAskUiStrings.panelSwitcherAria || 'Time corridor panels'}
        >
          <div className="flex rounded-xl bg-white/[0.06] p-1" role="tablist">
            {[0, 1, 2].map((i) => {
              const label =
                i === 0
                  ? nuAskUiStrings.panelPastTitle
                  : i === 1
                    ? nuAskUiStrings.panelNowTitle
                    : nuAskUiStrings.panelNextTitle;
              const selected = panel === i;
              return (
                <button
                  key={i}
                  type="button"
                  role="tab"
                  aria-selected={selected}
                  onClick={() => goToPanel(i)}
                  className={`min-w-0 flex-1 rounded-lg px-1.5 py-2 text-center text-[11px] font-bold uppercase tracking-[0.06em] transition-colors ${
                    selected ? 'bg-white/[0.12] text-white shadow-inner' : 'text-[#9CA3AF] active:bg-white/5'
                  }`}
                  style={selected ? { color: C.accent, boxShadow: `inset 0 0 0 1px ${C.accentSoft}` } : undefined}
                >
                  {label}
                </button>
              );
            })}
          </div>
        </nav>

        <div
          ref={scrollerRef}
          className={`flex min-h-0 w-full flex-1 snap-x snap-mandatory overflow-x-auto overflow-y-hidden ${
            panel === 1 && nowTvAskOpen ? 'relative z-[46]' : ''
          }`}
          style={{
            WebkitOverflowScrolling: 'touch',
            overscrollBehaviorX: 'contain',
            scrollSnapType: 'x mandatory'
          }}
        >
          {/* —— PAST —— (ref: cards + gutters only; dashed joins card edge → card edge; trail cap → NOW) */}
          <section
            className={`relative box-border min-h-0 w-full min-w-full max-w-full shrink-0 snap-center snap-always overflow-x-hidden overflow-y-auto ${NUASK_LAYOUT.sectionPad}`}
            style={nuAskDockSectionBottomPadStyle}
          >
            <div className="pointer-events-none absolute inset-x-0 top-[92px] z-[0] h-[38vh] overflow-hidden" aria-hidden>
              <div
                className="absolute inset-x-[-8%] top-0 h-full"
                style={{
                  background:
                    'linear-gradient(to bottom, rgba(139,92,246,0.26) 0%, rgba(45,226,197,0.18) 42%, rgba(0,0,0,0) 78%)',
                  animation: 'nuask-past-film-gate 10s ease-in-out infinite'
                }}
              />
              <div
                className="absolute inset-0 opacity-[0.32]"
                style={{
                  backgroundImage:
                    'radial-gradient(circle at 20% 40%, rgba(255,255,255,0.22) 0.6px, transparent 0.8px), radial-gradient(circle at 80% 70%, rgba(125,211,252,0.2) 0.6px, transparent 0.9px)',
                  backgroundSize: '5px 5px, 7px 7px',
                  animation: 'nuask-past-grain-a 9s linear infinite'
                }}
              />
              <div
                className="absolute inset-0 opacity-[0.24]"
                style={{
                  backgroundImage:
                    'radial-gradient(circle at 60% 30%, rgba(196,181,253,0.24) 0.7px, transparent 1px), radial-gradient(circle at 30% 80%, rgba(255,255,255,0.18) 0.6px, transparent 0.9px)',
                  backgroundSize: '6px 6px, 8px 8px',
                  animation: 'nuask-past-grain-b 11s linear infinite'
                }}
              />
              <div
                className="absolute inset-x-[-18%] top-[6%] h-[52%] rounded-[999px]"
                style={{
                  background:
                    'radial-gradient(ellipse at 50% 50%, rgba(167,139,250,0.46) 0%, rgba(45,226,197,0.30) 46%, rgba(45,226,197,0.00) 74%)',
                  animation: 'nuask-past-ambient-drift 7.2s ease-in-out infinite'
                }}
              />
            </div>
            {corridorDataReady ? (
            <div key={`past-calm-${corridorCalmRevealKey}`} style={NUASK_CALM_FADE_STYLE}>
            <div className="relative mb-6">
              <div className={CORRIDOR_META_BAND}>
                <div className="grid grid-cols-3 gap-1 text-center">
                  {pastStopsRow.map((s, i) => (
                    <p key={`hdr-${i}-${s.time}`} className="text-[9px] font-medium text-white/90">
                      {s.time}
                    </p>
                  ))}
                </div>
              </div>
              <div className={TIME_CORRIDOR_GRID}>
                <CorridorInbound />
                {pastStopsRow.map((s, idx) => (
                  <React.Fragment key={`past-${idx}-${s.title}`}>
                    <button
                      type="button"
                      onClick={() => {
                        const target = pastCardTargets[idx];
                        if (!target) return;
                        if (window.appState?.set) {
                          window.appState.set({
                            view: 'activity-detail',
                            activityParams: {
                              tripKey: target.tripKey,
                              dayKey: target.dayKey,
                              activityIndex: target.activityIndex,
                              lang: resolveNuAskPreferredLanguageCode(),
                              selectedActivity: target.activity
                            }
                          });
                        }
                      }}
                      disabled={!pastCardTargets[idx]}
                      className={`min-w-0 flex ${CORRIDOR_RAIL_H} flex-col justify-center rounded-xl border border-white/10 bg-[#11151d] p-2 text-center shadow-[0_8px_24px_-14px_rgba(0,0,0,0.9)]`}
                      aria-label={pastCardTargets[idx] ? `Open ${s.title}` : s.title}
                      style={{
                        boxShadow: pastCardTargets[idx]
                          ? `0 0 0 1px ${C.accentSoft}, 0 8px 24px -14px rgba(0,0,0,0.9)`
                          : '0 0 0 1px rgba(255,255,255,0.04), inset 0 0 18px rgba(255,255,255,0.03)',
                        opacity: pastCardTargets[idx] ? 1 : 0.42,
                        filter: pastCardTargets[idx] ? 'none' : 'saturate(0.55) brightness(0.75)',
                        animation: `nuask-edge-refract-ring ${10.8 + idx * 0.7}s ease-in-out infinite`,
                        animationDelay: `${idx * 0.35}s`
                      }}
                    >
                      <div className="mx-auto flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 bg-black/45 text-sm">
                        {renderCorridorIcon(s.icon, !pastCardTargets[idx])}
                      </div>
                      <p className="mt-1 line-clamp-1 text-[10px] font-semibold leading-tight text-white">{s.title}</p>
                      <p className="mt-0.5 line-clamp-1 text-[9px] text-[#9CA3AF]">{s.sub}</p>
                    </button>
                    {idx < pastStopsRow.length - 1 ? (
                      <div className={`flex min-w-[8px] flex-col justify-center ${CORRIDOR_RAIL_H}`}>
                        <CorridorDash />
                      </div>
                    ) : (
                      <CorridorTrailCap />
                    )}
                  </React.Fragment>
                ))}
              </div>
            </div>

            {showPastDetailBlocks ? (
              <>
            <div
              className="relative mb-5 overflow-hidden rounded-2xl border p-4 shadow-lg"
              style={{
                borderColor: 'rgba(139,92,246,0.25)',
                background: 'linear-gradient(135deg, rgba(46,16,101,0.55) 0%, rgba(15,15,25,0.98) 55%, #000 100%)',
                boxShadow: '0 0 48px -12px rgba(139,92,246,0.35), 0 20px 50px -24px rgba(45,226,197,0.12)',
                animation: 'nuask-edge-refract-ring 11.6s ease-in-out infinite'
              }}
            >
              <div className="pointer-events-none absolute -right-6 -top-10 h-32 w-32 rounded-full bg-violet-600/20 blur-3xl" aria-hidden />
              <div className="relative flex items-start justify-between gap-3">
                <div className="min-w-0 pr-2">
                  <p className="text-[17px] font-bold leading-snug text-white">
                    {pastHeroTitle}
                  </p>
                  <p className="mt-2 text-[12px] leading-relaxed text-[#D1D5DB]">
                    {pastHeroBody}
                  </p>
                </div>
                <div className="relative flex h-[76px] w-[76px] shrink-0 flex-col items-center justify-center rounded-full border-2 bg-black/50 text-center">
                  <div
                    className="pointer-events-none absolute inset-0 rounded-full blur-md"
                    style={{ background: `radial-gradient(circle, ${C.accentMuted} 0%, transparent 70%)` }}
                    aria-hidden
                  />
                  <div
                    className="relative flex h-full w-full flex-col items-center justify-center rounded-full border-2"
                    style={{ borderColor: C.accent, boxShadow: `0 0 28px ${C.accentMuted}` }}
                  >
                    <span className="text-xl font-bold text-white">{placesActivityCount}</span>
                    <span className="text-[9px] font-semibold uppercase tracking-wide text-[#A1A1AA]">{nuAskUiStrings.placesLabel}</span>
                  </div>
                </div>
              </div>
            </div>

            <p
              className="mb-2 text-center text-[10px] font-bold uppercase tracking-[0.22em]"
              style={{ color: C.accent }}
            >
              {nuAskUiStrings.daySummary}
            </p>
            <div
              className="mb-5 grid grid-cols-4 gap-px overflow-hidden rounded-2xl border p-0"
              style={{
                backgroundColor: 'rgba(255,255,255,0.06)',
                borderColor: 'rgba(255,255,255,0.08)',
                animation: 'nuask-edge-refract-ring 12.8s ease-in-out infinite'
              }}
            >
              {[
                ['👣', nuAskUiStrings.statStepsLabel || 'Steps', DEMO.stats.steps],
                ['📏', nuAskUiStrings.statDistLabel || 'Dist', DEMO.stats.distance],
                ['⏱', nuAskUiStrings.statTimeLabel || 'Time', DEMO.stats.duration],
                ['⚡', nuAskUiStrings.statEnergyLabel || 'Energy', DEMO.stats.energy]
              ].map(([ic, lab, val]) => (
                <div key={String(lab)} className="flex flex-col items-center bg-black/80 px-1 py-3 text-center">
                  <span className="text-[14px] leading-none opacity-90">{ic}</span>
                  <p className="mt-1 text-[11px] font-bold tabular-nums" style={{ color: C.accent }}>
                    {val}
                  </p>
                  <p className="mt-0.5 text-[8px] font-medium uppercase tracking-wide text-[#6B7280]">{lab}</p>
                </div>
              ))}
            </div>

            <div
              className="mb-5 overflow-hidden rounded-2xl border border-white/[0.08] bg-[#030303] p-3"
              style={{ animation: 'nuask-edge-refract-ring 12.2s ease-in-out infinite' }}
            >
              <p
                className="mb-2 text-center text-[10px] font-bold uppercase tracking-[0.22em]"
                style={{ color: C.accent }}
              >
                {nuAskUiStrings.journeyMoments}
              </p>
              <div className="relative mb-2 h-2" aria-hidden>
                <div className="absolute left-3 right-3 top-1/2 -translate-y-1/2 border-t border-dashed" style={{ borderColor: C.stroke }} />
                <div className="absolute left-3 top-1/2 h-1.5 w-1.5 -translate-y-1/2 rounded-full bg-[#2DE2C5]" />
                <div className="absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#2DE2C5]" />
                <div className="absolute right-3 top-1/2 h-1.5 w-1.5 -translate-y-1/2 rounded-full bg-[#2DE2C5]" />
              </div>
              <div className="grid grid-cols-3 gap-2">
                {pastJourneyItems.map((item, idx) => (
                  <button
                    key={`${item.time}-${item.title}-${idx}`}
                    type="button"
                    onClick={() => {
                      const target = pastCardTargets[idx];
                      if (!target || !window.appState?.set) return;
                      window.appState.set({
                        view: 'activity-detail',
                        activityParams: {
                          tripKey: target.tripKey,
                          dayKey: target.dayKey,
                          activityIndex: target.activityIndex,
                          lang: resolveNuAskPreferredLanguageCode(),
                          selectedActivity: target.activity
                        }
                      });
                    }}
                    disabled={!pastCardTargets[idx]}
                    className={`overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d] text-left ${!pastCardTargets[idx] ? 'opacity-45' : ''}`}
                    style={{ animation: `nuask-edge-refract-ring ${11.4 + idx * 0.9}s ease-in-out infinite`, animationDelay: `${idx * 0.22}s` }}
                  >
                    <div className="relative h-[86px] w-full overflow-hidden">
                      {item.imageUrl ? (
                        <img
                          src={item.imageUrl}
                          alt={item.title}
                          className="h-full w-full object-cover"
                          onError={(e) => {
                            e.target.style.display = 'none';
                            const fallback = e.target.nextElementSibling;
                            if (fallback) fallback.style.display = 'flex';
                          }}
                        />
                      ) : null}
                      <div
                        className="absolute inset-0 hidden items-center justify-center text-2xl"
                        style={{
                          display: item.imageUrl ? 'none' : 'flex',
                          background:
                            'linear-gradient(145deg, rgba(13,148,136,0.35) 0%, rgba(15,23,42,0.95) 78%)'
                        }}
                      >
                        {renderCorridorIcon(item.icon, !pastCardTargets[idx])}
                      </div>
                      <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" aria-hidden />
                    </div>
                    <div className="p-2">
                      <p className="text-[9px] font-semibold text-[#9CA3AF]">{item.time}</p>
                      <p className="mt-0.5 line-clamp-1 text-[10px] font-semibold text-white">{item.title}</p>
                      <p className="mt-0.5 line-clamp-1 text-[9px] text-[#9CA3AF]">{item.subtitle}</p>
                    </div>
                  </button>
                ))}
              </div>
            </div>
              </>
            ) : (
              <div
                className="relative mb-5 overflow-hidden rounded-2xl border p-4 shadow-lg"
                style={{
                  borderColor: 'rgba(45,226,197,0.22)',
                  background: 'radial-gradient(120% 100% at 50% 0%, rgba(45,226,197,0.16) 0%, rgba(3,10,18,0.96) 45%, #02060b 100%)',
                  boxShadow: '0 0 44px -16px rgba(45,226,197,0.35), 0 18px 48px -28px rgba(0,0,0,0.9)'
                }}
              >
                <div className="pointer-events-none absolute -left-8 -top-8 h-24 w-24 rounded-full bg-cyan-400/20 blur-3xl" aria-hidden />
                <div className="pointer-events-none absolute -right-8 -bottom-8 h-24 w-24 rounded-full bg-teal-400/20 blur-3xl" aria-hidden />
                <p className="text-[12px] font-bold uppercase tracking-[0.18em]" style={{ color: C.accent }}>
                  {nuAskUiStrings.panelPastTitle || 'PAST'} · STANDBY
                </p>
                <p className="mt-2 text-[13px] leading-relaxed text-[#D1D5DB]">
                  {nuAskUiStrings.errNoTrip || 'No active trip linked'}
                </p>
              </div>
            )}
            <a
              href={pastMiniMapLinkUrl || '#'}
              target="_blank"
              rel="noopener noreferrer"
              onClick={(e) => {
                if (!pastMiniMapLinkUrl) e.preventDefault();
              }}
              className={`group relative block overflow-hidden rounded-2xl border bg-[#030303] ${
                pastMiniMapMode === 'standby_random' ? 'opacity-60' : 'opacity-100'
              }`}
              style={{
                borderColor:
                  pastMiniMapMode === 'standby_random' ? 'rgba(255,255,255,0.10)' : 'rgba(45,226,197,0.22)',
                boxShadow:
                  pastMiniMapMode === 'standby_random'
                    ? '0 0 0 1px rgba(255,255,255,0.05), inset 0 0 22px rgba(255,255,255,0.03)'
                    : '0 0 34px -18px rgba(45,226,197,0.36), 0 14px 34px -22px rgba(0,0,0,0.85)'
              }}
              aria-label={nuAskUiStrings.nextViewOnMap || 'Open map'}
            >
              <div className="relative h-[148px] w-full overflow-hidden">
                {pastMiniMapSrc ? (
                  <img
                    src={pastMiniMapSrc}
                    alt={nuAskUiStrings.nextViewOnMap || 'Map'}
                    className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.02]"
                    onError={() => setPastMiniMapImgFailed(true)}
                  />
                ) : null}
                <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-black/35" aria-hidden />
                <div className="absolute left-3 top-3 rounded-md border border-white/20 bg-black/55 px-2 py-1 text-[9px] font-semibold uppercase tracking-[0.16em] text-[#D1D5DB]">
                  {nuAskUiStrings.panelPastTitle || 'PAST'} · MAP
                </div>
                <div className="absolute bottom-3 left-3 right-3 flex items-end justify-between gap-3">
                  <p className="line-clamp-2 text-[11px] font-medium text-white/95">
                    {pastMiniMapMode === 'standby_random'
                      ? nuAskUiStrings.errNoTrip || 'No active trip linked'
                      : pastMiniMapMode === 'active_with_pins'
                        ? `${pastMapPoints.length} stop${pastMapPoints.length > 1 ? 's' : ''} pinned`
                        : nuAskUiStrings.journeyMoments || 'Journey map'}
                  </p>
                  <span className="shrink-0 rounded-md border border-white/20 bg-black/60 px-2 py-1 text-[9px] font-semibold text-[#E5E7EB]">
                    {nuAskUiStrings.nextViewOnMap || 'View map'}
                  </span>
                </div>
              </div>
            </a>
            </div>
            ) : null}
          </section>

          {/* —— NOW —— (ref: dashed only prev → ○ and ○ → next; no full-bleed line through the circle) */}
          <section
            className={`relative box-border flex h-full min-h-0 w-full min-w-full max-w-full shrink-0 snap-center snap-always flex-col overflow-hidden ${NUASK_LAYOUT.nowSectionPad}`}
            style={nuAskDockSectionBottomPadStyle}
          >
            <div
              className={[
                'relative shrink-0 overflow-visible',
                /*
                 * Stacking: TV plate uses z-[50] (+ isolate). Without a z on this chrome
                 * wrapper, the seam arrow’s z-[55] is trapped under the TV stacking context
                 * and paints behind the middle container. Keep chrome above the TV so the
                 * arrow can seam-overlay the corridor/TV edge.
                 */
                showNowCorridorChrome ? 'z-[51]' : '',
                /* Collapsed strip frees vertical space for the TV; keep a slim seam for the overlay arrow. */
                !showNowCorridorChrome
                  ? 'mb-0'
                  : NUASK_A2_COLLAPSE_CORRIDOR && !nowCorridorExpanded
                    ? 'mb-2'
                    : 'mb-6'
              ].join(' ')}
            >
              {showNowCorridorChrome ? (
              <div key={`corridor-calm-now-${corridorCalmRevealKey}`} style={NUASK_CALM_FADE_STYLE}>
              {NUASK_A2_COLLAPSE_CORRIDOR && !nowCorridorExpanded ? (
                <>
                  {/* Collapsed: words strip — prev · available window · next */}
                  <div
                    className="rounded-2xl border border-white/10 bg-[rgba(12,18,22,0.92)] px-2.5 py-2.5 shadow-[0_8px_24px_rgba(0,0,0,0.35)]"
                    role="status"
                    aria-label={nuAskUiStrings.panelNowTitle || 'NOW'}
                  >
                    <div className="grid grid-cols-[1fr_auto_1fr] items-start gap-2">
                      <div className="min-w-0 text-left">
                        <p className="text-[8px] font-bold uppercase tracking-[0.1em] text-[#9CA3AF]">
                          {nuAskUiStrings.panelPastTitle || 'PAST'}
                        </p>
                        <p className="mt-1 line-clamp-2 text-[11px] font-semibold leading-snug text-white">
                          {nowPrevCard.title}
                        </p>
                        {nowPrevCard.time ? (
                          <p className="mt-0.5 text-[9px] tabular-nums text-[#9CA3AF]">{nowPrevCard.time}</p>
                        ) : null}
                      </div>
                      <div className="min-w-0 max-w-[9.5rem] px-1 text-center">
                        {showAvailableWindowChrome ? (
                          <>
                            <p className="text-[8px] font-bold uppercase tracking-[0.1em] text-[#9CA3AF]">
                              {nuAskUiStrings.availableWindow || 'Available Window'}
                            </p>
                            <p className="mt-1 text-[13px] font-semibold tabular-nums tracking-tight" style={{ color: C.accent }}>
                              {hasKnownWindowEnd
                                ? formatDurationMins(nowClock.mins)
                                : nuAskUiStrings.availableWindowNoFixedEnd}
                            </p>
                            {showAvailableWindowRange ? (
                              <p className="mt-0.5 text-[9px] tabular-nums text-[#9CA3AF]">
                                {nowClock.rangeStartLabel || nowClock.nowLabel} – {nowClock.endLabel}
                              </p>
                            ) : null}
                          </>
                        ) : null}
                      </div>
                      <div className="min-w-0 text-right">
                        <p className="text-[8px] font-bold uppercase tracking-[0.1em] text-[#9CA3AF]">
                          {nuAskUiStrings.panelNextTitle || 'NEXT'}
                        </p>
                        <p className="mt-1 line-clamp-2 text-[11px] font-semibold leading-snug text-white">
                          {nowNextCard.title}
                        </p>
                        {nowNextCard.time ? (
                          <p className="mt-0.5 text-[9px] tabular-nums text-[#9CA3AF]">
                            <span className="inline-flex flex-wrap items-center justify-end gap-1">
                              {nowNextCard.time}
                              {fixedBookingBadgeEl}
                            </span>
                          </p>
                        ) : null}
                      </div>
                    </div>
                  </div>
                  {travelLeaveAlert && nowTvMode !== 'go_next' ? (
                    <div
                      className={`mt-2 rounded-xl border px-3 py-2 text-[14px] leading-snug ${
                        travelLeaveAlert.tone === 'urgent'
                          ? 'border-rose-400/45 bg-rose-950/40 text-rose-50'
                          : 'border-amber-400/45 bg-amber-950/30 text-amber-50'
                      }`}
                      role="status"
                    >
                      <p className="font-semibold">{travelLeaveAlert.title}</p>
                      <p className="mt-1 text-[14px] leading-relaxed text-white/85">{travelLeaveAlert.body}</p>
                    </div>
                  ) : null}
                </>
              ) : (
                <>
              <div className={CORRIDOR_META_BAND}>
                <div className={`${TIME_CORRIDOR_GRID} text-center`}>
                  <div className="invisible shrink-0 pointer-events-none" aria-hidden>
                    <CorridorInboundLabelAlign />
                  </div>
                  <p className="text-[9px] font-medium text-white/90">{nowPrevCard.time}</p>
                  <span className="min-w-[8px] shrink-0" aria-hidden />
                  <p className="text-[10px] font-bold uppercase tracking-[0.14em]" style={{ color: C.accent }}>
                    {nuAskUiStrings.nowWord || 'NOW'} · {nowClock.nowLabel}
                  </p>
                  <span className="min-w-[8px] shrink-0" aria-hidden />
                  <p className="text-[9px] font-medium text-white/90">
                    <span className="inline-flex flex-wrap items-center justify-center gap-1">
                      {nowNextCard.time}
                      {fixedBookingBadgeEl}
                    </span>
                  </p>
                  <div className="invisible shrink-0 pointer-events-none" aria-hidden>
                    <CorridorOutboundLabelAlign />
                  </div>
                </div>
              </div>

              <div ref={tourNowRailRef} className={`${TIME_CORRIDOR_GRID}`}>
                <CorridorInbound />
                <div className="min-w-0">
                  <div
                    className={`flex ${CORRIDOR_RAIL_H} flex-col justify-center rounded-xl border border-white/20 bg-[#11151d] p-2 text-center opacity-[0.78]`}
                    style={{
                      boxShadow:
                        '0 0 0 1px rgba(45,226,197,0.20), 0 10px 26px -14px rgba(0,0,0,0.9), 0 0 22px -12px rgba(45,226,197,0.42)',
                      animation: 'nuask-edge-refract-ring 6.2s ease-in-out infinite'
                    }}
                  >
                    <div className="mx-auto flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 bg-black/45 text-sm">
                      {renderCorridorIcon(nowPrevCard.icon, nowPrevPlaceholder)}
                    </div>
                    <p className="mt-1 line-clamp-2 text-[10px] font-semibold leading-tight text-white">{nowPrevCard.title}</p>
                    <p className="mt-0.5 text-[9px] text-[#9CA3AF]">{nowPrevCard.sub}</p>
                  </div>
                </div>

                <div className={`flex min-w-[8px] flex-col justify-center ${CORRIDOR_RAIL_H}`}>
                  <CorridorDash />
                </div>
                <div className="relative z-[6] flex min-h-0 min-w-0 flex-col items-center justify-center justify-self-center overflow-visible">
                  <div
                    className="relative flex h-[120px] w-[min(120px,100%)] max-w-[120px] shrink-0 flex-col items-center justify-center overflow-visible"
                    data-nuask-now-orb-locked={nowOrbTapLocked ? 'true' : undefined}
                    style={
                      nowOrbTapLocked
                        ? { filter: 'grayscale(1) brightness(0.68)', opacity: 0.78 }
                        : undefined
                    }
                  >
                    <div className="pointer-events-none absolute left-1/2 top-1/2 z-0 h-0 w-0 overflow-visible">
                      {(nowRecoLoading ? [0, 1, 2] : [0, 1]).map((i) => (
                        <div
                          key={`mega-${i}`}
                          className="absolute left-1/2 top-1/2"
                          style={{
                            width: 'clamp(76px, 30vw, 124px)',
                            height: 'clamp(76px, 30vw, 124px)',
                            transform: 'translate(-50%, -50%)'
                          }}
                          aria-hidden
                        >
                          <div
                            className={`h-full w-full rounded-full border-2 ${
                              nowRecoLoading ? 'border-cyan-300/35' : 'border-cyan-400/14'
                            }`}
                            style={{
                              animation: nowOrbTapLocked
                                ? 'none'
                                : `nuask-mega-ring-inner ${nowRecoLoading ? 2.15 : 3.4}s cubic-bezier(0.18, 0.85, 0.28, 1) infinite`,
                              animationDelay: `${i * (nowRecoLoading ? 0.48 : 0.92)}s`,
                              transformOrigin: 'center center',
                              boxShadow: nowRecoLoading
                                ? '0 0 22px rgba(34,211,238,0.18), inset 0 0 14px rgba(45,226,197,0.06)'
                                : '0 0 14px rgba(45,226,197,0.06)'
                            }}
                          />
                        </div>
                      ))}
                    </div>
                    <button
                      ref={nuAskNowOrbBtnRef}
                      type="button"
                      onClick={handleNowOrbClick}
                      onPointerDown={handleNowOrbPointerDown}
                      onPointerUp={handleNowOrbPointerUp}
                      onPointerCancel={handleNowOrbPointerCancel}
                      onContextMenu={(evt) => evt.preventDefault()}
                      className={`relative z-20 flex h-[120px] w-[120px] shrink-0 items-center justify-center touch-none ${
                        nowRecoLoading ? 'cursor-wait' : nowOrbTapLocked ? 'cursor-not-allowed' : 'cursor-pointer'
                      }`}
                      style={{ touchAction: 'none', WebkitUserSelect: 'none', userSelect: 'none' }}
                      aria-disabled={nowOrbTapLocked ? true : undefined}
                      aria-label={
                        nowRecoLoading
                          ? nuAskUiStrings.refreshAriaLoading || 'Refreshing recommendations'
                          : nuAskUiStrings.refreshAriaIdle || 'Refresh top recommendation'
                      }
                    >
                      <span
                        className="pointer-events-none absolute inset-0 z-0 rounded-full opacity-60"
                        style={{
                          background:
                            'radial-gradient(circle, rgba(45,226,197,0.38) 0%, rgba(45,226,197,0.16) 44%, rgba(45,226,197,0.00) 76%)',
                          filter: 'blur(1px)',
                          animation: nowOrbTapLocked
                            ? 'none'
                            : nowRecoLoading
                              ? 'nuask-orb-idle-breathe 0.95s ease-in-out infinite'
                              : 'nuask-orb-idle-breathe 2s ease-in-out infinite'
                        }}
                        aria-hidden
                      />
                      <span
                        className="pointer-events-none absolute inset-[6px] z-0 rounded-full opacity-75"
                        style={{
                          border: `2px solid ${C.accent}`,
                          boxShadow: '0 0 12px rgba(45,226,197,0.42), 0 0 22px rgba(45,226,197,0.22)',
                          animation: nowOrbTapLocked
                            ? 'none'
                            : nowRecoLoading
                              ? 'nuask-orb-idle-breathe 1s ease-in-out infinite'
                              : 'nuask-orb-idle-breathe 2s ease-in-out infinite'
                        }}
                        aria-hidden
                      />
                      <div
                        className="absolute inset-0 z-0 rounded-full blur-2xl opacity-70"
                        style={{ background: 'radial-gradient(circle, rgba(45,226,197,0.34) 0%, transparent 68%)' }}
                        aria-hidden
                      />
                      <div className="absolute inset-2 z-0 rounded-full border opacity-95" style={{ borderColor: C.accent }} aria-hidden />
                      <div className="absolute inset-0 z-0 rounded-full border border-dashed opacity-70" style={{ borderColor: C.accent }} aria-hidden />
                      <div
                        className="relative z-10 flex h-[62px] w-[62px] items-center justify-center rounded-full text-white"
                        style={{
                          background: `linear-gradient(165deg, #5FF5E0 0%, ${C.accent} 45%, #0d6b5c 100%)`,
                          boxShadow:
                            '0 0 18px rgba(45,226,197,0.95), 0 0 36px rgba(45,226,197,0.8), inset 0 1px 0 rgba(255,255,255,0.45)'
                        }}
                      >
                        {nowRecoLoading ? null : <span className="h-2.5 w-2.5 rounded-full bg-white/95" />}
                      </div>
                      {nowRecoLoading ? (
                        <>
                          <div className="pointer-events-none absolute inset-[16%] z-[25]">
                            <div
                              className="relative h-full w-full animate-spin rounded-full border border-cyan-200/45"
                              style={{ animationDuration: '2.8s' }}
                            />
                            <div
                              className="absolute inset-[18%] flex flex-col items-center justify-center rounded-full border border-cyan-200/70 px-1 backdrop-blur-[2px]"
                              style={{
                                animation: 'nuask-sync-glow 1.35s ease-in-out infinite',
                                background: 'rgba(0,8,12,0.55)'
                              }}
                            >
                              <span className="flex justify-center gap-[1px] leading-none">
                                {(nuAskUiStrings.syncWord || 'Syncing').split('').map((ch, si) => (
                                  <span
                                    key={`sync-ch-${si}`}
                                    className="inline-block text-[clamp(6px,1.65vw,8px)] font-bold uppercase tracking-[0.14em] text-cyan-50"
                                    style={{
                                      animation: 'nuask-sync-letter 1.05s ease-in-out infinite',
                                      animationDelay: `${si * 0.07}s`
                                    }}
                                  >
                                    {ch}
                                  </span>
                                ))}
                              </span>
                              <span className="mt-1 flex gap-0.5">
                                {[0, 1, 2].map((di) => (
                                  <span
                                    key={`dot-${di}`}
                                    className="block h-1 w-1 rounded-full bg-cyan-200/90"
                                    style={{
                                      animation: 'nuask-sync-letter 0.65s ease-in-out infinite',
                                      animationDelay: `${di * 0.12}s`
                                    }}
                                  />
                                ))}
                              </span>
                            </div>
                          </div>
                          <span
                            className="pointer-events-none absolute inset-[14px] z-[25] rounded-full border-2 border-transparent border-t-cyan-200 border-r-cyan-300/80 animate-spin"
                            style={{ animationDuration: '0.7s' }}
                            aria-hidden
                          />
                          <span
                            className="pointer-events-none absolute inset-[4px] z-[25] rounded-full border border-cyan-300/40 animate-spin"
                            style={{ animationDuration: '1.4s', animationDirection: 'reverse' }}
                            aria-hidden
                          />
                        </>
                      ) : null}
                    </button>
                  </div>
                </div>

                <div className={`flex min-w-[8px] flex-col justify-center ${CORRIDOR_RAIL_H}`}>
                  <CorridorDash />
                </div>

                <div className="min-w-0">
                  <div
                    className={`flex ${CORRIDOR_RAIL_H} flex-col justify-center rounded-xl border border-cyan-400/35 bg-[#11151d] p-2 text-center opacity-[0.82] shadow-[0_0_22px_-12px_rgba(45,226,197,0.7)]`}
                    style={{
                      boxShadow: `0 0 0 1px ${C.accentSoft}, 0 0 22px -12px rgba(45,226,197,0.55)`,
                      animation: 'nuask-edge-refract-ring 6.9s ease-in-out infinite',
                      animationDelay: '0.55s'
                    }}
                  >
                    <div className="mx-auto flex h-9 w-9 items-center justify-center rounded-xl border border-cyan-400/30 bg-black/45 text-sm">
                      {renderCorridorIcon(nowNextCard.icon, nowNextPlaceholder)}
                    </div>
                    <p className="mt-1 line-clamp-2 text-[10px] font-semibold leading-tight text-white">{nowNextCard.title}</p>
                    <p className="mt-0.5 text-[9px] text-[#9CA3AF]">{nowNextCard.sub}</p>
                  </div>
                </div>
                <CorridorOutbound />
              </div>

              {showAvailableWindowChrome ? (
              <div className="mt-2 flex w-full justify-center px-0.5">
                <div className="w-[min(92vw,20rem)] max-w-[min(92vw,20rem)] shrink-0 text-center">
                  <p className="text-[11px] text-[#9CA3AF]">
                    {nuAskUiStrings.availableWindow || 'Available Window'}{' '}
                    <span className="font-semibold text-white">
                      {hasKnownWindowEnd
                        ? formatDurationMins(nowClock.mins)
                        : nuAskUiStrings.availableWindowNoFixedEnd}
                    </span>
                  </p>
                  {showAvailableWindowRange ? (
                    <p className="mt-0.5 text-[11px] text-white">
                      {nowClock.rangeStartLabel || nowClock.nowLabel} - {nowClock.endLabel}
                    </p>
                  ) : null}
                  {availableWindowBridgeText ? (
                    <p className="mt-1.5 text-[10px] leading-snug text-[#9CA3AF]">{availableWindowBridgeText}</p>
                  ) : null}
                </div>
              </div>
              ) : null}

                </>
              )}

              {/* A2: frosted glass seam arrow — sits above corridor/TV edge; does not reserve a layout row.
                  Expanded: nudge further down into the empty mb seam so it does not cover Available Window / bridge copy. */}
              {NUASK_A2_COLLAPSE_CORRIDOR ? (
                <button
                  type="button"
                  aria-expanded={nowCorridorExpanded}
                  aria-label={
                    nowCorridorExpanded
                      ? nuAskUiStrings.corridorCollapseAria || 'Collapse corridor'
                      : nuAskUiStrings.corridorExpandAria || 'Expand corridor'
                  }
                  onClick={() => setNowCorridorExpanded((v) => !v)}
                  className={[
                    'absolute left-1/2 bottom-0 z-[55] -translate-x-1/2',
                    nowCorridorExpanded ? 'translate-y-[120%]' : 'translate-y-[60%]',
                    NUASK_A2_SEAM_ARROW_CLASS
                  ].join(' ')}
                  style={NUASK_A2_SEAM_ARROW_STYLE}
                >
                  <span
                    className={[
                      'block h-1.5 w-1.5 border-r-2 border-b-2 border-current transition-transform duration-200',
                      nowCorridorExpanded ? 'translate-y-[1px] rotate-[-135deg]' : '-translate-y-px rotate-[45deg]'
                    ].join(' ')}
                    aria-hidden
                  />
                </button>
              ) : null}
              </div>
              ) : null}
            </div>

            {showLocationPrompt ? (
              <div className="mb-2 flex items-center justify-between gap-2 px-0.5">
                <p className="text-[10px] text-amber-200/90">{nuAskUiStrings.locationOffHint}</p>
                <button
                  type="button"
                  onClick={refreshNowRecommendations}
                  className="text-[10px] font-semibold uppercase tracking-wide text-amber-200 underline underline-offset-2"
                >
                  {nuAskUiStrings.retry || 'Retry'}
                </button>
              </div>
            ) : null}

            <div
              className={[
                'relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-[13px] border border-white/[0.09] bg-[#070a0c] shadow-[0_12px_48px_-20px_rgba(0,0,0,0.85)] isolate',
                /* Flex-1 fills height freed when corridor is collapsed; shrinks when corridor expands. */
                'transition-[flex-grow] duration-300 ease-out',
                corridorTourOpen ? 'z-0 pointer-events-none' : 'z-[50]'
              ].join(' ')}
              aria-hidden={corridorTourOpen ? true : undefined}
            >
              {panel === 1 && nowTvAskOpen && typeof window !== 'undefined' && window.AskSylvanPage ? (
                <div className="absolute inset-0 z-[60] flex min-h-0 flex-col overflow-hidden rounded-[inherit]">
                  <div
                    className="pointer-events-auto absolute inset-0 bg-black/45 backdrop-blur-[3px]"
                    aria-hidden
                    onClick={closeNowTvAsk}
                  />
                  <div
                    className="pointer-events-auto relative m-2 flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-white/[0.16] shadow-[0_24px_64px_-20px_rgba(0,0,0,0.75),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl"
                    style={{
                      background:
                        'linear-gradient(165deg, rgba(255,255,255,0.10) 0%, rgba(12,18,22,0.42) 28%, rgba(5,8,12,0.58) 100%)'
                    }}
                    role="dialog"
                    aria-label={nuAskUiStrings.ariaAskSylvan || 'Ask Sylvan'}
                  >
                    <div className="flex shrink-0 items-center justify-between gap-2 border-b border-white/[0.10] bg-white/[0.04] px-3 py-2 backdrop-blur-md">
                      <button
                        type="button"
                        onClick={closeNowTvAsk}
                        className="rounded-lg px-2 py-1 text-[13px] font-semibold text-cyan-100/95 active:bg-white/10"
                      >
                        {nuAskUiStrings.back || 'Back'}
                      </button>
                      <span className="pointer-events-none text-[13px] font-semibold uppercase tracking-[0.14em] text-white/90">
                        {nuAskUiStrings.ariaAskSylvan || 'Ask Sylvan'}
                      </span>
                      <span className="w-12 shrink-0" aria-hidden />
                    </div>
                    <div className="relative min-h-0 flex-1 overflow-hidden">
                      {React.createElement(window.AskSylvanPage, {
                        embeddedExploreTv: true,
                        embeddedGlassPanel: true
                      })}
                    </div>
                  </div>
                </div>
              ) : null}
              <div
                ref={nowSectionScrollRef}
                data-nuask-now-scroll="1"
                className="flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-y-contain pb-2"
                style={{ WebkitOverflowScrolling: 'touch' }}
              >
                <div
                  ref={nowTopRecoScrollAnchorRef}
                  className={`w-full min-w-0 shrink-0 ${
                    nowHotbarDetail?.kind === 'emergency' ||
                    nowNearbyOpen ||
                    nowWeatherPlanOpen ||
                    nowUseMyTimeOpen
                      ? NUASK_LAYOUT.recoTopOverlay
                      : NUASK_LAYOUT.recoTopDefault
                  }`}
                >
              {nowHotbarDetail?.kind === 'emergency' ? (
                <NuAskEmergencyShell
                  phase={nowHotbarDetail.phase}
                  errorMessage={nowHotbarDetail.errorMessage || ''}
                  emergencyId={nowHotbarDetail.emergencyId || ''}
                  languageCode={
                    nowHotbarDetail.languageCode ||
                    (typeof window !== 'undefined' ? resolveNuAskPreferredLanguageCode() : null) ||
                    'en'
                  }
                  nuAskT={nuAskUiStrings}
                  onBack={clearNowHotbarDetail}
                  onCancel={clearNowHotbarDetail}
                  onRetry={() => void startEmergencyFromNowHotbar()}
                />
              ) : nowUseMyTimeOpen ? (
                <NuAskUseMyTimeShell
                  phase={nowUseMyTimePhase}
                  errorMessage={nowUseMyTimeError}
                  windowMins={nowClock.mins}
                  endLabel={nowClock.endLabel}
                  rows={nowUseMyTimeRows}
                  nuAskT={nuAskUiStrings}
                  mapsRegion={mapsRegion}
                  onBack={closeUseMyTimePanel}
                />
              ) : nowWeatherPlanOpen ? (
                <NuAskWeatherPlanShell
                  phase={nowWeatherPlanPhase}
                  errorMessage={nowWeatherPlanError}
                  data={nowWeatherPlanData}
                  nuAskT={nuAskUiStrings}
                  onBack={closeWeatherPlanPanel}
                  onApplyOrder={applyWeatherDayOrder}
                  applyPhase={nowWeatherApplyPhase}
                  applyError={nowWeatherApplyError}
                />
              ) : nowNearbyOpen ? (
                <NuAskNearbyPoisShell
                  phase={nowNearbyPhase}
                  errorMessage={nowNearbyError}
                  pois={nowNearbyPois}
                  nuAskT={nuAskUiStrings}
                  mapsRegion={mapsRegion}
                  onBack={closeNearbyPanel}
                />
              ) : nowEnterTheFrayHold ? (
                <div
                  className="relative flex min-h-0 w-full flex-1 flex-col items-center justify-center overflow-hidden rounded-[14px] border border-cyan-400/30 bg-[#040809] px-5 py-6 text-center"
                  style={{
                    boxShadow: `inset 0 0 0 1px rgba(45,226,197,0.14), 0 0 48px -20px rgba(34,211,238,0.45)`,
                    animation: 'nuask-edge-refract-ring 6.4s ease-in-out infinite'
                  }}
                >
                  <div
                    className="pointer-events-none absolute inset-0 opacity-[0.22]"
                    style={{
                      backgroundImage:
                        'linear-gradient(rgba(45,226,197,0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(45,226,197,0.07) 1px, transparent 1px)',
                      backgroundSize: '22px 22px'
                    }}
                    aria-hidden
                  />
                  <div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[14px]" aria-hidden>
                    <div
                      className="absolute left-0 right-0 h-[28%] bg-gradient-to-b from-cyan-400/25 via-cyan-300/10 to-transparent"
                      style={{ animation: 'nuask-fray-scan 2.8s ease-in-out infinite' }}
                    />
                  </div>
                  <div
                    className="pointer-events-none absolute inset-3 rounded-[10px] border border-dashed border-cyan-400/25"
                    style={{ animation: 'nuask-fray-edge 2.2s ease-in-out infinite' }}
                    aria-hidden
                  />
                  <p className="relative z-[1] font-mono text-[11px] uppercase tracking-[0.42em] text-cyan-300/75">
                    {nuAskUiStrings.corridorLink || 'Corridor link'}
                  </p>
                  <div className="relative z-[1] mt-5 flex flex-wrap justify-center gap-x-1 gap-y-1">
                    {(nuAskUiStrings.enterTheFray || 'ENTER THE FRAY').split('').map((ch, i) => (
                      <span
                        key={`fray-${i}-${ch}`}
                        className="inline-block text-[clamp(15px,4.2vw,22px)] font-black uppercase tracking-[0.08em] text-white"
                        style={{
                          textShadow: '0 0 18px rgba(45,226,197,0.55), 0 0 42px rgba(34,211,238,0.35)',
                          animation: 'nuask-sync-letter 1.2s ease-in-out infinite',
                          animationDelay: `${i * 0.045}s`
                        }}
                      >
                        {ch === ' ' ? '\u00a0' : ch}
                      </span>
                    ))}
                  </div>
                  {nowRecoLoading ? (
                    <>
                      <div className="relative z-[1] mt-4 flex w-full max-w-[min(22rem,100%)] flex-wrap items-center justify-center gap-x-2 gap-y-1 px-3">
                        {[
                          nuAskUiStrings.syncChipLive || 'Live POIs',
                          nuAskUiStrings.syncChipRank || 'Catalog rank',
                          nuAskUiStrings.syncChipWindow || 'Your window'
                        ].map((label, i) => (
                          <span
                            key={`sync-chip-${i}`}
                            className="inline-flex items-center gap-2 whitespace-nowrap font-mono text-[11px] uppercase tracking-[0.06em] text-cyan-100/55"
                          >
                            {i > 0 ? (
                              <span className="text-cyan-100/35" aria-hidden>
                                ·
                              </span>
                            ) : null}
                            {label}
                          </span>
                        ))}
                      </div>
                      <p className="relative z-[1] mt-2 max-w-[18rem] px-3 text-center text-[11px] leading-snug text-cyan-100/70">
                        {nuAskUiStrings.syncKeepAppOpen || "Don't close the app while loading"}
                      </p>
                    </>
                  ) : (
                    <p className="relative z-[1] mt-4 max-w-[17rem] font-mono text-[11px] uppercase leading-relaxed tracking-[0.22em] text-cyan-100/55">
                      {nuAskUiStrings.preparingHandshake || 'Preparing corridor handshake'}
                    </p>
                  )}
                  <div className="relative z-[1] mt-6 flex items-center justify-center gap-1.5">
                    {[0, 1, 2, 3, 4].map((i) => (
                      <span
                        key={`fray-dot-${i}`}
                        className="h-1 w-4 rounded-full bg-gradient-to-r from-cyan-300/20 to-cyan-200/90"
                        style={{
                          animation: 'nuask-fray-pulse 1.05s ease-in-out infinite',
                          animationDelay: `${i * 0.1}s`
                        }}
                      />
                    ))}
                  </div>
                </div>
              ) : (
                <div className="relative flex min-h-0 flex-1 flex-col overflow-hidden" data-nuask-now-inner="situation-reco">
                <div
                  data-nuask-now-pane="left"
                  className={
                    showNowEnRouteGo || (showNowRecoTopStack && nowTvInnerPane === 'right')
                      ? 'hidden'
                      : 'flex min-h-0 flex-1 flex-col overflow-hidden'
                  }
                >
                {showNowLeaveSoonOnLeft ? (
                  <div
                    className={`mb-2 shrink-0 rounded-xl border px-3 py-2.5 ${
                      travelLeaveAlert.tone === 'urgent'
                        ? 'border-rose-400/45 bg-rose-950/40'
                        : 'border-amber-400/45 bg-amber-950/30'
                    }`}
                    role="status"
                    data-nuask-go-next-dual="inspect"
                    data-nuask-go-next-alert="top"
                  >
                    <p
                      className={`text-[14px] font-semibold leading-snug ${
                        travelLeaveAlert.tone === 'urgent' ? 'text-rose-50' : 'text-amber-50'
                      }`}
                    >
                      {travelLeaveAlert.title}
                    </p>
                    {travelLeaveAlert.body ? (
                      <p className="mt-1.5 text-[14px] leading-relaxed text-white/88">{travelLeaveAlert.body}</p>
                    ) : null}
                  </div>
                ) : null}
                <div className="overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d]">
                  <div className="p-3.5">
                    {showAvailableWindowChrome ? (
                      <>
                    <p className="text-[12px] text-[#9CA3AF]">
                      {nuAskUiStrings.availableWindow || 'Available Window'}{' '}
                      <span className="text-[14px] font-semibold text-white">
                        {hasKnownWindowEnd
                          ? formatDurationMins(nowClock.mins)
                          : nuAskUiStrings.availableWindowNoFixedEnd}
                      </span>
                    </p>
                    {showAvailableWindowRange ? (
                      <p className="mt-0.5 text-[14px] text-white">
                        {nowClock.rangeStartLabel || nowClock.nowLabel} - {nowClock.endLabel}
                      </p>
                    ) : null}
                      </>
                    ) : null}
                    {!hasActiveTripContext ? (
                      <p className="mt-2 text-[14px] leading-relaxed text-[#9CA3AF]">
                        {nuAskUiStrings.errNoTrip || 'No active trip linked'}
                      </p>
                    ) : null}
                    {nowCurrentStop && nowCurrentStop.title ? (
                      <div className={showAvailableWindowChrome ? 'mt-3' : undefined} data-nuask-occupancy-tv="current">
                        <p className="text-[12px] text-[#9CA3AF]">
                          {nuAskUiStrings.nowOccupancyHeading || 'This stop'}
                        </p>
                        <button
                          type="button"
                          onClick={openNowCurrentActivityDetail}
                          disabled={!nowCurrentStop.detailTarget}
                          className="mt-2 block w-full text-left disabled:cursor-default"
                          aria-label={
                            nowCurrentStop.detailTarget
                              ? nowCurrentStop.title
                              : undefined
                          }
                        >
                          <div className="relative h-[120px] w-full overflow-hidden rounded-lg bg-[#0c1929]">
                            {nowCurrentStop.imageUrl ? (
                              <img
                                src={nowCurrentStop.imageUrl}
                                alt={nowCurrentStop.title}
                                className="absolute inset-0 h-full w-full object-cover opacity-90"
                              />
                            ) : nowCurrentStopMapUrl ? (
                              <img
                                src={nowCurrentStopMapUrl}
                                alt=""
                                className="absolute inset-0 h-full w-full object-cover opacity-95"
                              />
                            ) : (
                              <div
                                className="absolute inset-0 flex items-center justify-center text-5xl opacity-40"
                                aria-hidden
                              >
                                {renderCorridorIcon(nowCurrentStop.icon, false)}
                              </div>
                            )}
                            <div
                              className="absolute inset-0"
                              style={{
                                backgroundImage:
                                  'linear-gradient(to top, #11151d 0%, transparent 55%), radial-gradient(ellipse 80% 70% at 50% 40%, rgba(56,189,248,0.12), transparent 60%)'
                              }}
                              aria-hidden
                            />
                          </div>
                          <p className="mt-2 text-[17px] font-bold tracking-tight text-white">
                            {nowCurrentStop.title}
                          </p>
                          <p className="mt-1 text-[14px] font-semibold text-[#9CA3AF]">
                            {nowCurrentStop.time}
                            {nowCurrentStop.sub ? ` · ${nowCurrentStop.sub}` : ''}
                          </p>
                          {nowCurrentStop.body ? (
                            <p className="mt-2 text-[16px] leading-relaxed text-[#9CA3AF]">
                              {nowCurrentStop.body}
                            </p>
                          ) : null}
                        </button>
                      </div>
                    ) : null}
                    {nowCiVisibleLines.length > 0 ? (
                      <>
                        <div className="mt-3 border-t border-white/[0.06]" aria-hidden="true" />
                        <ul className="mt-2.5 space-y-1.5" style={NUASK_CALM_FADE_STYLE}>
                          {nowCiVisibleLines.map((line) => (
                            <li key={line} className="flex gap-2 text-[16px] leading-relaxed text-[#9CA3AF]">
                              <span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-[#6B7280]" aria-hidden="true" />
                              <span>{line}</span>
                            </li>
                          ))}
                        </ul>
                      </>
                    ) : null}
                    {nextStopDirectionsUrl ? (
                      <a
                        href={nextStopDirectionsUrl}
                        target="_blank"
                        rel="noopener noreferrer"
                        data-nuask-now-context-map="next"
                        className="mt-3 flex w-full items-center justify-center rounded-xl border border-[#4B5563] bg-transparent py-2.5 text-[13px] font-semibold text-white active:opacity-90"
                      >
                        {nuAskUiStrings.nextViewOnMap || 'View on Map'}
                      </a>
                    ) : null}
                  </div>
                </div>
                </div>
                {showNowEnRouteGo || showNowRecoTopStack ? (
                <div
                  data-nuask-now-pane={showNowEnRouteGo ? 'enroute' : 'right'}
                  className={
                    showNowEnRouteGo || (showNowRecoTopStack && nowTvInnerPane === 'right')
                      ? 'relative flex min-h-0 flex-1 flex-col overflow-hidden'
                      : 'hidden'
                  }
                >
                <div
                  key={topRecoRevealKey}
                  className="relative isolate flex w-full min-h-0 flex-col overflow-x-hidden overflow-hidden rounded-2xl p-[2px]"
                  style={{
                    animation: 'nuask-top-reco-reveal 0.58s cubic-bezier(0.22, 1, 0.36, 1) both',
                    boxShadow: `0 0 44px -8px rgba(45,226,197,0.38), 0 24px 50px -22px rgba(0,0,0,0.95), 0 0 0 1px rgba(45,226,197,0.12), inset 0 0 0 1px rgba(45,226,197,0.18)`
                  }}
                >
                  {/* Static rim + subtle moving perimeter sweep for "live" feel. */}
                  <div
                    className="pointer-events-none absolute inset-0 z-0 rounded-2xl opacity-90"
                    style={{
                      background:
                        'linear-gradient(135deg, rgba(45,226,197,0.12) 0%, rgba(16,185,129,0.08) 42%, rgba(45,226,197,0.1) 100%)',
                      boxShadow: 'inset 0 0 32px -8px rgba(45,226,197,0.15)'
                    }}
                    aria-hidden
                  />
                  <div className="pointer-events-none absolute inset-0 z-[1] overflow-hidden rounded-2xl" aria-hidden>
                    <div
                      className="absolute inset-y-0 left-[-30%] w-[40%]"
                      style={{
                        background:
                          'linear-gradient(90deg, rgba(45,226,197,0.00) 0%, rgba(45,226,197,0.12) 55%, rgba(125,255,235,0.00) 100%)',
                        filter: 'blur(1px)',
                        animation: nowRecoLoading
                          ? 'nuask-top-rim-sweep 1.9s linear infinite'
                          : 'nuask-top-rim-sweep 3.9s linear infinite'
                      }}
                    />
                  </div>
                  {/* Rim gutter via parent p-[2px] — not m-[2px]+w-full (that clips the right/bottom edge). */}
                  <div
                    className="relative z-10 flex w-full min-h-0 flex-col rounded-[14px] border border-cyan-300/20 bg-[#0d0d0d]"
                    style={{
                      boxShadow: `inset 0 1px 0 rgba(255,255,255,0.04), 0 18px 48px -18px rgba(0,0,0,0.92), 0 0 28px -14px ${C.accentSoft}`,
                      animation: 'nuask-edge-refract-ring 6.8s ease-in-out infinite'
                    }}
                  >
              {showNowEnRouteGo && nowGoCardModel ? (
                <>
                  <div className="flex items-center justify-between border-b border-white/[0.06] px-3 py-2.5">
                    <p className="flex items-center gap-1 text-[12px] font-bold uppercase tracking-[0.18em] text-[#9CA3AF]">
                      <span
                        className={nowGoCardModel.alertTone === 'urgent' ? 'text-rose-300' : 'text-amber-300'}
                      >
                        ●
                      </span>{' '}
                      {nowGoCardModel.headerLabel}
                    </p>
                    <span
                      className={`rounded-full border-2 px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wide ${
                        nowGoCardModel.alertTone === 'urgent'
                          ? 'border-rose-400/70 bg-rose-500/15 text-rose-100'
                          : 'border-amber-400/70 bg-amber-500/15 text-amber-100'
                      }`}
                    >
                      {nowGoCardModel.badgeLabel}
                    </span>
                  </div>
                  {nowCiVisibleLines.length > 0 ? (
                    <div className="px-3.5 pt-3 pb-2">
                      <ul className="space-y-1.5" style={NUASK_CALM_FADE_STYLE}>
                        {nowCiVisibleLines.map((line) => (
                          <li key={line} className="flex gap-2 text-[16px] leading-relaxed text-[#9CA3AF]">
                            <span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-[#6B7280]" aria-hidden="true" />
                            <span>{line}</span>
                          </li>
                        ))}
                      </ul>
                    </div>
                  ) : null}
                  <div className="relative h-[176px] w-full overflow-hidden bg-[#1a1018]">
                    {nowGoCardModel.coords ? (
                      <img
                        src={`${SMART_BRAIN_MEDIA_BASE}/map-image?lat=${encodeURIComponent(String(nowGoCardModel.coords.lat))}&lng=${encodeURIComponent(String(nowGoCardModel.coords.lng))}&label=${encodeURIComponent((asText(nowGoCardModel.title) || 'G').slice(0, 1))}&zoom=14&size=1200x700`}
                        alt=""
                        className="absolute inset-0 h-full w-full object-cover"
                      />
                    ) : null}
                    <div className="absolute inset-0 bg-gradient-to-t from-black via-black/40 to-transparent" aria-hidden />
                    <div className="absolute bottom-2 left-3 right-3">
                      <p className="truncate text-[16px] font-bold leading-tight text-white drop-shadow-md">
                        {asText(nowGoCardModel.title)}
                      </p>
                      <p className="mt-0.5 text-[12px] font-medium text-white/85">
                        {nowGoCardModel.timeSub}
                        {nowGoCardModel.variant === 'next' && fixedBookingBadgeEl ? (
                          <span className="ml-1.5 inline-block align-middle">{fixedBookingBadgeEl}</span>
                        ) : null}
                      </p>
                    </div>
                  </div>
                  {/* No pb — shared action row below supplies pb-3.5 (same gap contract as Decide). */}
                  <div className="px-3.5 pt-3.5">
                    {nowGoCardModel.variant !== 'next' ? (
                    <div
                      className={`rounded-xl border px-3 py-2.5 ${
                        nowGoCardModel.alertTone === 'urgent'
                          ? 'border-rose-400/45 bg-rose-950/40'
                          : 'border-amber-400/45 bg-amber-950/30'
                      }`}
                      role="status"
                    >
                      <p
                        className={`text-[14px] font-semibold leading-snug ${
                          nowGoCardModel.alertTone === 'urgent' ? 'text-rose-50' : 'text-amber-50'
                        }`}
                      >
                        {nowGoCardModel.alertTitle}
                      </p>
                      {nowGoCardModel.alertBody ? (
                        <p className="mt-1.5 text-[14px] leading-relaxed text-white/88">{nowGoCardModel.alertBody}</p>
                      ) : null}
                    </div>
                    ) : null}
                    <div className={`${nowGoCardModel.variant !== 'next' ? 'mt-3 ' : ''}rounded-xl border border-white/[0.08] bg-black/50 px-3 py-3`}>
                      <p className="text-[12px] font-semibold uppercase tracking-[0.14em] text-[#9CA3AF]">
                        {nuAskUiStrings.fromLocationToNext || 'From your location → next stop'}
                      </p>
                      <p className="mt-2 text-[30px] font-bold tabular-nums leading-none text-cyan-200">
                        ~{nowGoCardModel.etaMins != null ? nowGoCardModel.etaMins : '—'}{' '}
                        {nuAskUiStrings.minSuffix || 'min'}
                      </p>
                      <p className="mt-1.5 text-[13px] text-[#9CA3AF]">
                        {nowGoCardModel.kmStraight != null
                          ? formatDistanceLabel(nowGoCardModel.kmStraight)
                          : '—'}{' '}
                        {nuAskUiStrings.straightLineUrbanNote ||
                          'straight-line · urban-pace estimate (not live traffic)'}
                      </p>
                      {nowGoCardModel.variant === 'next' ? (
                        <p className="mt-2 text-[12px] leading-relaxed text-[#6B7280]">
                          {nuAskUiStrings.sidetripOffNote}
                        </p>
                      ) : null}
                    </div>
                    {nowGoCardModel.directionsUrl ? (
                      <a
                        href={nowGoCardModel.directionsUrl}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="mt-4 flex w-full items-center justify-center rounded-xl py-3 text-[14px] font-bold text-black active:opacity-90"
                        style={{ backgroundColor: C.accent, boxShadow: `0 8px 28px -4px ${C.accentMuted}` }}
                      >
                        {nuAskUiStrings.openDirections || 'Open directions'}
                      </a>
                    ) : null}
                  </div>
                </>
              ) : (
                <>
              <div
                className="relative border-b px-3 py-2.5"
                style={{
                  borderColor: nowRecoLoading ? 'rgba(45,226,197,0.42)' : 'rgba(45,226,197,0.28)',
                  background:
                    'linear-gradient(180deg, rgba(45,226,197,0.09) 0%, rgba(8,12,18,0.96) 48%, rgba(0,0,0,0.35) 100%)'
                }}
              >
                <div
                  className="pointer-events-none absolute inset-0 mix-blend-soft-light opacity-[0.09]"
                  style={{
                    backgroundImage: `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(
                      '<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(#n)"/></svg>'
                    )}")`,
                    backgroundSize: '128px 128px'
                  }}
                  aria-hidden
                />
                <div
                  className={`pointer-events-none absolute inset-0 ${nowRecoLoading ? 'opacity-[0.24]' : 'opacity-[0.14]'}`}
                  style={{
                    backgroundImage:
                      'repeating-linear-gradient(90deg, transparent, transparent 10px, rgba(45,226,197,0.14) 10px, rgba(45,226,197,0.14) 11px)',
                    animation: `nuask-hud-grid-drift ${nowRecoLoading ? 5.5 : 18}s linear infinite`
                  }}
                  aria-hidden
                />
                <div className="relative z-[1]" data-nuask-explain-root="reco_top">
                  <div className="flex min-h-0 items-center justify-between gap-2">
                    <p className="flex min-w-0 flex-1 items-center gap-2 text-[14px] font-bold uppercase leading-tight tracking-[0.2em]">
                      <span className="relative flex h-2.5 w-2.5 shrink-0 items-center justify-center">
                        <span
                          className="absolute rounded-full"
                          style={{
                            left: '50%',
                            top: '50%',
                            width: nowRecoLoading ? 12 : 10,
                            height: nowRecoLoading ? 12 : 10,
                            marginLeft: nowRecoLoading ? -6 : -5,
                            marginTop: nowRecoLoading ? -6 : -5,
                            backgroundColor: C.accent,
                            animation: `nuask-led-halo ${nowRecoLoading ? 1.9 : 2.75}s ease-in-out infinite`,
                            animationDelay: '0.06s',
                            transformOrigin: 'center'
                          }}
                          aria-hidden
                        />
                        <span
                          className="relative z-[1] inline-flex rounded-full"
                          style={{
                            width: nowRecoLoading ? 10 : 8,
                            height: nowRecoLoading ? 10 : 8,
                            backgroundColor: C.accent,
                            boxShadow: nowRecoLoading
                              ? `0 0 10px ${C.accent}, 0 0 16px rgba(45,226,197,0.32)`
                              : `0 0 8px ${C.accent}, 0 0 3px rgba(255,255,255,0.5)`
                          }}
                          aria-hidden
                        />
                      </span>
                      <span className="min-w-0 bg-gradient-to-r from-cyan-100 via-white to-cyan-50 bg-clip-text text-transparent">
                        {nuAskUiStrings.topRecommendation || 'Top recommendation'}
                      </span>
                    </p>
                    <button
                      type="button"
                      className="flex shrink-0 items-center gap-1 rounded-md border border-transparent bg-transparent p-0.5 text-left outline-none ring-offset-0 focus-visible:ring-2 focus-visible:ring-amber-400/45"
                      aria-expanded={nuAskExplainKey === 'reco_top'}
                      aria-label={nuAskUiStrings.explainAriaRecoTop || 'Explain top recommendation label'}
                      title={nuAskUiStrings.explainBangTitle || 'Explain'}
                      onClick={(e) => {
                        e.stopPropagation();
                        setNuAskExplainKey((k) => (k === 'reco_top' ? null : 'reco_top'));
                      }}
                    >
                      {String(topRecoTag || '').trim() ? (
                        <span
                          className="rounded-full border border-emerald-400/70 bg-gradient-to-b from-emerald-500/30 to-emerald-950/50 px-2.5 py-0.5 text-[11px] font-bold uppercase leading-none tracking-[0.12em] text-emerald-50"
                          style={{
                            animation: `nuask-badge-hud-glow ${nowRecoLoading ? 1.15 : 2.5}s ease-in-out infinite`
                          }}
                        >
                          {topRecoTag}
                        </span>
                      ) : null}
                      <span className={nuAskExplainBangBtnClass} aria-hidden>
                        !
                      </span>
                    </button>
                  </div>
                  {nuAskExplainKey === 'reco_top' ? (
                    <div className={`absolute right-0 z-[75] mt-1.5 ${nuAskExplainBubbleBoxClass}`} role="dialog" aria-modal="true">
                      <p className="text-[13px] font-medium leading-relaxed text-slate-100/95">{topRecoHudCaption}</p>
                      <button
                        type="button"
                        className="mt-2.5 w-full rounded-lg bg-cyan-500/25 py-2 text-[12px] font-bold uppercase tracking-wide text-cyan-50 active:bg-cyan-500/35"
                        onClick={() => setNuAskExplainKey(null)}
                      >
                        {nuAskUiStrings.explainDismiss || 'Got it'}
                      </button>
                    </div>
                  ) : null}
                </div>
              </div>
              <div
                className={`relative w-full overflow-hidden bg-[#1c1410] ${hasLiveReco ? 'h-[176px]' : 'h-[120px]'}`}
              >
                {topRecoPhotoUrl && !topRecoImageExhausted ? (
                  <img
                    src={topRecoPhotoUrl}
                    alt={topRecoName}
                    className="absolute inset-0 h-full w-full object-cover"
                    onError={() => {
                      setTopRecoImageIdx((idx) => {
                        const next = idx + 1;
                        if (next < topRecoPhotoCandidates.length) return next;
                        setTopRecoImageExhausted(true);
                        return idx;
                      });
                    }}
                  />
                ) : null}
                <div
                  className="absolute inset-0 opacity-28"
                  style={{
                    backgroundImage:
                      'radial-gradient(ellipse 90% 80% at 30% 20%, rgba(255,255,255,0.14), transparent 55%), radial-gradient(ellipse 70% 60% at 70% 60%, rgba(45,226,197,0.10), transparent 52%)'
                  }}
                  aria-hidden
                />
                <div
                  className="absolute inset-0"
                  style={{
                    background:
                      'linear-gradient(to bottom, rgba(8,12,16,0.52) 0%, rgba(8,12,16,0) 22%, rgba(8,12,16,0) 78%, rgba(8,12,16,0.62) 100%)'
                  }}
                  aria-hidden
                />
              </div>
              {/* No pb — shared action row below supplies pb-3.5; avoids double gap vs pre–Decide→Go single p-3.5 box. */}
              <div className="relative px-3.5 pt-3.5">
                <div className="flex items-center justify-between gap-2">
                  <p className="text-[18px] font-bold leading-tight text-white">{topRecoName}</p>
                  <button
                    type="button"
                    onClick={() => setTopRecoExpanded((v) => !v)}
                    className="shrink-0 rounded-lg border border-white/15 px-2 py-1 text-[14px] font-semibold uppercase tracking-wide text-[#D1D5DB]"
                  >
                    {topRecoExpanded
                      ? nuAskUiStrings.hideDetails || 'Hide Details'
                      : nuAskUiStrings.showDetails || 'Show Details'}
                  </button>
                </div>
                {topRecoExpanded ? (
                  <>
                    <p className="mt-1.5 text-[14px] leading-relaxed text-[#9CA3AF]">{topRecoDesc}</p>
                    <div className="mt-3 space-y-1.5 text-[13px] text-[#9CA3AF]">
                      {topRecoDistance ? <p>{topRecoDistance}</p> : null}
                      {hasLiveReco && topReco?.suggested_start_hhmm ? (
                        <p className="text-[13px] text-[#A7F3D0]">
                          {nuAskUiStrings.bestSlotLabel || 'Best slot'}: {topReco.suggested_start_hhmm}
                        </p>
                      ) : null}
                      {hasLiveReco && topReco?.action_hint ? (
                        <p className="text-[12px] text-[#9CA3AF]">
                          {topReco.action_hint === 'replace_next'
                            ? (nuAskUiStrings.actionHintReplaceNext || 'Action: replace next slot')
                            : (nuAskUiStrings.actionHintAddNow || 'Action: add in this gap')}
                        </p>
                      ) : null}
                      {hasLiveReco && Number.isFinite(Number(topReco?.poi?.distance_km)) ? (
                        <p className="text-[12px] text-[#6B7280]">
                          ~{roughUrbanDriveMinutes(Number(topReco.poi.distance_km))}{' '}
                          {nuAskUiStrings.urbanPaceFromDistance || 'min urban pace (from straight-line distance estimate, not live traffic)'}
                        </p>
                      ) : null}
                      {topRecoRating ? <p>{topRecoRating}</p> : null}
                      {topRecoCategory ? <p className="uppercase tracking-wide text-[#A7F3D0]">{topRecoCategory}</p> : null}
                    </div>
                  </>
                ) : null}
                {nowRecoLoading ? (
                  <p className="mt-3 text-[13px] text-[#9CA3AF]">
                    {nuAskUiStrings.loadingAlternatives || 'Loading alternatives…'}
                  </p>
                ) : null}
                {!nowRecoLoading && nowRecoError ? (
                  <p className="mt-3 text-[13px] text-amber-300">{nowRecoError}</p>
                ) : null}
                {!nowRecoLoading && hasLiveReco ? (
                  <div className="relative mt-3" data-nuask-explain-root="alternatives_slot_time">
                    <button
                      type="button"
                      className="mb-1 flex w-full min-w-0 items-center justify-between gap-2 rounded-md border border-transparent bg-transparent p-0 text-left outline-none ring-offset-0 focus-visible:ring-2 focus-visible:ring-amber-400/45"
                      aria-expanded={nuAskExplainKey === 'alternatives_slot_time'}
                      aria-label={
                        nuAskUiStrings.explainAriaAlternativesTime || 'Explain suggested times vs distance'
                      }
                      title={nuAskUiStrings.explainBangTitle || 'Explain'}
                      onClick={(e) => {
                        e.stopPropagation();
                        setNuAskExplainKey((k) =>
                          k === 'alternatives_slot_time' ? null : 'alternatives_slot_time'
                        );
                      }}
                    >
                      <span className="min-w-0 flex-1 text-[12px] font-semibold uppercase tracking-[0.16em] text-[#9CA3AF]">
                        {nuAskUiStrings.alternativesHeading || 'Alternatives'}
                      </span>
                      <span className={nuAskExplainBangBtnClass} aria-hidden>
                        !
                      </span>
                    </button>
                    {nuAskExplainKey === 'alternatives_slot_time' ? (
                      <div
                        className={`absolute left-0 z-[75] mt-1.5 ${nuAskExplainBubbleBoxClass}`}
                        role="dialog"
                        aria-modal="true"
                      >
                        <p className="text-[13px] font-medium leading-relaxed text-slate-100/95">
                          {nuAskUiStrings.alternativesSlotExplain ||
                            'Suggested start times come from gap-fit in your itinerary window. Kilometers are straight-line from you to the venue; urban minutes are a rough pace estimate, not live traffic.'}
                        </p>
                        <button
                          type="button"
                          className="mt-2.5 w-full rounded-lg bg-cyan-500/25 py-2 text-[12px] font-bold uppercase tracking-wide text-cyan-50 active:bg-cyan-500/35"
                          onClick={() => setNuAskExplainKey(null)}
                        >
                          {nuAskUiStrings.explainDismiss || 'Got it'}
                        </button>
                      </div>
                    ) : null}
                    <div className="space-y-1.5">
                      {altRecos.map(({ opt, fullIdx }, altRowIndex) => {
                        const rowKey = getRecoKey(opt, fullIdx);
                        const isRankedTopPick = hasLiveReco && rankedTopKey != null && rowKey === rankedTopKey;
                        const tierRowLabel = resolveNuAskRecoTierBadge(opt);
                        const strongFitRowLabel = isRankedTopPick
                          ? (rankedTopReco?.label && String(rankedTopReco.label).trim()) || nuAskUiStrings.strongFitDefault || 'Strong fit'
                          : '';
                        const rowBadgeText = tierRowLabel || strongFitRowLabel;
                        return (
                        <div key={rowKey}>
                          <button
                            type="button"
                            onClick={() => handleAlternativeRowClick(opt, fullIdx, altRowIndex)}
                            className={`flex w-full items-center justify-between rounded-lg border px-2.5 py-2 text-left ${
                              selectedRecoKey === rowKey
                                ? 'border-cyan-300/45 bg-cyan-400/10'
                                : 'border-white/10 bg-black/25'
                            }`}
                          >
                            <div className="min-w-0 pr-2">
                              <p className="truncate text-[13px] font-semibold text-white">
                                {opt?.poi?.name || opt?.summary || `${nuAskUiStrings.optionN || 'Option'} ${altRowIndex + 2}`}
                              </p>
                              <p className="text-[12px] text-[#9CA3AF]">{formatDistanceLabel(opt?.poi?.distance_km) || nuAskUiStrings.nearbyLabel || 'Nearby'}</p>
                              {hasLiveReco && opt?.suggested_start_hhmm ? (
                                <p className="text-[12px] text-[#A7F3D0]">
                                  {nuAskUiStrings.bestSlotLabelShort || nuAskUiStrings.bestSlotLabel || 'Best'}: {opt.suggested_start_hhmm}
                                </p>
                              ) : null}
                              {hasLiveReco && opt?.action_hint ? (
                                <p className="text-[12px] text-[#9CA3AF]">
                                  {opt.action_hint === 'replace_next'
                                    ? (nuAskUiStrings.actionHintReplaceNext || 'Action: replace next slot')
                                    : (nuAskUiStrings.actionHintAddNow || 'Action: add in this gap')}
                                </p>
                              ) : null}
                            </div>
                            <div className="flex shrink-0 items-center gap-1.5">
                              {rowBadgeText ? (
                                <span
                                  className={`max-w-[6.5rem] truncate rounded border px-1.5 py-0.5 text-[9px] font-bold uppercase leading-none tracking-wide ${
                                    tierRowLabel
                                      ? opt?.recommendation_tier === 'personalized'
                                        ? 'border-violet-400/55 bg-violet-950/45 text-violet-100/95'
                                        : 'border-slate-400/45 bg-slate-900/55 text-slate-200/95'
                                      : 'border-emerald-500/50 bg-emerald-950/40 text-emerald-200/95'
                                  }`}
                                  title={rowBadgeText}
                                >
                                  {rowBadgeText}
                                </span>
                              ) : null}
                              <span className="text-[12px] font-semibold text-[#A7F3D0]">
                                {Number.isFinite(Number(opt?.poi?.rating)) ? Number(opt.poi.rating).toFixed(1) : '—'}
                              </span>
                            </div>
                          </button>
                          {expandedAltRecoKey === rowKey ? (
                            <div
                              id={`nuask-alt-detail-${altRowIndex}`}
                              className="mt-1 scroll-mt-2 rounded-lg border border-white/10 bg-black/30 px-2.5 py-2"
                            >
                              <p className="text-[12px] leading-relaxed text-[#9CA3AF]">
                                {opt?.poi?.rich_description ||
                                  opt?.poi?.description ||
                                  opt?.summary ||
                                  nuAskUiStrings.noExtraDetails ||
                                  'No additional details yet.'}
                              </p>
                              <div className="mt-1.5 flex flex-wrap gap-2 text-[11px]">
                                <span className="rounded-full border border-white/15 px-2 py-0.5 text-[#D1D5DB]">
                                  {formatDistanceLabel(opt?.poi?.distance_km) || nuAskUiStrings.nearbyLabel || 'Nearby'}
                                </span>
                                <span className="rounded-full border border-white/15 px-2 py-0.5 text-[#D1D5DB]">
                                  {Number.isFinite(Number(opt?.poi?.rating))
                                    ? `${Number(opt.poi.rating).toFixed(1)} ${nuAskUiStrings.ratingWord || 'rating'}`
                                    : nuAskUiStrings.ratingNa || 'Rating n/a'}
                                </span>
                                {opt?.poi?.category ? (
                                  <span className="rounded-full border border-cyan-300/30 px-2 py-0.5 text-[#A7F3D0] uppercase">
                                    {opt.poi.category}
                                  </span>
                                ) : null}
                              </div>
                            </div>
                          ) : null}
                        </div>
                        );
                      })}
                      {altRecos.length === 0 ? (
                        <div className="rounded-lg border border-dashed border-white/15 bg-black/20 px-2.5 py-2">
                          <p className="text-[12px] text-[#9CA3AF]">
                            {nuAskUiStrings.noAlternativesYet || 'No live alternatives available yet.'}
                          </p>
                        </div>
                      ) : null}
                    </div>
                    {hasLiveReco && poiHoursVerifyDisclaimerResolved ? (
                      <div className="relative mt-2.5" data-nuask-explain-root="poi_hours">
                        <button
                          type="button"
                          className="flex w-full items-start gap-2 rounded-lg border border-cyan-500/25 bg-cyan-950/15 px-2.5 py-2 text-left outline-none ring-offset-0 focus-visible:ring-2 focus-visible:ring-amber-400/45"
                          role="note"
                          aria-expanded={nuAskExplainKey === 'poi_hours'}
                          aria-label={`${(nuAskUiStrings.poiHoursVerifyShort || 'Verify hours and status before you go.').trim()} ${nuAskUiStrings.explainBangTitle || 'Explain'}.`}
                          title={nuAskUiStrings.explainBangTitle || 'Explain'}
                          onClick={(e) => {
                            e.stopPropagation();
                            setNuAskExplainKey((k) => (k === 'poi_hours' ? null : 'poi_hours'));
                          }}
                        >
                          <span className="min-w-0 flex-1 text-[12px] font-medium leading-snug text-slate-200/95">
                            {nuAskUiStrings.poiHoursVerifyShort ||
                              'Verify hours and status before you go — listings can be wrong.'}
                          </span>
                          <span className={nuAskExplainBangBtnClass} aria-hidden>
                            !
                          </span>
                        </button>
                        {nuAskExplainKey === 'poi_hours' ? (
                          <div
                            className="absolute bottom-full left-2 right-2 z-[75] mb-1.5 rounded-xl border border-cyan-400/45 bg-[rgba(5,14,16,0.97)] px-3 py-2.5 shadow-[0_16px_48px_rgba(0,0,0,0.65)] backdrop-blur-md"
                            role="dialog"
                            aria-modal="true"
                          >
                            <p className="text-[13px] font-medium leading-relaxed text-slate-100/95">
                              {poiHoursVerifyDisclaimerResolved}
                            </p>
                            <button
                              type="button"
                              className="mt-2.5 w-full rounded-lg bg-cyan-500/25 py-2 text-[12px] font-bold uppercase tracking-wide text-cyan-50 active:bg-cyan-500/35"
                              onClick={() => setNuAskExplainKey(null)}
                            >
                              {nuAskUiStrings.explainDismiss || 'Got it'}
                            </button>
                          </div>
                        ) : null}
                      </div>
                    ) : null}
                  </div>
                ) : null}
              </div>
                </>
              )}
              <div className="relative px-3.5 pb-3.5">
                <div className={`mt-4 grid gap-2 ${showUndoThirdSlot ? 'grid-cols-3' : 'grid-cols-2'}`}>
                  {goAddedActionRow ? (
                    <button
                      type="button"
                      onClick={() => void undoLastMicroActionAdd()}
                      disabled={undoLeftDisabled}
                      title={undoLeftTitle}
                      className={`flex flex-col items-center justify-center rounded-xl bg-[#27272A] py-2.5 text-center active:bg-[#3f3f46] disabled:cursor-not-allowed ${
                        undoLeftDisabled ? 'opacity-50' : ''
                      }`}
                    >
                      <span className="text-[13px] font-bold text-white">
                        {nuAskUiStrings.undoLastAdd || 'Undo'}
                      </span>
                      <span className="mt-0.5 text-[12px] font-medium text-[#9CA3AF]">
                        {nuAskUiStrings.undoLastAddSub || 'Last add'}
                      </span>
                    </button>
                  ) : (
                  <button
                    type="button"
                    onClick={() => submitRecoSelection('add')}
                    disabled={addHereDisabled}
                    title={addHereDisabled ? patchMicroDisabledReason || undefined : undefined}
                    className={`flex flex-col items-center justify-center overflow-hidden rounded-xl py-2.5 text-center active:opacity-90 disabled:cursor-not-allowed ${
                      addHereDisabled ? 'opacity-60' : ''
                    }`}
                    style={{
                      backgroundImage: addHereDisabled
                        ? 'linear-gradient(180deg, rgba(75,85,99,0.88) 0%, rgba(55,65,81,0.9) 54%, rgba(31,41,55,0.92) 100%)'
                        : 'linear-gradient(180deg, rgba(45,226,197,0.98) 0%, rgba(13,148,136,0.96) 54%, rgba(8,74,78,0.98) 100%)',
                      backgroundSize: '100% 128%',
                      backgroundPosition: 'center 12%',
                      border: addHereDisabled ? '1px solid rgba(156,163,175,0.28)' : '1px solid rgba(45,226,197,0.18)',
                      boxShadow: addHereDisabled
                        ? 'inset 0 1px 0 rgba(255,255,255,0.04)'
                        : 'inset 0 1px 0 rgba(255,255,255,0.10)',
                      filter: addHereDisabled ? 'grayscale(0.6) brightness(0.65)' : 'none'
                    }}
                  >
                    <span
                      className="text-[13px] font-bold tracking-tight text-white"
                      style={{ textShadow: '0 1px 2px rgba(0,0,0,0.45)' }}
                    >
                      {nuAskUiStrings.addHere || 'Add Here'}
                    </span>
                    <span
                      className="mt-0.5 text-[14px] font-medium text-teal-50"
                      style={{ textShadow: '0 1px 2px rgba(0,0,0,0.35)' }}
                    >
                      {nowClock.nowLabel}
                    </span>
                  </button>
                  )}
                  <button
                    type="button"
                    onClick={() => submitRecoSelection('replace')}
                    disabled={replaceNextForcedOff}
                    title={replaceNextForcedOffReason || undefined}
                    className={`flex flex-col items-center justify-center rounded-xl bg-gradient-to-b from-violet-600 to-violet-900 py-2.5 text-center shadow-md active:opacity-90 ${
                      replaceNextForcedOff ? 'opacity-60 grayscale-[0.35]' : ''
                    }`}
                  >
                    <span className="text-[13px] font-bold text-white">{nuAskUiStrings.replaceNext || 'Replace Next'}</span>
                    <span className="mt-0.5 truncate px-0.5 text-[13px] font-medium text-white/85">{nowNextCard.title}</span>
                  </button>
                  {showUndoThirdSlot ? (
                    <button
                      type="button"
                      onClick={() => void undoLastMicroActionAdd()}
                      disabled={!!nowRecoActionLoading}
                      title={nuAskUiStrings.undoLastAddHint || undefined}
                      className="flex flex-col items-center justify-center rounded-xl bg-[#27272A] py-2.5 text-center active:bg-[#3f3f46] disabled:opacity-50"
                    >
                      <span className="text-[13px] font-bold text-white">
                        {nuAskUiStrings.undoLastAdd || 'Undo'}
                      </span>
                      <span className="mt-0.5 text-[12px] font-medium text-[#9CA3AF]">
                        {nuAskUiStrings.undoLastAddSub || 'Last add'}
                      </span>
                    </button>
                  ) : null}
                </div>
                {addHereDisabled &&
                addHereDisabledBannerLines &&
                addHereDisabledBannerLines.length > 0 &&
                nowTvMode !== 'go_added' ? (
                  <div
                    className="mt-2 rounded-lg border border-amber-500/25 bg-amber-950/25 px-2.5 py-2 text-center"
                    role="status"
                    aria-live="polite"
                  >
                    {addHereDisabledBannerLines.map((line, i) => (
                      <p
                        key={`add-here-disabled-${i}`}
                        className={`text-[14px] leading-snug text-amber-200/95 ${i > 0 ? 'mt-1' : ''}`}
                      >
                        {line}
                      </p>
                    ))}
                  </div>
                ) : null}
                {nowNextHardBooking && !nowNextPlaceholder && hasLiveReco && !addHereDisabledBannerLines ? (
                  <div
                    className="mt-2 rounded-lg border border-amber-500/25 bg-amber-950/25 px-2.5 py-2 text-center"
                    role="status"
                    aria-live="polite"
                  >
                    <p className="text-[14px] leading-snug text-amber-200/95">
                      {nuAskUiStrings.patchHardBookingProtected ||
                        nuAskUiStrings.replaceNextHardBookingHint ||
                        'This is a fixed booking and cannot be replaced.'}
                    </p>
                  </div>
                ) : null}
                {nowRecoProposal ? (
                  <div className="mt-2 rounded-xl border border-cyan-400/25 bg-[#061a18]/90 px-3 py-2">
                    <p className="text-[12px] text-[#9CA3AF]">
                      {(nuAskUiStrings.confirmAddTime || 'Confirm add at {time}?').replace(
                        '{time}',
                        nowRecoProposal.proposed_start_hhmm
                      )}
                    </p>
                    <div className="mt-2 flex gap-2">
                      <button
                        type="button"
                        onClick={() => void confirmRecoProposal('confirm')}
                        disabled={!!nowRecoActionLoading}
                        className="flex-1 rounded-lg bg-emerald-500/90 py-2 text-[13px] font-bold text-black active:opacity-90 disabled:opacity-40"
                      >
                        {nuAskUiStrings.confirm || 'Confirm'}
                      </button>
                      <button
                        type="button"
                        onClick={() => void confirmRecoProposal('edit')}
                        disabled={!!nowRecoActionLoading}
                        className="flex-1 rounded-lg bg-white/10 py-2 text-[13px] font-bold text-white active:bg-white/15 disabled:opacity-40"
                      >
                        {nuAskUiStrings.editTime || 'Edit time'}
                      </button>
                      <button
                        type="button"
                        onClick={() => setNowRecoProposal(null)}
                        disabled={!!nowRecoActionLoading}
                        className="rounded-lg bg-white/5 px-3 py-2 text-[13px] font-semibold text-white/80 active:bg-white/10 disabled:opacity-40"
                      >
                        {nuAskUiStrings.cancel || 'Cancel'}
                      </button>
                    </div>
                  </div>
                ) : null}
                {nowRecoActionInfo && !enRouteLockActive ? <p className="mt-2 text-[12px] text-emerald-300">{nowRecoActionInfo}</p> : null}
                {nowTvMode === 'decide' && nextStopDirectionsUrl ? (
                  <p className="mt-2 text-center">
                    <a
                      href={nextStopDirectionsUrl}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-[12px] font-semibold text-cyan-300/90 underline-offset-2 hover:text-cyan-200 hover:underline"
                    >
                      {nuAskUiStrings.nextMapsLink || 'Next · Maps'}
                    </a>
                  </p>
                ) : null}
                {nowRecoActionError ? <p className="mt-2 text-[12px] text-amber-300">{nowRecoActionError}</p> : null}
                {nowTvMode === 'decide' ? (
                <div className="mt-4 pt-3">
                  <p className="text-[14px] font-semibold text-[#D1D5DB]">
                    {nuAskUiStrings.whyHeading || 'Why this recommendation?'}
                  </p>
                  {adminWhyUnlocked ? (
                    <p className="mt-0.5 text-[11px] font-medium tracking-wide text-amber-400/85">
                      {nuAskUiStrings.whyAdminTraceHint || 'Engine trace (admin)'}
                    </p>
                  ) : null}
                  {hasLiveReco && recoWhyLines.length > 0 ? (
                    <ul className="mt-1.5 list-disc space-y-1 pl-4 text-[15px] leading-relaxed text-[#6B7280]">
                      {recoWhyLines.map((line, i) => (
                        <li key={`why-${i}`}>{line}</li>
                      ))}
                    </ul>
                  ) : (
                    <p className="mt-1.5 text-[15px] leading-relaxed text-[#6B7280]">
                      {hasLiveReco
                        ? nuAskUiStrings.whyFallbackPull ||
                          'Pull to refresh once more if decision details did not load.'
                        : nowRecoError
                          ? nuAskUiStrings.whyFallbackNoFeasible ||
                            'Options were ranked but none fit your free window once travel time is included. Tap refresh after your itinerary updates.'
                          : nuAskUiStrings.whyFallbackWait ||
                            'Waiting for a live catalog-ranked option. Tap refresh after location is available.'}
                    </p>
                  )}
                </div>
                ) : null}
              </div>
              </div>
                </div>
                </div>
                ) : null}
                </div>
              )}
            </div>
              </div>
              {/* LEFT/RIGHT A2 seam — pinned to the TV plate, sibling of nowSectionScrollRef (does not scroll with LEFT copy). */}
              {showNowInnerPaneArrow ? (
                <button
                  type="button"
                  data-nuask-now-inner-arrow="1"
                  aria-expanded={nowTvInnerPane === 'right'}
                  aria-label={
                    nowTvInnerPane === 'right'
                      ? nuAskUiStrings.nowTvInnerShowSituation || 'Show situation'
                      : nuAskUiStrings.nowTvInnerShowRecommendation || 'Show recommendation'
                  }
                  onClick={() => setNowTvInnerPane((p) => (p === 'right' ? 'left' : 'right'))}
                  className={[
                    'absolute top-1/2 z-[52] -translate-y-1/2',
                    nowTvInnerPane === 'right' ? 'left-1' : 'right-1',
                    NUASK_A2_SEAM_ARROW_CLASS
                  ].join(' ')}
                  style={NUASK_A2_SEAM_ARROW_STYLE}
                >
                  <span
                    className={[
                      'block h-1.5 w-1.5 border-r-2 border-b-2 border-current',
                      nowTvInnerPane === 'right' ? 'rotate-[135deg]' : 'rotate-[-45deg]'
                    ].join(' ')}
                    aria-hidden
                  />
                </button>
              ) : null}
            </div>
          </section>

          {/* —— NEXT —— (time corridor: times row — then TIME_CORRIDOR_GRID cards) */}
          <section
            className={`relative box-border min-h-0 w-full min-w-full max-w-full shrink-0 snap-center snap-always overflow-x-hidden overflow-y-auto ${NUASK_LAYOUT.sectionPad}`}
            style={nuAskDockSectionBottomPadStyle}
          >
            <div className="pointer-events-none absolute inset-x-0 top-[94px] z-[0] h-[36vh] overflow-hidden" aria-hidden>
              <div
                className="absolute left-[-32%] top-[14%] h-[2px] w-[52%] rounded-full bg-cyan-300/35"
                style={{ animation: 'nuask-next-ambient-flow 2.9s linear infinite' }}
              />
              <div
                className="absolute left-[-40%] top-[42%] h-[2px] w-[58%] rounded-full bg-emerald-300/30"
                style={{ animation: 'nuask-next-ambient-flow 3.6s linear infinite', animationDelay: '0.65s' }}
              />
            </div>
            {corridorDataReady ? (
            <div key={`next-calm-${corridorCalmRevealKey}`} style={NUASK_CALM_FADE_STYLE}>
            <div className="relative">
            <div className="relative mb-6">
              <div className={CORRIDOR_META_BAND}>
                <div className={`${TIME_CORRIDOR_GRID} text-center`}>
                  <div className="invisible shrink-0 pointer-events-none" aria-hidden>
                    <CorridorInboundLabelAlign />
                  </div>
                  <p className={`text-[9px] font-medium ${nextStandbyMode ? 'text-white/55' : 'text-white/90'}`}>{nowClock.endLabel}</p>
                  <span className="min-w-[8px] shrink-0" aria-hidden />
                  <p className={`text-[9px] font-medium ${nextStandbyMode ? 'text-white/55' : 'text-white/90'}`}>{nextLiveTime || '—'}</p>
                  <span className="min-w-[8px] shrink-0" aria-hidden />
                  <p className={`text-[9px] font-medium ${nextStandbyMode ? 'text-white/55' : 'text-white/90'}`}>{nextLiveAfterTime || '—'}</p>
                  <div className="invisible shrink-0 pointer-events-none" aria-hidden>
                    <CorridorOutboundLabelAlign />
                  </div>
                </div>
              </div>
              <div className="pointer-events-none mb-1 px-7" aria-hidden>
                <div
                  className="h-[2px] w-full rounded-full bg-gradient-to-r from-transparent via-cyan-300/45 to-transparent"
                  style={{ animation: 'nuask-next-corridor-pulse 2.2s ease-in-out infinite' }}
                />
              </div>
              <div className={TIME_CORRIDOR_GRID}>
                <CorridorInbound />
                <div className="min-w-0">
                  <div
                    className={`flex ${CORRIDOR_RAIL_H} flex-col justify-center rounded-xl border border-white/[0.08] bg-[#11151d] p-2 text-center shadow-[0_8px_24px_-14px_rgba(0,0,0,0.9)]`}
                    style={{
                      opacity: nextStandbyMode ? 0.30 : 0.48,
                      filter: nextStandbyMode ? 'saturate(0.45) brightness(0.72)' : 'none',
                      animation: 'nuask-edge-refract-ring 8.1s ease-in-out infinite'
                    }}
                  >
                    <div className="mx-auto flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 bg-black/45 text-sm">
                      {renderCorridorIcon('⏱️', nextStandbyMode)}
                    </div>
                    <p className="mt-1 line-clamp-2 text-[10px] font-semibold leading-tight text-white">
                      {nuAskUiStrings.availableWindow || 'Available window'}
                    </p>
                    <p className="mt-0.5 line-clamp-2 text-[9px] text-[#9CA3AF]">
                      {(nuAskUiStrings.nextCardWindowSubTemplate || '{mins} min left · ends {end}')
                        .replace(/\{mins\}/g, String(nowClock.mins))
                        .replace(/\{end\}/g, nowClock.endLabel)}
                    </p>
                  </div>
                </div>
                <div className={`flex min-w-[8px] flex-col justify-center ${CORRIDOR_RAIL_H}`}>
                  <CorridorDash />
                </div>
                <div className="min-w-0">
                  <button
                    type="button"
                    onClick={() => {
                      if (!nextLiveFirst) return;
                      const firstIsUnique =
                        Boolean(nextLiveFirstOccurrenceToken) &&
                        (!nextLiveSecondOccurrenceToken ||
                          nextLiveFirstOccurrenceToken !== nextLiveSecondOccurrenceToken);
                      setNextSelectedOccurrenceToken(firstIsUnique ? nextLiveFirstOccurrenceToken : '');
                      setNextSelectedSlot('fa1');
                    }}
                    disabled={!nextLiveFirst}
                    aria-pressed={nextSelectedSlotForRender === 'fa1'}
                    aria-label={
                      nextLiveFirst
                        ? (nuAskUiStrings.nextSelectActivityAriaTemplate || 'Select {activity}').replace(
                            /\{activity\}/g,
                            nextLiveTitle
                          )
                        : undefined
                    }
                    className={`flex w-full ${CORRIDOR_RAIL_H} flex-col justify-center rounded-xl p-2 text-center disabled:cursor-default`}
                    style={{
                      border: nextStandbyMode || nextSelectedSlotForRender !== 'fa1'
                        ? '1px solid rgba(255,255,255,0.08)'
                        : `2px solid ${C.accent}`,
                      boxShadow: nextStandbyMode || nextSelectedSlotForRender !== 'fa1'
                        ? '0 0 0 1px rgba(255,255,255,0.04), inset 0 0 18px rgba(255,255,255,0.03)'
                        : `0 0 28px ${C.accentMuted}`,
                      opacity: nextStandbyMode ? 0.38 : nextSelectedSlotForRender === 'fa1' ? 1 : 0.72,
                      filter: nextStandbyMode ? 'saturate(0.5) brightness(0.75)' : 'none',
                      backgroundColor: nextStandbyMode ? '#10141b' : 'rgba(0,0,0,0.7)',
                      animation: 'nuask-edge-refract-ring 7.4s ease-in-out infinite',
                      animationDelay: '0.45s'
                    }}
                  >
                    <div className="mx-auto flex h-9 w-9 items-center justify-center rounded-xl border border-cyan-400/30 bg-black/45 text-sm">
                      {renderCorridorIcon(nextLiveIcon, nextStandbyMode)}
                    </div>
                    <p className="mt-1 text-center text-[10px] font-bold leading-tight text-white">{nextLiveTitle}</p>
                    <p className="mt-0.5 line-clamp-2 text-[9px] font-medium text-[#9CA3AF]">{nextLiveFirst ? nextLiveSub : nextHonestEmptyCopy}</p>
                  </button>
                </div>
                <div className={`flex min-w-[8px] flex-col justify-center ${CORRIDOR_RAIL_H}`}>
                  <CorridorDash />
                </div>
                <div className="min-w-0">
                  <button
                    type="button"
                    onClick={() => {
                      if (!nextLiveSecond) return;
                      const secondIsUnique =
                        Boolean(nextLiveSecondOccurrenceToken) &&
                        (!nextLiveFirstOccurrenceToken ||
                          nextLiveSecondOccurrenceToken !== nextLiveFirstOccurrenceToken);
                      setNextSelectedOccurrenceToken(secondIsUnique ? nextLiveSecondOccurrenceToken : '');
                      setNextSelectedSlot(secondIsUnique ? 'fa2' : 'fa1');
                    }}
                    disabled={!nextLiveSecond}
                    aria-pressed={nextSelectedSlotForRender === 'fa2'}
                    aria-label={
                      nextLiveSecond
                        ? (nuAskUiStrings.nextSelectActivityAriaTemplate || 'Select {activity}').replace(
                            /\{activity\}/g,
                            nextLiveAfterTitle
                          )
                        : undefined
                    }
                    className={`flex w-full ${CORRIDOR_RAIL_H} flex-col justify-center rounded-xl border border-white/[0.08] bg-[#11151d] p-2 text-center shadow-[0_8px_24px_-14px_rgba(0,0,0,0.9)] disabled:cursor-default`}
                    style={{
                      border: !nextStandbyMode && nextSelectedSlotForRender === 'fa2' && nextLiveSecond
                        ? `2px solid ${C.accent}`
                        : '1px solid rgba(255,255,255,0.08)',
                      boxShadow: !nextStandbyMode && nextSelectedSlotForRender === 'fa2' && nextLiveSecond
                        ? `0 0 28px ${C.accentMuted}`
                        : undefined,
                      opacity: nextStandbyMode ? 0.34 : nextSelectedSlotForRender === 'fa2' && nextLiveSecond ? 1 : 0.72,
                      filter: nextStandbyMode ? 'saturate(0.45) brightness(0.72)' : 'none',
                      animation: 'nuask-edge-refract-ring 8.8s ease-in-out infinite',
                      animationDelay: '0.9s'
                    }}
                  >
                    <div className="mx-auto flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 bg-black/45 text-sm">
                      {renderCorridorIcon(nextLiveSecond ? nextLiveAfterIcon : '◌', nextStandbyMode)}
                    </div>
                    <p className="mt-1 line-clamp-2 text-[10px] font-semibold leading-tight text-white">{nextLiveSecond ? nextLiveAfterTitle : ''}</p>
                    <p className="mt-0.5 line-clamp-2 text-[9px] text-[#9CA3AF]">{nextLiveSecond ? nextLiveAfterSub : nextHonestEmptyCopy}</p>
                  </button>
                </div>
                <CorridorOutbound />
              </div>
            </div>
            {/* Upcoming highlight — outer shell matches PAST “Journey moments” */}
            <div
              className="mb-5 overflow-hidden rounded-2xl border border-white/[0.08] bg-[#030303] p-3"
              style={{ animation: 'nuask-edge-refract-ring 8.9s ease-in-out infinite' }}
            >
              <p
                className="mb-2 text-center text-[10px] font-bold uppercase tracking-[0.22em]"
                style={{ color: C.accent }}
              >
                {nuAskUiStrings.nextHighlightHeading || 'Upcoming highlight'}
              </p>
              {nextSelectedActivity ? (
                <div className="overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d]">
                <button
                  type="button"
                  onClick={openNextSelectedActivityDetail}
                  disabled={!nextSelectedDetailTarget}
                  className="block w-full text-left disabled:cursor-default"
                  aria-label={
                    nextSelectedDetailTarget
                      ? (nuAskUiStrings.nextOpenActivityAriaTemplate || 'Open {activity}').replace(
                          /\{activity\}/g,
                          nextSelectedTitle
                        )
                      : undefined
                  }
                >
                <div className="relative h-[152px] w-full overflow-hidden bg-[#0c1929]">
                  {nextSelectedImageUrl ? (
                    <img
                      src={nextSelectedImageUrl}
                      alt={nextSelectedTitle || ''}
                      className="absolute inset-0 h-full w-full object-cover opacity-90"
                    />
                  ) : nextPanelMapImageUrl ? (
                    <img
                      src={nextPanelMapImageUrl}
                      alt=""
                      className="absolute inset-0 h-full w-full object-cover opacity-95"
                    />
                  ) : (
                    <div
                      className="absolute inset-0 flex items-center justify-center text-6xl opacity-40"
                      aria-hidden
                    >
                      {renderCorridorIcon(nextSelectedIcon, nextStandbyMode)}
                    </div>
                  )}
                  <div
                    className="absolute inset-0"
                    style={{
                      backgroundImage:
                        'linear-gradient(to top, #11151d 0%, transparent 55%), radial-gradient(ellipse 80% 70% at 50% 40%, rgba(56,189,248,0.12), transparent 60%)'
                    }}
                    aria-hidden
                  />
                </div>
                <div className="border-t border-white/[0.06] p-3.5">
                  <p className="text-[17px] font-bold tracking-tight text-white">{nextSelectedTitle}</p>
                  <p className="mt-1 text-[12px] font-semibold text-[#9CA3AF]">
                    {nextSelectedTime}
                    {nextSelectedSub ? ` · ${nextSelectedSub}` : ''}
                    {nextHighlightDistanceLine
                      ? ` · ${nextHighlightDistanceLine}`
                      : ''}
                  </p>
                  <p className="mt-2 text-[13px] leading-relaxed text-[#9CA3AF]">
                    {String(nextSelectedBody || '').trim()}
                  </p>
                </div>
                </button>
                <div className="mt-4 flex items-stretch gap-3 border-t border-white/[0.06] px-3.5 pb-3.5 pt-3.5">
                    {nextLiveDirectionsUrl ? (
                      <a
                        href={nextLiveDirectionsUrl}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="flex flex-1 items-center justify-center rounded-xl border border-[#4B5563] bg-transparent py-2.5 text-[13px] font-semibold text-white active:opacity-90"
                      >
                        {nuAskUiStrings.nextViewOnMap || 'View on Map'}
                      </a>
                    ) : (
                      <button
                        type="button"
                        disabled
                        className="flex flex-1 cursor-not-allowed items-center justify-center rounded-xl border border-[#4B5563]/50 bg-transparent py-2.5 text-[13px] font-semibold text-[#6B7280]"
                      >
                        {nuAskUiStrings.nextViewOnMap || 'View on Map'}
                      </button>
                    )}
                    {nextLiveDirectionsUrl ? (
                      <a
                        href={nextLiveDirectionsUrl}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="relative block h-[52px] w-[72px] shrink-0 overflow-hidden rounded-lg border border-white/10 bg-[#1A1A1A] active:opacity-90"
                        aria-label={nuAskUiStrings.nextViewOnMap || 'Open directions in Maps'}
                      >
                        {nextSelectedCoords && nextHighlightThumbMapUrl ? (
                          <img
                            src={nextHighlightThumbMapUrl}
                            alt=""
                            className="absolute inset-0 h-full w-full object-cover"
                            onError={(e) => {
                              e.target.style.display = 'none';
                              const fb = e.target.nextElementSibling;
                              if (fb) fb.style.display = 'flex';
                            }}
                          />
                        ) : null}
                        <div
                          className={`absolute inset-0 flex flex-col items-center justify-center ${nextSelectedCoords && nextHighlightThumbMapUrl ? 'hidden' : 'flex'}`}
                          aria-hidden
                        >
                          <div
                            className="absolute inset-0 opacity-50"
                            style={{
                              backgroundImage:
                                'linear-gradient(rgba(148,163,184,0.12) 1px, transparent 1px), linear-gradient(90deg, rgba(148,163,184,0.12) 1px, transparent 1px)',
                              backgroundSize: '7px 7px'
                            }}
                          />
                          <div
                            className="absolute bottom-2 left-1/2 h-2 w-2 -translate-x-1/2 rounded-full border-2 border-white"
                            style={{ backgroundColor: C.accent }}
                          />
                        </div>
                      </a>
                    ) : (
                      <div
                        className="relative h-[52px] w-[72px] shrink-0 overflow-hidden rounded-lg border border-white/10 bg-[#1A1A1A]"
                        aria-hidden
                      >
                        <div
                          className="absolute inset-0 opacity-50"
                          style={{
                            backgroundImage:
                              'linear-gradient(rgba(148,163,184,0.12) 1px, transparent 1px), linear-gradient(90deg, rgba(148,163,184,0.12) 1px, transparent 1px)',
                            backgroundSize: '7px 7px'
                          }}
                        />
                        <div
                          className="absolute bottom-2 left-1/2 h-2 w-2 -translate-x-1/2 rounded-full border-2 border-white"
                          style={{ backgroundColor: C.accent }}
                        />
                      </div>
                    )}
                  </div>
                </div>
              ) : (
                <div className="overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d] opacity-45" style={{ filter: 'saturate(0.45) brightness(0.74)' }}>
                  <div className="flex h-[152px] items-center justify-center bg-[#0c1929] text-5xl opacity-40" aria-hidden>
                    ◌
                  </div>
                  <div className="border-t border-white/[0.06] p-3.5">
                    <p className="text-[17px] font-bold tracking-tight text-white">&nbsp;</p>
                    <p className="mt-1 text-[12px] font-semibold text-[#9CA3AF]">—</p>
                    <p className="mt-2 text-[13px] leading-relaxed text-[#9CA3AF]">
                      {nextHonestEmptyCopy}
                    </p>
                    <div className="mt-4 flex items-stretch gap-3">
                      <button
                        type="button"
                        disabled
                        className="flex flex-1 cursor-not-allowed items-center justify-center rounded-xl border border-[#4B5563]/50 bg-transparent py-2.5 text-[13px] font-semibold text-[#6B7280]"
                      >
                        {nuAskUiStrings.nextViewOnMap || 'View on Map'}
                      </button>
                    </div>
                  </div>
                </div>
              )}
            </div>

            {/* Before you go — same outer shell; preparation for the selected FA1/FA2 row (no 86px image) */}
            <div
              className="mb-5 overflow-hidden rounded-2xl border border-white/[0.08] bg-[#030303] p-3"
              style={{ animation: 'nuask-edge-refract-ring 9.6s ease-in-out infinite', animationDelay: '0.55s' }}
            >
              <p className="mb-2 text-center text-[10px] font-semibold uppercase tracking-[0.2em] text-[#6B7280]">
                {nuAskUiStrings.nextBeforeYouGoHeading || 'Before you go'}
              </p>
              <div className="overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d] p-3.5">
                {nextBeforeYouGoBind.outcome === 'matched' && nextBeforeYouGoBind.texts.length ? (
                  nextBeforeYouGoBind.texts.map((line, idx) => (
                    <p
                      key={`byg-${idx}`}
                      className={`${idx ? 'mt-2' : ''} text-[12px] leading-relaxed text-[#9CA3AF]`}
                    >
                      {line}
                    </p>
                  ))
                ) : (
                  <p className="text-[12px] leading-relaxed text-[#9CA3AF]">
                    {nuAskUiStrings.nextBeforeYouGoQuiet || 'Nothing extra to prepare'}
                  </p>
                )}
              </div>
            </div>
            </div>
            </div>
            ) : null}
          </section>
        </div>

        {/* NOW hotbar (panel 1) + inline Ask — one full-width shell, gap-0: hotkeys flush above chat, chat flush above InternalShellTwinNav */}
        <div
          ref={nuAskNowDockRef}
          className={`pointer-events-auto absolute bottom-0 left-0 right-0 flex flex-col gap-0 ${
            panel === 1 && nowTvAskOpen ? 'z-[30]' : 'z-40'
          }`}
          style={{ backgroundColor: C.bg }}
        >
          {panel === 0 || panel === 1 || panel === 2 ? (
            <div className="flex w-full flex-col border-x border-t border-white/[0.08] border-b-0 bg-[#070a0c] shadow-[0_-4px_28px_-12px_rgba(0,0,0,0.75)]">
              {panel === 1 && !NUASK_A2_COLLAPSE_HOTKEYS ? (
                <div
                  className={
                    (ST && ST.classes && ST.classes.shellTwinDockOuter) ||
                    'border-t border-white/[0.06] bg-[#05070a]/97 backdrop-blur-md pt-0 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]'
                  }
                >
                  <div className={(ST && ST.classes && ST.classes.shellTwinDockHubInner) || 'px-3 sm:px-4 pb-0.5 pt-0.5'}>
                    <div className="mx-auto grid w-full max-w-[440px] grid-cols-4 gap-1 min-w-0 [touch-action:manipulation]">
                    {nowHotbarItems.map((h, hubIdx) => {
                      const glow = h.glow;
                      const ring = h.ring;
                      return (
                      <button
                        key={h.key}
                        type="button"
                        onClick={() => activateNowHotkey(h)}
                        className={`group relative flex min-h-0 min-w-0 flex-col items-center overflow-hidden rounded-2xl border border-white/10 px-0.5 pb-1.5 pt-2 text-center backdrop-blur-lg transition duration-150 motion-safe:active:scale-[0.97] motion-safe:hover:brightness-110 ${
                          h.target ? '' : 'opacity-95'
                        }`}
                        style={{
                          background: [
                            `radial-gradient(96% 52% at 50% 108%, ${glow}, transparent 56%)`,
                            'radial-gradient(95% 54% at 50% -4%, rgba(255,255,255,0.09) 0%, transparent 46%)',
                            'linear-gradient(168deg, rgba(255,255,255,0.078) 0%, rgba(255,255,255,0.03) 11%, rgba(18,24,34,0.58) 40%, rgba(4,6,10,0.94) 100%)'
                          ].join(', '),
                          boxShadow: [
                            'inset 0 1px 0 rgba(255,255,255,0.13)',
                            `inset 0 0 0 1px ${ring}`,
                            'inset 0 14px 32px -10px rgba(0,0,0,0.32)',
                            'inset 0 -36px 48px -12px rgba(0,0,0,0.48)',
                            `0 16px 44px -16px ${glow}`,
                            '0 1px 0 rgba(0,0,0,0.42)'
                          ].join(', ')
                        }}
                        aria-label={h.title}
                      >
                        <div
                          className="pointer-events-none absolute inset-x-0 bottom-0 h-[42%] opacity-[0.28]"
                          style={{
                            background: `linear-gradient(to top, ${glow}, transparent)`
                          }}
                          aria-hidden
                        />
                        <div
                          className="pointer-events-none absolute inset-x-0 bottom-0 h-px opacity-[0.82]"
                          style={{
                            background: `linear-gradient(90deg, transparent, ${glow}, transparent)`
                          }}
                          aria-hidden
                        />
                        <div
                          className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/40 to-transparent opacity-[0.85]"
                          aria-hidden
                        />
                        <div
                          className="pointer-events-none absolute inset-x-0 top-[2.25rem] h-12 -translate-y-1/2 opacity-[0.16]"
                          style={{
                            background: `radial-gradient(ellipse 72% 85% at 50% 50%, ${glow}, transparent 72%)`
                          }}
                          aria-hidden
                        />
                        <div className="relative mb-1.5 flex h-10 w-full max-w-full shrink-0 items-center justify-center">
                          <div
                            className="absolute h-11 w-11 rounded-full opacity-[0.56] blur-lg motion-safe:animate-pulse motion-safe:transition-opacity motion-safe:group-hover:opacity-[0.68]"
                            style={{ background: glow, animationDelay: `${hubIdx * 0.12}s` }}
                            aria-hidden
                          />
                          <div
                            className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full border border-white/[0.12] bg-gradient-to-b from-slate-800/52 via-[#0a1018]/78 to-[#020408]/92 backdrop-blur-sm motion-safe:transition-transform motion-safe:group-active:scale-95"
                            style={{
                              boxShadow: `inset 0 1px 0 rgba(255,255,255,0.12), inset 0 -14px 26px -10px rgba(0,0,0,0.42), inset 0 0 0 1px ${ring}, 0 0 28px -8px ${glow}`
                            }}
                          >
                            <div
                              className="pointer-events-none absolute inset-0 rounded-full opacity-[0.82]"
                              style={{
                                background: `radial-gradient(circle at 50% 24%, ${glow}, transparent 54%)`
                              }}
                              aria-hidden
                            />
                            <span className="relative z-[1] flex items-center justify-center">
                              <HotbarIcon spec={h} />
                            </span>
                          </div>
                          {h.badgeMins ? (
                            <span className="absolute -right-0.5 -top-0.5 z-10 rounded-full border border-violet-500/40 bg-[#2e1065] px-1 py-0.5 text-[7px] font-bold leading-none text-white">
                              {formatDurationMins(nowClock.mins)}
                            </span>
                          ) : null}
                        </div>
                        <p className="relative z-[1] w-full min-w-0 truncate px-0.5 text-center text-[9px] font-bold leading-tight text-white">
                          {h.title}
                        </p>
                      </button>
                      );
                    })}
                    </div>
                  </div>
                </div>
              ) : null}
              <div className="relative flex w-full shrink-0 flex-col overflow-visible border-x border-white/[0.08] border-t border-white/[0.06] bg-[#030508]/95 backdrop-blur-sm">
              <div className="px-2.5 pb-0 pt-2">
              <div className="mx-auto max-w-md space-y-1">
                {panel === 0 || panel === 2 ? (
                  /* PAST / NEXT: keep the dock band, but no mic / copy / wordings (reserve height). */
                  <div
                    className="min-h-[3.25rem] rounded-2xl border border-transparent"
                    aria-hidden
                  />
                ) : !(panel === 1 && nowTvAskOpen) ? (
                  <>
                {nuAskLastAssistantText && nuAskLatestMounted ? (
                  <div
                    className={`transition-[max-height,opacity,transform] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] ${
                      nuAskLatestVisible
                        ? nuAskLatestHolo
                          ? 'max-h-[min(58svh,620px)] opacity-100 translate-y-0'
                          : 'max-h-28 opacity-100 translate-y-0'
                        : 'max-h-0 opacity-0 -translate-y-1 overflow-hidden'
                    }`}
                  >
                    {nuAskLatestHolo ? (
                      <div className="relative flex max-h-[min(58svh,620px)] flex-col overflow-hidden rounded-2xl border border-cyan-400/30 bg-gradient-to-b from-cyan-950/30 via-[#070c10]/96 to-[#030508] px-3 py-2.5 shadow-[0_0_44px_-10px_rgba(34,211,238,0.35),inset_0_0_0_1px_rgba(45,226,197,0.14)] backdrop-blur-md">
                        <div
                          className="pointer-events-none absolute inset-0 opacity-[0.12]"
                          style={{
                            backgroundImage:
                              'repeating-linear-gradient(180deg, transparent, transparent 3px, rgba(6,182,212,0.07) 3px, rgba(6,182,212,0.07) 4px)'
                          }}
                          aria-hidden
                        />
                        <div
                          className="pointer-events-none absolute inset-0 opacity-[0.09]"
                          style={{
                            backgroundImage:
                              'linear-gradient(105deg, transparent 40%, rgba(165,243,252,0.04) 50%, transparent 60%)',
                            backgroundSize: '180% 100%',
                            animation: 'nuask-hud-grid-drift 14s linear infinite'
                          }}
                          aria-hidden
                        />
                        <div className="relative flex min-h-0 flex-1 flex-col">
                          <div className="flex shrink-0 items-center justify-between gap-2">
                            <p className="text-[9px] font-semibold uppercase tracking-[0.22em] text-cyan-200/85">
                              {nuAskUiStrings.latest || 'Latest'}
                            </p>
                            <span className="font-mono text-[8px] uppercase tracking-wider text-[#64748B]">
                              {nuAskUiStrings.replyHoloHint || 'Holo'}
                            </span>
                          </div>
                          <p
                            className="mt-2 min-h-0 flex-1 overflow-y-auto overscroll-y-contain break-words whitespace-pre-wrap pb-1 text-[12px] leading-relaxed text-[#e5e7eb]"
                            style={{ overflowWrap: 'anywhere' }}
                          >
                            {nuAskLastAssistantText}
                          </p>
                          <div className="mt-2 grid shrink-0 grid-cols-4 gap-1">
                            <button
                              type="button"
                              onClick={dismissNuAskLatestHolo}
                              className="flex flex-col items-center justify-center rounded-xl border border-white/12 bg-black/40 px-1 py-2 text-[9px] font-semibold uppercase tracking-wide text-cyan-50 active:bg-white/10"
                            >
                              <span className="mb-0.5 font-mono text-[11px] leading-none text-cyan-400/90">1</span>
                              {nuAskUiStrings.replyBack || 'Back'}
                            </button>
                            <button
                              type="button"
                              onClick={() => {
                                if (panel === 1) openNowFullChatInTv();
                                else corridorNavigate('ask-sylvan');
                              }}
                              className="flex flex-col items-center justify-center rounded-xl border border-white/12 bg-black/40 px-1 py-2 text-[9px] font-semibold uppercase tracking-wide text-cyan-50 active:bg-white/10"
                            >
                              <span className="mb-0.5 font-mono text-[11px] leading-none text-cyan-400/90">2</span>
                              {nuAskUiStrings.openFullChat || 'Full chat'}
                            </button>
                            <button
                              type="button"
                              onClick={() => void copyNuAskLatestReply()}
                              className="flex flex-col items-center justify-center rounded-xl border border-white/12 bg-black/40 px-1 py-2 text-[9px] font-semibold uppercase tracking-wide text-cyan-50 active:bg-white/10"
                            >
                              <span className="mb-0.5 font-mono text-[11px] leading-none text-cyan-400/90">3</span>
                              {nuAskUiStrings.replyCopy || 'Copy'}
                            </button>
                            <button
                              type="button"
                              onClick={clearNuAskLatestReply}
                              className="flex flex-col items-center justify-center rounded-xl border border-white/12 bg-black/40 px-1 py-2 text-[9px] font-semibold uppercase tracking-wide text-cyan-50 active:bg-white/10"
                            >
                              <span className="mb-0.5 font-mono text-[11px] leading-none text-cyan-400/90">4</span>
                              {nuAskUiStrings.replyClear || 'Clear'}
                            </button>
                          </div>
                        </div>
                      </div>
                    ) : (
                      <div className="rounded-xl border border-white/[0.08] bg-[#141820]/95 px-2.5 py-1.5 shadow-[0_6px_16px_rgba(0,0,0,0.35)]">
                        <div className="flex items-start justify-between gap-2">
                          <p className="text-[9px] font-semibold uppercase tracking-wide text-[#6B7280]">
                            {nuAskUiStrings.latest || 'Latest'}
                          </p>
                          <button
                            type="button"
                            onClick={() => setNuAskLatestHolo(true)}
                            className="shrink-0 rounded-lg border border-cyan-500/25 bg-cyan-950/40 px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-cyan-200/90 active:opacity-90"
                          >
                            {nuAskUiStrings.replyExpand || 'Expand'}
                          </button>
                        </div>
                        <p className="mt-0.5 line-clamp-3 whitespace-pre-wrap text-[11px] leading-snug text-[#D1D5DB]">
                          {nuAskLastAssistantText}
                        </p>
                      </div>
                    )}
                  </div>
                ) : null}
                {nuAskInlineError ? (
                  <p className="text-center text-[10px] leading-tight text-amber-400/95">{nuAskInlineError}</p>
                ) : null}
                <div className="flex items-center gap-2 rounded-2xl border border-white/[0.08] bg-[#12151c]/95 px-2 py-1.5 backdrop-blur-md">
                  <button
                    type="button"
                    onClick={handleNuAskVoiceToggle}
                    onContextMenu={(evt) => evt.preventDefault()}
                    disabled={nuAskInlineSending || nuAskInlineTyping}
                    title={
                      nuAskSttPhase === 'recording'
                        ? nuAskUiStrings.voiceRecordingHint || 'Tap to stop'
                        : nuAskSttPhase === 'transcribing'
                          ? nuAskUiStrings.voiceTranscribingHint || 'Transcribing…'
                          : nuAskUiStrings.voiceLimitHint || 'Tap to talk · max 20s'
                    }
                    className={
                      nuAskSttPhase === 'recording'
                        ? 'touch-manipulation inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-rose-400/50 bg-rose-500/20 text-[16px] text-rose-100 shadow-[0_0_18px_-6px_rgba(244,63,94,0.55)] transition enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50'
                        : 'touch-manipulation inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-emerald-400/35 bg-emerald-500/12 text-[16px] text-emerald-100 transition enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50'
                    }
                    aria-label={
                      nuAskSttPhase === 'recording'
                        ? nuAskUiStrings.voiceRecordingHint || 'Tap to stop recording'
                        : nuAskUiStrings.ariaAskSylvan || 'Ask Sylvan'
                    }
                    aria-pressed={nuAskSttPhase === 'recording'}
                  >
                    {nuAskSttPhase === 'recording' ? (
                      <span className="text-rose-300">●</span>
                    ) : nuAskSttPhase === 'transcribing' ? (
                      <span className="text-sky-200">…</span>
                    ) : (
                      <span aria-hidden>🎙</span>
                    )}
                  </button>
                  <span className="min-w-0 flex-1 truncate text-[11px] leading-tight text-[#9CA3AF]">
                    {nuAskInlineTyping ? (
                      <span className="text-[#6B7280]">{nuAskUiStrings.typing || 'Sylvan is typing…'}</span>
                    ) : nuAskInlineSending ? (
                      <span className="text-[#6B7280]">Sending…</span>
                    ) : nuAskInlineInput ? (
                      plainTextFromChatMarkdown(nuAskInlineInput)
                    ) : nuAskSttPhase === 'recording' ? (
                      nuAskUiStrings.voiceTapStop || 'Recording… tap to stop'
                    ) : nuAskSttPhase === 'transcribing' ? (
                      nuAskUiStrings.voiceTranscribing || 'Transcribing…'
                    ) : (
                      nuAskUiStrings.voiceTapToTalk || 'Tap to talk'
                    )}
                  </span>
                  {panel === 1 ? (
                    <button
                      type="button"
                      onClick={openNowFullChatInTv}
                      className="shrink-0 rounded-lg px-1.5 py-1 text-[10px] font-medium text-emerald-400/90 hover:bg-white/[0.04] active:opacity-90"
                    >
                      {nuAskUiStrings.openFullChat || 'Full chat'}
                    </button>
                  ) : null}
                </div>
                  </>
                ) : null}
              </div>
              </div>
            </div>
            </div>
          ) : null}
          {!embeddedAppShell && window.InternalShellTwinNav
            ? React.createElement(window.InternalShellTwinNav, {
                variant: 'nuask',
                leftLabel: nuAskUiStrings.navProfile || 'Profile',
                rightLabel: nuAskUiStrings.navExplore || 'Explore',
                ariaLabel: nuAskUiStrings.corridorBottomNavAria || 'Power Up and Explore',
                staticTwinEdge: true
              })
            : null}
        </div>
        {NUASK_A2_COLLAPSE_HOTKEYS && panel === 1 ? (
          <>
            {/* Floating frosted glass arrow only — overlays TV/chat seam; does not reserve hotbar band height. */}
            {!nowHotkeysOpen && !nowHotkeysLeaving ? (
              <button
                type="button"
                id="nuask-a2-tools-toggle"
                aria-expanded={false}
                aria-controls="nuask-a2-hotkey-overlay"
                aria-label={nuAskUiStrings.toolsOpenAria || 'Open tools'}
                onClick={openNowHotkeys}
                className={`pointer-events-auto absolute left-1/2 z-[56] -translate-x-1/2 translate-y-1/2 ${NUASK_A2_SEAM_ARROW_CLASS}`}
                style={{
                  ...NUASK_A2_SEAM_ARROW_STYLE,
                  bottom: `${Math.max(64, nuAskNowDockHeight || 110)}px`
                }}
              >
                <span
                  className="block h-1.5 w-1.5 translate-y-px rotate-[-135deg] border-r-2 border-b-2 border-current"
                  aria-hidden
                />
              </button>
            ) : null}
            {nowHotkeysOpen || nowHotkeysLeaving ? (
            <div
              id="nuask-a2-hotkey-overlay"
              className="pointer-events-auto absolute inset-0 z-[55]"
              aria-hidden="false"
            >
              <button
                type="button"
                className={`${
                  nowHotkeysLeaving ? 'sf-hub-tools-backdrop-out' : 'sf-hub-tools-backdrop-in'
                } absolute inset-0 border-0 bg-black/50`}
                aria-label={nuAskUiStrings.toolsCloseAria || 'Close tools'}
                onClick={closeNowHotkeys}
              />
              <div
                role="dialog"
                aria-label={nuAskUiStrings.toolsLabel || 'More Intelligence'}
                className={`${
                  nowHotkeysLeaving ? 'sf-hub-tools-sheet-out' : 'sf-hub-tools-sheet-in'
                } absolute left-0 right-0 max-h-[min(62dvh,460px)] overflow-y-auto rounded-t-[22px] border border-white/[0.14] border-b-0 px-3 pb-3 pt-2 shadow-[0_-18px_50px_rgba(0,0,0,0.45)] backdrop-blur-[20px]`}
                style={{
                  background: 'rgba(12, 18, 22, 0.92)',
                  bottom: `${Math.max(64, nuAskNowDockHeight || 110)}px`
                }}
                onAnimationEnd={onNowHotkeysSheetAnimEnd}
              >
                <div className="mx-auto mb-2.5 mt-1 h-1 w-10 rounded-full bg-white/25" aria-hidden />
                <p className="mb-2.5 text-center text-[10px] font-bold uppercase tracking-[0.12em] text-[#9CA3AF]">
                  {nuAskUiStrings.toolsLabel || 'More Intelligence'}
                </p>
                <div className="mx-auto grid w-full max-w-[440px] grid-cols-2 gap-2 min-w-0 [touch-action:manipulation]">
                  {nowHotbarItems.map((h, hubIdx) => {
                    const glow = h.glow;
                    const ring = h.ring;
                    const isEmergency = h.key === 'emergency';
                    return (
                      <button
                        key={`a2-${h.key}`}
                        type="button"
                        onClick={() => activateNowHotkey(h)}
                        className={`group relative flex min-h-[4.75rem] min-w-0 flex-col items-center justify-center overflow-hidden rounded-2xl border px-1.5 pb-2 pt-2.5 text-center backdrop-blur-lg transition duration-150 motion-safe:active:scale-[0.97] motion-safe:hover:brightness-110 ${
                          isEmergency ? 'border-rose-400/35' : 'border-white/10'
                        }`}
                        style={{
                          background: [
                            `radial-gradient(96% 52% at 50% 108%, ${glow}, transparent 56%)`,
                            'radial-gradient(95% 54% at 50% -4%, rgba(255,255,255,0.09) 0%, transparent 46%)',
                            'linear-gradient(168deg, rgba(255,255,255,0.078) 0%, rgba(255,255,255,0.03) 11%, rgba(18,24,34,0.58) 40%, rgba(4,6,10,0.94) 100%)'
                          ].join(', '),
                          boxShadow: [
                            'inset 0 1px 0 rgba(255,255,255,0.13)',
                            `inset 0 0 0 1px ${ring}`,
                            `0 12px 36px -14px ${glow}`
                          ].join(', ')
                        }}
                        aria-label={h.title}
                      >
                        <div className="relative mb-1 flex h-9 w-full items-center justify-center">
                          <div
                            className="absolute h-10 w-10 rounded-full opacity-[0.5] blur-lg"
                            style={{ background: glow, animationDelay: `${hubIdx * 0.12}s` }}
                            aria-hidden
                          />
                          <div
                            className="relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-full border border-white/[0.12] bg-gradient-to-b from-slate-800/52 via-[#0a1018]/78 to-[#020408]/92"
                            style={{
                              boxShadow: `inset 0 1px 0 rgba(255,255,255,0.12), inset 0 0 0 1px ${ring}, 0 0 22px -8px ${glow}`
                            }}
                          >
                            <span className="relative z-[1] flex items-center justify-center">
                              <HotbarIcon spec={h} />
                            </span>
                          </div>
                          {h.badgeMins ? (
                            <span className="absolute -right-0.5 -top-0.5 z-10 rounded-full border border-violet-500/40 bg-[#2e1065] px-1 py-0.5 text-[7px] font-bold leading-none text-white">
                              {formatDurationMins(nowClock.mins)}
                            </span>
                          ) : null}
                        </div>
                        <p
                          className={`relative z-[1] w-full truncate px-0.5 text-[11px] font-bold leading-tight ${
                            isEmergency ? 'text-rose-200' : 'text-white'
                          }`}
                        >
                          {h.title}
                        </p>
                        {h.sub ? (
                          <p className="relative z-[1] mt-0.5 line-clamp-2 w-full px-0.5 text-[9px] leading-snug text-[#9CA3AF]">
                            {h.sub}
                          </p>
                        ) : null}
                      </button>
                    );
                  })}
                </div>
                <button
                  type="button"
                  className="mt-2.5 w-full border-0 bg-transparent py-1.5 text-center text-[12px] font-semibold text-[#9CA3AF] hover:text-white"
                  onClick={closeNowHotkeys}
                >
                  {nuAskUiStrings.toolsCollapse || 'Collapse'}
                </button>
              </div>
            </div>
            ) : null}
          </>
        ) : null}
        {SpotlightTourCmp ? (
          <SpotlightTourCmp
            open={corridorTourOpen}
            steps={corridorTourSteps}
            labels={{
              skip: corridorTourLabels.skip,
              next: corridorTourLabels.next,
              done: corridorTourLabels.done,
              dontShowAgain: corridorTourLabels.dontShowAgain
            }}
            showDontShowAgain
            dontShowAgain={corridorTourDontShow}
            onDontShowAgainChange={setCorridorTourDontShow}
            onComplete={closeCorridorTour}
            onSkip={closeCorridorTour}
          />
        ) : null}
        {ex1RadialHint ? (
          <div
            className="pointer-events-none fixed bottom-[calc(5.5rem+env(safe-area-inset-bottom,0px))] left-1/2 z-[270] max-w-[min(90vw,20rem)] -translate-x-1/2 rounded-full border border-white/25 px-3 py-1.5 text-center text-[11px] font-medium text-white/95 shadow-[0_4px_18px_rgba(0,0,0,0.35)]"
            style={{
              backgroundColor: 'rgba(255, 255, 255, 0.12)',
              backdropFilter: 'blur(14px) saturate(1.45)',
              WebkitBackdropFilter: 'blur(14px) saturate(1.45)'
            }}
            role="status"
          >
            Noted · {ex1RadialHint}
          </div>
        ) : null}
        {(() => {
          const Ex1Overlay = typeof window !== 'undefined' ? window.Ex1NowRadialOverlay : null;
          if (!Ex1Overlay || !ex1RadialOpen) return null;
          return (
            <Ex1Overlay
              open={ex1RadialOpen}
              anchor={ex1RadialAnchor}
              wedges={ex1RadialWedges}
              loading={!ex1RadialSeatsReady}
              companionLine={ex1CompanionLine || undefined}
              spacePrompt={ex1SpacePrompt || undefined}
              hisMode={true}
              memoryChips={ex1MemoryChips}
              focusCard={ex1FocusCard || undefined}
              fieldStatus={ex1FieldStatus || undefined}
              hisTv={ex1HisTv || undefined}
              onCancel={handleEx1RadialCancel}
              onSelect={handleEx1RadialSelect}
              onConclude={handleEx1Conclude}
            />
          );
        })()}
      </div>
    );
  }

  if (typeof window !== 'undefined') {
    window.NuAskSylvanPage = NuAskSylvanPage;
  }
})();
