/**
 * 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 (`activity` local datetime ≤ now) from the loaded itinerary timeline; when none yet (e.g. trip starts tomorrow), the PAST rail shows **ghost** slots — never 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 **pastOnly**), not the count of filled **visible** corridor slots (**pastRealCount**, max 3).
 * 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).
 * **Add Here — post-start lock:** For **30 minutes** after the start time of the corridor **previous** activity (`nowPrevCard` source row), **Add Here** is **disabled** (not merely a post-click error); **Replace Next** stays enabled when a real next row exists. While disabled, a **single amber status strip** under the action row explains the gate (same chrome for loading, no-reco, post-start lock, 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.
 * Travel-only mode: when straight-line travel time to the next itinerary stop meets/exceeds the remaining time window,
 * micro-action POIs are suppressed — UI shows only next-stop guidance (ETA, directions). Background geolocation keeps travel math current.
 * NOW rail: “Available Window” sits in a full-width row **below** the three-column corridor 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** tier chips (violet vs slate) when the API sends `recommendation_tier`; catalog **#1** (`nowRecoOptions[0]`) can still show an emerald **STRONG FIT** chip when that row is the ranked-first POI. 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; hero photo fallbacks: POI media → next itinerary activity image → map tile.
 * TOP reco gate: full recommendation container is hidden until the first refresh cycle completes; while awaiting/syncing, a tech “Enter the fray” hold screen shows; on reveal, a short blur/translate-in animation runs.
 * NEXT: **same time-corridor contract** as PAST/NOW — `CORRIDOR_META_BAND` wraps **times row only** (`TIME_CORRIDOR_GRID`), then card rail; third slot uses **`nextAfterCard`** from the **second future** itinerary activity (demo strip when none). Below the rail, **Upcoming highlight** and **After that** sit in the same **`bg-[#030303]`** section shells as PAST **Journey moments**. Highlight hero: activity photo → else **map-image** tile; body from **`nextHighlightBody`**; meta **`nextHighlightDistanceLine`** when coords exist; **`nextHighlightThumbMapUrl`** is a small map **preview** beside “View on Map” (also links to directions when available)—not a help icon; map tile labels use **ASCII** (`mapLabelLetter`) so markers are not emoji “?”. **After that** uses the same **tall image + text** journey-card pattern as PAST when `imageUrl` / itinerary exists.
 * NOW hotbar (panel NOW): **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\`** (**Power Up · Explore**) beneath the inline chat strip. **NOW dock (Explore parity):** four hotkeys use **\`shellTwinDockOuter\` / \`shellTwinDockHubInner\`** (same tokens as Explore/Power Up hub); 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). **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 **four NOW hotkeys** + chat + twin nav (hotkeys sit directly on top of the chat composer band; 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.
 */
(function () {
  'use strict';

  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 '';
  }

  /** 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;
  }

  function isChinaCoords(coords) {
    if (!coords) return false;
    const lat = Number(coords.lat);
    const lng = Number(coords.lng);
    if (!Number.isFinite(lat) || !Number.isFinite(lng)) return false;
    return lat >= 18.0 && lat <= 54.0 && lng >= 73.0 && lng <= 135.0;
  }

  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 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 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 travelMin = 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 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-[8px] 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-[10px] 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-[9px] uppercase tracking-[0.36em] text-rose-300/80">
                  {t.emergencyProtocol || 'Emergency protocol'}
                </p>
                <p className="relative z-[1] mt-3 text-[12px] font-semibold text-white">
                  {t.emergencyConnecting || 'Connecting…'}
                </p>
                <p className="relative z-[1] mt-2 max-w-[17rem] text-[11px] 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-[10px] 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-[12px] 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-[10px] 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-[10px] 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-[10px] 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-[11px] 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 }) {
    const t = nuAskT || {};
    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-[8px] uppercase tracking-[0.28em] text-cyan-400/55">{t.shellNearbyKicker || 'Nearby'}</p>
            <p className="mt-0.5 truncate text-[11px] font-semibold text-white">{t.shellNearbyTitle || 'Explore Nearby'}</p>
          </div>
          <button
            type="button"
            onClick={onBack}
            className="shrink-0 text-[10px] 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-[12px] font-medium text-[#9CA3AF]">{t.loadingNearbyPois || 'Loading catalog POIs…'}</p>
          ) : null}
          {phase === 'error' ? (
            <p className="px-3 py-6 text-center text-[12px] leading-relaxed text-amber-200/95">{errorMessage}</p>
          ) : null}
          {phase === 'ready' && (!pois || pois.length === 0) ? (
            <p className="py-8 text-center text-[11px] 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 = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${poi.lat},${poi.lng}`)}`;
                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-[13px] font-semibold leading-snug text-white">{poi.name || 'Place'}</p>
                      <p className="mt-1 text-[10px] 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 }) {
    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 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-[8px] uppercase tracking-[0.28em] text-sky-400/55">{t.shellImproveKicker || 'Weather first'}</p>
            <p className="mt-0.5 truncate text-[11px] font-semibold text-white">{t.shellImproveTitle || 'Improve Plan'}</p>
          </div>
          <button
            type="button"
            onClick={onBack}
            className="shrink-0 text-[10px] 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-[12px] 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-[12px] 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-[8px] font-mono uppercase tracking-wide text-sky-300/70">{s.label}</p>
                    <p className="mt-0.5 text-[11px] font-semibold text-white">
                      {s.temp_c != null ? `${s.temp_c}°` : '—'}
                    </p>
                    <p className="text-[9px] leading-tight text-[#9CA3AF]">
                      {s.condition || '—'}
                      {s.pop != null ? ` · ${s.pop}% ${t.weatherRainWord || 'rain'}` : ''}
                    </p>
                  </div>
                ))}
              </div>
              <p className="mb-2 text-[10px] 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-[10px] 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-[11px]"
                  >
                    <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-[9px] 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-[9px] 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-[9px] 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-[11px] 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-[9px] font-semibold uppercase tracking-wide text-[#6B7280]">
                    {t.weatherRouteSenseHeading || 'Route sense (mapped stops)'}
                  </p>
                  <p className="mt-1 text-[10px] 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-[10px] 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-[10px] 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-[11px] 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.start_time_local || '—'})</span>
                        <span className="block text-[9px] text-sky-200/75">{p.reason}</span>
                      </li>
                    ))}
                  </ol>
                </div>
              ) : null}
              <p className="text-[9px] 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 }) {
    const t = nuAskT || {};
    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-[8px] uppercase tracking-[0.28em] text-violet-400/55">{t.shellTimeKicker || 'Time budget'}</p>
            <p className="mt-0.5 truncate text-[11px] font-semibold text-white">{t.shellTimeTitle || 'Use My Time'}</p>
            <p className="mt-1 text-[9px] 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-[10px] 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-[12px] font-medium text-[#9CA3AF]">{t.scoringWindowPois || 'Scoring POIs against your window…'}</p>
          ) : null}
          {phase === 'error' ? (
            <p className="px-2 py-6 text-center text-[12px] 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-[11px] font-semibold text-amber-100">{t.noMinutesInWindow || 'No minutes left in this window'}</p>
              <p className="mt-1.5 text-[10px] 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-[11px] text-[#9CA3AF]">{t.noPoisInRange || 'No catalog POIs in range.'}</p>
          ) : null}
          {phase === 'ready' && W > 0 && rows && rows.length > 0 ? (
            <>
              <p className="mb-2 text-[9px] 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 = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${poi.lat},${poi.lng}`)}`;
                  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-[13px] font-semibold leading-snug text-white">
                            {poi.name || nuAskUiStrings.placeWord || 'Place'}
                          </p>
                          <span className={`shrink-0 rounded border px-1.5 py-px text-[8px] font-bold uppercase tracking-wide ${badge.cls}`}>
                            {badge.text}
                          </span>
                        </div>
                        <p className="mt-1 text-[10px] text-violet-200/75">
                          {cat} · {dm}
                        </p>
                        <p className="mt-1 font-mono text-[9px] leading-relaxed text-[#9CA3AF]">{leg}</p>
                      </a>
                    </li>
                  );
                })}
              </ul>
            </>
          ) : null}
        </div>
      </div>
    );
  }

  function NuAskSylvanPage({ embeddedAppShell = false } = {}) {
    const scrollerRef = useRef(null);
    const tourPanelNavRef = useRef(null);
    const tourNowRailRef = useRef(null);
    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: '🦁'
    });
    /** Third corridor slot on NEXT — following itinerary activity after `nowNextCard` (demo when no data). */
    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);
    /** Start time (ms) of the latest itinerary row at/ before “now” — drives 30m Add Here lock vs Replace Next. */
    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');
    /** 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);
    /** 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('');
    /** 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 [nowRecoProposal, setNowRecoProposal] = useState(null);
    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),
        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;
    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);
    const nuAskLatestHideTimerRef = useRef(null);
    const nuAskLatestClearTimerRef = useRef(null);
    const [nuAskSttPhase, setNuAskSttPhase] = useState('idle');
    const [nuAskSttSupported] = useState(() => {
      try {
        return !!(navigator?.mediaDevices?.getUserMedia && window?.MediaRecorder);
      } catch (e) {
        return false;
      }
    });
    const nowRecoReqSeqRef = useRef(0);
    const nowRecoInFlightRef = useRef(false);
    const prevPanelRef = useRef(-1);
    const nuAskNotifyPromptedRef = useRef(false);
    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);
    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 nowClockRef = useRef(nowClock);
    const nowNextCoordsRef = useRef(nowNextCoords);
    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) => {
      setNowEnRouteLockPoiName(asText(poiName).trim());
      setNowEnRouteLockPoiId(normalizePoiIdKey(poiId));
      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;
          }
          const baked =
            typeof window !== 'undefined' && window.NUASK_UI_DEFAULTS ? window.NUASK_UI_DEFAULTS : null;
          if (baked) {
            for (const k of Object.keys(baked)) {
              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]);

    useEffect(() => {
      const c = liveUserCoords;
      if (!c || !Number.isFinite(c.lat) || !Number.isFinite(c.lng)) return;
      if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
      navigator.serviceWorker.ready
        .then((reg) => {
          if (!reg?.active) return;
          reg.active.postMessage({
            type: 'NUASK_BACKGROUND_SET_COORDS',
            payload: { lat: c.lat, lng: c.lng }
          });
        })
        .catch(() => {});
    }, [liveUserCoords]);

    useEffect(() => {
      if (panel !== 1) return;
      if (nuAskNotifyPromptedRef.current) return;
      nuAskNotifyPromptedRef.current = true;
      if (typeof Notification === 'undefined' || typeof Notification.requestPermission !== 'function') return;
      if (Notification.permission === 'default') {
        Notification.requestPermission().catch(() => {});
      }
    }, [panel]);

    /** Light geolocation while NOW is visible so travel-vs-window works without tapping sync first. */
    useEffect(() => {
      if (panel !== 1 || !navigator.geolocation) return undefined;
      let cancelled = false;
      const read = () => {
        navigator.geolocation.getCurrentPosition(
          (pos) => {
            if (cancelled) return;
            setLiveUserCoords({ lat: pos.coords.latitude, lng: pos.coords.longitude });
          },
          () => {},
          { 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 timeline = corridorActivities
          .map((a) => {
            const dt = getActivityDateTime(a);
            return dt ? { ...a, __dt: dt } : null;
          })
          .filter(Boolean)
          .sort((a, b) => a.__dt.getTime() - b.__dt.getTime());

        const pastOnly = timeline.filter((a) => a.__dt.getTime() <= now.getTime());
        const futureOnly = timeline.filter((a) => a.__dt.getTime() > now.getTime());

        /** Do not fall back to `timeline` when `pastOnly` is empty — that mislabels future stops as PAST and inflates counts (Issue #93). */
        const visiblePast = pastOnly.slice(-3);

        setPlacesActivityCount(pastOnly.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);
          setNowCorridorPrevStartMs(0);
          setNowClock({
            nowLabel: formatTimeLabel(now),
            endLabel: formatTimeLabel(now),
            mins: 0
          });
          return;
        }

        const prev = pastOnly.length > 0 ? pastOnly[pastOnly.length - 1] : null;
        const next = futureOnly.length > 0 ? futureOnly[0] : null;

        if (prev) {
          setNowCorridorPrevStartMs(prev.__dt.getTime());
          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 {
          setNowCorridorPrevStartMs(0);
        }

        if (next) {
          setHasUpcomingActivity(true);
          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);
          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 end = next?.__dt ? new Date(next.__dt) : new Date(now);

        const mins = Math.max(0, Math.round((end.getTime() - now.getTime()) / 60000));
        setNowClock({
          nowLabel: formatTimeLabel(now),
          endLabel: formatTimeLabel(end),
          mins
        });
      };

      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-only for now.')
            .replace(/\{poi\}/g, nowEnRouteLockPoiName || asText(nowNextCard?.title) || (nuAskUiStrings.panelNowTitle || 'NOW'))
        );
        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 = async () => {
        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;
        }
      };

      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);

        const nextAct = nowNextCoordsRef.current;
        const clock = nowClockRef.current;
        if (nextAct && clock && clock.mins > 0) {
          const kmEdge = haversineKm(coords.lat, coords.lng, nextAct.lat, nextAct.lng);
          if (kmEdge != null) {
            const travelEdgeMin = roughUrbanDriveMinutes(kmEdge);
            if (travelEdgeMin >= clock.mins - 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);
        const 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);
        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);
        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 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);

      const getCoords = async () => {
        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;
        }
      };

      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);

      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]);

    /** 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([]);

      const getCoords = async () => {
        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;
        }
      };

      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);

      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 })
        });
        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, nuAskUiStrings]);

    /** 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([]);

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

      const getCoords = async () => {
        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;
        }
      };

      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);

      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 })
        });
        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, nuAskUiStrings]);

    /** 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' });

      const getCoords = async () => {
        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;
        }
      };

      const readChatId = () => {
        try {
          const s = localStorage.getItem('sylvanflow_chat_id');
          if (s && typeof s === 'string' && s.startsWith('chat:')) return s;
        } catch (e) {
          /* ignore */
        }
        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);
        const languageCode = resolveNuAskPreferredLanguageCode();
        const requestBody = {
          message: 'EMERGENCY',
          chat_id: readChatId(),
          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]);

    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 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);
          if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
            navigator.serviceWorker.ready
              .then((reg) => {
                if (reg?.active) reg.active.postMessage({ type: 'NUASK_BACKGROUND_TRIGGER' });
              })
              .catch(() => {});
          }
          return;
        }

        setNuAskInlineTyping(true);
        const polled = await waitForNuAskAssistantReply({
          timeoutMs: 12000,
          intervalMs: 1500,
          sinceTimestamp: userTimestamp
        });
        setNuAskInlineTyping(false);
        if (polled) {
          const nextLatest = polled.trim();
          showNuAskLatestCard(nextLatest);
          if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
            navigator.serviceWorker.ready
              .then((reg) => {
                if (reg?.active) reg.active.postMessage({ type: 'NUASK_BACKGROUND_TRIGGER' });
              })
              .catch(() => {});
          }
        } 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;
    }, []);

    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('Voice clip too large. Keep clips under 20 seconds.');
            setNuAskSttPhase('idle');
            return;
          }
          const audioBase64 = arrayBufferToBase64(arr);
          const sttLanguageCode = resolveNuAskPreferredLanguageCode();
          const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/stt/transcribe`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              audio_base64: audioBase64,
              mime_type: blob.type || 'audio/webm',
              duration_ms: durationMs,
              languageCode: sttLanguageCode
            })
          });
          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('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 || 'Could not transcribe voice.');
          setNuAskSttPhase('idle');
        }
      },
      [SF_API_BASE, nuAskUiStrings, sendNuAskInlineChat, touchNuAskChatBox]
    );

    const stopNuAskVoiceCapture = useCallback(() => {
      const activeRecorder = nuAskSttRecorderRef.current;
      if (!activeRecorder || activeRecorder.state !== 'recording') return;
      setNuAskSttPhase('transcribing');
      try {
        activeRecorder.stop();
      } catch (e) {
        setNuAskInlineError('Could not stop recording.');
        setNuAskSttPhase('idle');
        cleanupNuAskSttCapture();
      }
    }, [cleanupNuAskSttCapture]);

    const startNuAskVoiceCapture = useCallback(async () => {
      if (!nuAskSttSupported) {
        setNuAskInlineError('Voice input is not supported on this device.');
        return;
      }
      if (nuAskInlineSending || nuAskInlineTyping || nuAskSttPhase === 'transcribing') return;
      const activeRecorder = nuAskSttRecorderRef.current;
      if (activeRecorder && activeRecorder.state === 'recording') return;
      try {
        setNuAskInlineError('');
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        nuAskSttStreamRef.current = stream;
        nuAskSttChunksRef.current = [];
        nuAskSttStartedAtRef.current = Date.now();
        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('Voice recording failed. Please try again.');
          setNuAskSttPhase('idle');
          cleanupNuAskSttCapture();
        };
        recorder.onstop = async () => {
          const chunks = Array.isArray(nuAskSttChunksRef.current) ? nuAskSttChunksRef.current : [];
          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) {
            setNuAskSttPhase('idle');
            return;
          }
          if (blob.size < 720) {
            setNuAskInlineError('Recording too short — hold the mic a moment longer.');
            setNuAskSttPhase('idle');
            return;
          }
          await transcribeNuAskVoiceBlob(blob, Math.min(elapsed, NUASK_STT_MAX_MS));
        };
        recorder.start(120);
        setNuAskSttPhase('recording');
        nuAskSttTimeoutRef.current = setTimeout(() => {
          const current = nuAskSttRecorderRef.current;
          if (current && current.state === 'recording') {
            stopNuAskVoiceCapture();
          }
        }, NUASK_STT_MAX_MS + 300);
      } catch (e) {
        setNuAskInlineError('Microphone permission denied or unavailable.');
        setNuAskSttPhase('idle');
        cleanupNuAskSttCapture();
      }
    }, [
      NUASK_STT_MAX_MS,
      cleanupNuAskSttCapture,
      nuAskInlineSending,
      nuAskInlineTyping,
      nuAskSttPhase,
      nuAskSttSupported,
      stopNuAskVoiceCapture,
      transcribeNuAskVoiceBlob
    ]);

    const handleNuAskVoicePressStart = useCallback(
      (evt) => {
        evt?.preventDefault?.();
        touchNuAskChatBox();
        void startNuAskVoiceCapture();
      },
      [startNuAskVoiceCapture, touchNuAskChatBox]
    );

    const handleNuAskVoicePressEnd = useCallback(
      (evt) => {
        evt?.preventDefault?.();
        stopNuAskVoiceCapture();
      },
      [stopNuAskVoiceCapture]
    );

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

    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]);

    useEffect(() => {
      const enteredNow = panel === 1 && prevPanelRef.current !== 1;
      prevPanelRef.current = panel;
      if (!enteredNow) return;
      if (nowRecoInFlightRef.current || nowRecoLoading) return;
      const hasFreshReco = nowRecoOptions.length > 0 && nowRecoPersistUntilMs > Date.now();
      if (hasFreshReco) return;
      refreshNowRecommendations();
    }, [panel, SF_API_BASE, nowRecoLoading, nowRecoOptions.length, nowRecoPersistUntilMs]);

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

    useEffect(() => {
      if (wasNowRecoLoadingForRevealRef.current && !nowRecoLoading && nowRecoSyncPhase === 'ready') {
        setTopRecoRevealKey((k) => k + 1);
      }
      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 || '');
        if (until > Date.now()) {
          setNowEnRouteLockUntilMs(until);
          setNowEnRouteLockPoiName(poi);
          setNowEnRouteLockPoiId(poiId);
        } 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('');
        return undefined;
      }
      const t = setTimeout(() => {
        setNowEnRouteLockUntilMs(0);
        setNowEnRouteLockPoiName('');
        setNowEnRouteLockPoiId('');
      }, 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()) {
          localStorage.setItem(
            EN_ROUTE_LOCK_STORAGE_KEY,
            JSON.stringify({
              until_ms: nowEnRouteLockUntilMs,
              poi_name: nowEnRouteLockPoiName || '',
              poi_id: nowEnRouteLockPoiId || ''
            })
          );
        } else {
          localStorage.removeItem(EN_ROUTE_LOCK_STORAGE_KEY);
        }
      } catch {}
    }, [nowEnRouteLockUntilMs, nowEnRouteLockPoiName, nowEnRouteLockPoiId]);

    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(() => {
      setProfilePhotoUrl(null);
      if (!profile || !SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) return;

      const CACHE_KEY = 'profile_photo_cache_v1';
      const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;

      if (profile.profile_photo_url && !profile.profile_photo_url.includes('/api/profile-photo/')) {
        setProfilePhotoUrl(profile.profile_photo_url);
        return;
      }

      const r2Key = profile.profile_photo_r2_key;

      const fetchSelfPhoto = async () => {
        try {
          const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/profile-photo/self`);
          if (res.status === 204 || !res.ok) {
            setProfilePhotoUrl(null);
            return;
          }
          const blob = await res.blob();
          if (blob.size === 0 || !blob.type.startsWith('image/')) {
            setProfilePhotoUrl(null);
            return;
          }
          const reader = new FileReader();
          reader.onloadend = () => {
            const dataUrl = reader.result;
            if (r2Key && dataUrl) {
              try {
                localStorage.setItem(
                  CACHE_KEY,
                  JSON.stringify({ profile_photo_r2_key: r2Key, dataUrl, cachedAt: Date.now() })
                );
              } catch (e) {
                /* ignore */
              }
            }
            setProfilePhotoUrl(dataUrl);
          };
          reader.onerror = () => setProfilePhotoUrl(null);
          reader.readAsDataURL(blob);
        } catch (e) {
          setProfilePhotoUrl(null);
        }
      };

      if (r2Key || (profile.profile_photo_url && profile.profile_photo_url.includes('/api/profile-photo/'))) {
        (async () => {
          try {
            if (r2Key) {
              const cached = localStorage.getItem(CACHE_KEY);
              if (cached) {
                const cacheData = JSON.parse(cached);
                const cacheAge = Date.now() - (cacheData.cachedAt || 0);
                if (
                  cacheData.profile_photo_r2_key === r2Key &&
                  cacheAge < CACHE_MAX_AGE_MS &&
                  cacheData.dataUrl
                ) {
                  setProfilePhotoUrl(cacheData.dataUrl);
                  return;
                }
                localStorage.removeItem(CACHE_KEY);
              }
            }
            await fetchSelfPhoto();
          } catch (e) {
            setProfilePhotoUrl(null);
          }
        })();
      } else if (profile.photo_url) {
        setProfilePhotoUrl(profile.photo_url);
      }
    }, [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) {
          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);
      };
    }, []);

    useEffect(() => {
      const el = scrollerRef.current;
      if (!el) return;
      const scrollToNow = () => {
        const w = el.clientWidth;
        if (w > 0) el.scrollLeft = w;
      };
      scrollToNow();
      requestAnimationFrame(() => requestAnimationFrame(scrollToNow));
      const onScroll = () => {
        const cw = el.clientWidth || 1;
        const i = Math.round(el.scrollLeft / cw);
        setPanel(Math.max(0, Math.min(2, i)));
      };
      el.addEventListener('scroll', onScroll, { passive: true });
      return () => el.removeEventListener('scroll', onScroll);
    }, []);

    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(() => setCorridorTourOpen(true), 500);
      return () => window.clearTimeout(t);
    }, [profile?.user_key, corridorTourForce]);

    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 || nowClock.mins <= 0 || !nowNextCoords) return null;
      const travelMins = travelFromHereToNextMins;
      const windowMins = nowClock.mins;
      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.mins, nowNextCoords, nowNextCard.title, nuAskUiStrings]);

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

    const nextStopDirectionsUrl = useMemo(() => {
      if (!nowNextCoords) return '';
      const q = `${nowNextCoords.lat},${nowNextCoords.lng}`;
      return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(q)}`;
    }, [nowNextCoords]);

    /** NEXT hero sub-line: straight-line km + urban-pace minutes when device location and next coords exist. */
    const nextHighlightDistanceLine = useMemo(() => {
      if (!liveUserCoords || !nowNextCoords) return '';
      const km = haversineKm(liveUserCoords.lat, liveUserCoords.lng, nowNextCoords.lat, nowNextCoords.lng);
      if (km == null || !Number.isFinite(Number(km))) return '';
      const n = Number(km);
      const walkMin = roughUrbanDriveMinutes(n);
      const kmStr = n < 10 ? n.toFixed(1) : String(Math.round(n));
      const tpl = nuAskUiStrings.nextHighlightMetaTemplate || '~{walk} min · {km} km';
      return tpl.replace(/\{walk\}/g, String(walkMin)).replace(/\{km\}/g, kmStr);
    }, [liveUserCoords, nowNextCoords, nuAskUiStrings]);

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

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

    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 = isChinaCoords(pastMiniMapFocus);
      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]);

    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;
    /**
     * 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]
    );

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

    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));
      el.scrollTo({ left: i * w, behavior: 'smooth' });
    }, []);

    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 || !hasUpcomingActivity;
    const nowPrevPlaceholder = !hasActiveTripContext || asText(nowPrevCard?.icon) === '◌';
    const nowNextPlaceholder = !hasActiveTripContext || !hasUpcomingActivity || asText(nowNextCard?.icon) === '◌';
    const enRouteLockActive = nowEnRouteLockUntilMs > Date.now();
    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 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.';
      return '';
    }, [nowRecoActionLoading, hasLiveReco, nowNextPlaceholder, nuAskUiStrings]);
    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-only for now.')
          .replace(/\{poi\}/g, poi);
        const body = nuAskUiStrings.enRouteReplaceOnlyBody || '';
        const countdown = (nuAskUiStrings.enRouteCountdownTemplate || 'Time left before Add unlock: {time}').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 rankedTopReco = nowRecoOptions[0] || null;
    const rankedTopKey = rankedTopReco != null ? getRecoKey(rankedTopReco, 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-[11px] 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 recoWhyLines = (() => {
      if (!hasLiveReco || !topReco) return [];
      const opt = topReco;
      const poi = opt.poi || {};
      const lines = [];
      const fmt = (template, vars) => {
        let out = template;
        for (const [k, v] of Object.entries(vars || {})) {
          out = out.replace(new RegExp(`\{${k}\}`, 'g'), String(v));
        }
        return out;
      };
      if (nowRecoWeatherContext && typeof nowRecoWeatherContext === 'object') {
        const w = nowRecoWeatherContext;
        if (w.summary_line) {
          lines.push(
            fmt(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(
            fmt(
              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(
          fmt(
            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(
          fmt(nuAskUiStrings.whyIntentFitTemplate || 'Intent fit: {reason}.', {
            reason: mr
          })
        );
      const tt = joinTagList(nowRecoTravelerTags, 10);
      if (tt)
        lines.push(
          fmt(
            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(
          fmt(nuAskUiStrings.whyPoiCategoryTagsTemplate || 'POI category tags: {tags}.', {
            tags: ct
          })
        );
      if (vt)
        lines.push(
          fmt(nuAskUiStrings.whyVibeTagsTemplate || 'Behaviour / vibe tags: {tags}.', {
            tags: vt
          })
        );
      if (st)
        lines.push(
          fmt(nuAskUiStrings.whySuitabilityTagsTemplate || 'Suitability tags: {tags}.', {
            tags: st
          })
        );
      const lr = asText(opt.learning_reason);
      if (lr) {
        lines.push(
          fmt(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(
            fmt(nuAskUiStrings.whyRankingSignalTemplate || 'Ranking signal: {parts}.', {
              parts: parts.join(', ')
            })
          );
      }
      const dk = Number(poi.distance_km);
      if (Number.isFinite(dk)) {
        const drive = roughUrbanDriveMinutes(dk);
        lines.push(
          fmt(
            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(
          fmt(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(
            fmt(
              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(
          fmt(
            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;
    })();

    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) => {
      setNowRecoActionError('');
      setNowRecoActionInfo('');
      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;
        }
      }
      if (modeLabel === 'add' && nowEnRouteLockUntilMs > Date.now()) {
        setNowRecoActionInfo(
          (nuAskUiStrings.enRouteReplaceOnlyTitle || 'On the way to {poi}. Replace-only for now.')
            .replace(/\{poi\}/g, nowEnRouteLockPoiName || asText(nowNextCard?.title) || (nuAskUiStrings.panelNowTitle || 'NOW'))
        );
        return;
      }
      if (modeLabel === 'replace' && nowNextPlaceholder) {
        setNowRecoActionError(
          nuAskUiStrings.replaceNextNoFutureActivity || 'No next activity to replace yet.'
        );
        return;
      }

      if (!topReco?.id) {
        setNowRecoActionError(nuAskUiStrings.noLiveRecommendationYet || '');
        return;
      }
      if (!nowRecoMicroActionId) {
        setNowRecoActionError(nuAskUiStrings.patchDisabledNoReco || '');
        return;
      }
      if (!SF_API_BASE || !window.SylvanFlowAuth?.authenticatedFetch) {
        setNowRecoActionError(nuAskUiStrings.errApiUnavailable || '');
        return;
      }

      setNowRecoActionLoading(modeLabel);
      try {
        const now = new Date();
        const clientDayKey = formatClientDayKeyLocal(now);
        const clientLocalHHMM = formatClientLocalHHMM(now);

        // Propose-only flow for Add Here: compute a reasonable slot using x+y+z.
        // If no day itinerary exists yet (first add), server returns NOT_FOUND_STATE and we fall back to immediate patch.
        if (modeLabel === 'add' && liveUserCoords && topReco?.poi) {
          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: topReco.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: topReco.id,
          client_day_key: clientDayKey,
          client_local_hhmm: clientLocalHHMM,
          patch_mode: modeLabel === 'replace' ? 'replace_next' : 'add',
          ...(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();
        if (!res.ok || !data?.ok) throw new Error(data?.error || 'Unable to apply recommendation');
        if (modeLabel === 'add') {
          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
          );
          setNowRecoActionInfo(
            (nuAskUiStrings.enRouteReplaceOnlyTitle || 'On the way to {poi}. Replace-only for now.')
              .replace(/\{poi\}/g, asText(topReco?.poi?.name || topReco?.summary || nowEnRouteLockPoiName || ''))
          );
        } else {
          setNowRecoActionInfo(nuAskUiStrings.recommendationAppliedReplaceNext || 'Recommendation applied (replace-next request sent).');
        }
      } catch (e) {
        setNowRecoActionError(e?.message || 'Unable to apply recommendation');
      } finally {
        setNowRecoActionLoading('');
      }
    };

    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',
          ...(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();
        if (!res.ok || !data?.ok) throw new Error(data?.error || 'Unable to apply recommendation');
        setNowRecoProposal(null);
        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
        );
        setNowRecoActionInfo(
          (nuAskUiStrings.addedToItineraryAtTemplate || 'Added to your itinerary at {time}.').replace(/\{time\}/g, hhmm)
        );
      } catch (e) {
        setNowRecoActionError(e?.message || 'Unable to apply recommendation');
      } finally {
        setNowRecoActionLoading('');
      }
    };

    const skipSelectedRecommendation = () => {
      setNowRecoActionError('');
      setNowRecoActionInfo('Skipped this recommendation for now.');
      if (!hasLiveReco) return;
      const currentKey = topReco ? getRecoKey(topReco, selectedRecoIdx >= 0 ? selectedRecoIdx : 0) : null;
      const remaining = nowRecoOptions.filter((opt, idx) => getRecoKey(opt, idx) !== currentKey);
      setNowRecoOptions(remaining);
      setSelectedRecoKey(remaining.length > 0 ? getRecoKey(remaining[0], 0) : null);
    };

    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 scroll-smooth ${
            panel === 1 && nowTvAskOpen ? 'relative z-[46]' : ''
          }`}
          style={{ WebkitOverflowScrolling: 'touch' }}
        >
          {/* —— 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 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>
            <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>
          </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 flex-col overflow-hidden ${NUASK_LAYOUT.nowSectionPad} ${NUASK_NOW_DOCK_SCROLL_PB}`}
          >
            <div className="relative mb-4">
              <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">{nowNextCard.time}</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">
                    <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: `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
                      type="button"
                      onClick={refreshNowRecommendations}
                      className={`relative z-20 flex h-[120px] w-[120px] shrink-0 items-center justify-center ${nowRecoLoading ? 'cursor-wait' : 'cursor-pointer'}`}
                      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: 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: 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>

              <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">{formatDurationMins(nowClock.mins)}</span>
                  </p>
                  <p className="mt-0.5 text-[11px] text-white">
                    {nowClock.nowLabel} - {nowClock.endLabel}
                  </p>
                  {availableWindowBridgeText ? (
                    <p className="mt-1.5 text-[10px] leading-snug text-[#9CA3AF]">{availableWindowBridgeText}</p>
                  ) : null}
                </div>
              </div>

            </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 z-[50] flex min-h-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 ${NUASK_NOW_TV_RIM_MAX}`}
            >
              {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] bg-[#070a0c]">
                  <div className="flex shrink-0 items-center justify-between gap-2 border-b border-white/[0.08] bg-[#070a0c]/98 px-3 py-2">
                    <button
                      type="button"
                      onClick={closeNowTvAsk}
                      className="rounded-lg px-2 py-1 text-[11px] font-semibold text-cyan-200/95 active:bg-white/10"
                    >
                      {nuAskUiStrings.back || 'Back'}
                    </button>
                    <span className="pointer-events-none text-[11px] 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 })}
                  </div>
                </div>
              ) : null}
              <div
                ref={nowSectionScrollRef}
                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}
                  onBack={closeUseMyTimePanel}
                />
              ) : nowWeatherPlanOpen ? (
                <NuAskWeatherPlanShell
                  phase={nowWeatherPlanPhase}
                  errorMessage={nowWeatherPlanError}
                  data={nowWeatherPlanData}
                  nuAskT={nuAskUiStrings}
                  onBack={closeWeatherPlanPanel}
                />
              ) : nowNearbyOpen ? (
                <NuAskNearbyPoisShell
                  phase={nowNearbyPhase}
                  errorMessage={nowNearbyError}
                  pois={nowNearbyPois}
                  nuAskT={nuAskUiStrings}
                  onBack={closeNearbyPanel}
                />
              ) : !showNowRecoTopStack ? (
                <div
                  className="relative flex min-h-[240px] w-full flex-1 flex-col items-center justify-center overflow-hidden rounded-[14px] border border-cyan-400/30 bg-[#040809] px-5 py-10 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-[9px] 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(13px,4.2vw,20px)] 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>
                  <p
                    className="relative z-[1] mt-4 max-w-[17rem] font-mono text-[9px] uppercase leading-relaxed tracking-[0.22em] text-cyan-100/55"
                  >
                    {nowRecoLoading
                      ? nuAskUiStrings.syncDetailLive || 'Syncing live POIs · catalog rank · your window'
                      : 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
                  key={topRecoRevealKey}
                  className="relative isolate flex w-full min-h-0 flex-col overflow-x-hidden overflow-y-visible rounded-2xl"
                  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>
                  <div
                    className="relative z-10 m-[2px] 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'
                    }}
                  >
              {travelOnlyMode && travelLeaveAlert ? (
                <>
                  <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-[10px] font-bold uppercase tracking-[0.18em] text-[#9CA3AF]">
                      <span className="text-rose-300">●</span> {nuAskUiStrings.yourNextStop || 'Your next stop'}
                    </p>
                    <span className="rounded-full border-2 border-rose-400/70 bg-rose-500/15 px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-rose-100">
                      {nuAskUiStrings.goNow || 'Go now'}
                    </span>
                  </div>
                  <div className="relative h-[176px] w-full overflow-hidden bg-[#1a1018]">
                    {nowNextCoords ? (
                      <img
                        src={`${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=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-[14px] font-bold leading-tight text-white drop-shadow-md">
                        {asText(nowNextCard.title)}
                      </p>
                      <p className="mt-0.5 text-[10px] font-medium text-white/85">
                        {nowNextCard.time} · {nuAskUiStrings.untilWord || 'until'} {nowClock.endLabel}
                      </p>
                    </div>
                  </div>
                  <div className="p-3.5 pr-2.5">
                    <div
                      className={`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"
                    >
                      <p
                        className={`text-[12px] font-semibold leading-snug ${
                          travelLeaveAlert.tone === 'urgent' ? 'text-rose-50' : 'text-amber-50'
                        }`}
                      >
                        {travelLeaveAlert.title}
                      </p>
                      <p className="mt-1.5 text-[11px] leading-relaxed text-white/88">{travelLeaveAlert.body}</p>
                    </div>
                    <div className="mt-3 rounded-xl border border-white/[0.08] bg-black/50 px-3 py-3">
                      <p className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[#9CA3AF]">
                        {nuAskUiStrings.fromLocationToNext || 'From your location → next stop'}
                      </p>
                      <p className="mt-2 text-[28px] font-bold tabular-nums leading-none text-cyan-200">
                        ~{travelFromHereToNextMins != null ? travelFromHereToNextMins : '—'}{' '}
                        {nuAskUiStrings.minSuffix || 'min'}
                      </p>
                      <p className="mt-1.5 text-[11px] text-[#9CA3AF]">
                        {travelKmStraightToNext != null ? formatDistanceLabel(travelKmStraightToNext) : '—'}{' '}
                        {nuAskUiStrings.straightLineUrbanNote || 'straight-line · urban-pace estimate (not live traffic)'}
                      </p>
                      <p className="mt-2 text-[10px] leading-relaxed text-[#6B7280]">{nuAskUiStrings.sidetripOffNote}</p>
                    </div>
                    {nextStopDirectionsUrl ? (
                      <a
                        href={nextStopDirectionsUrl}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="mt-4 flex w-full items-center justify-center rounded-xl py-3 text-[12px] 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-[10px] 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-[9px] 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-[11px] 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-[10px] 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 h-[176px] w-full overflow-hidden bg-[#1c1410]">
                {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>
              <div className="relative p-3.5 pr-2.5">
                <div className="flex items-center justify-between gap-2">
                  <p className="text-[16px] 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-[10px] 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-[12px] leading-relaxed text-[#9CA3AF]">{topRecoDesc}</p>
                    <div className="mt-3 space-y-1.5 text-[11px] text-[#9CA3AF]">
                      {topRecoDistance ? <p>{topRecoDistance}</p> : null}
                      {hasLiveReco && topReco?.suggested_start_hhmm ? (
                        <p className="text-[11px] text-[#A7F3D0]">
                          {nuAskUiStrings.bestSlotLabel || 'Best slot'}: {topReco.suggested_start_hhmm}
                        </p>
                      ) : null}
                      {hasLiveReco && topReco?.action_hint ? (
                        <p className="text-[10px] 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-[10px] text-[#6B7280]">
                          ~{roughUrbanDriveMinutes(Number(topReco.poi.distance_km))}{' '}
                          {nuAskUiStrings.urbanPaceFromDistance || 'min urban pace (from straight-line distance estimate, not live traffic)'}
                        </p>
                      ) : (
                        <p>{DEMO.reco.lineDwell}</p>
                      )}
                      {topRecoRating ? <p>{topRecoRating}</p> : null}
                      {topRecoCategory ? <p className="uppercase tracking-wide text-[#A7F3D0]">{topRecoCategory}</p> : null}
                    </div>
                  </>
                ) : null}
                {nowRecoLoading ? (
                  <p className="mt-3 text-[11px] text-[#9CA3AF]">
                    {nuAskUiStrings.loadingAlternatives || 'Loading alternatives…'}
                  </p>
                ) : null}
                {!nowRecoLoading && nowRecoError ? (
                  <p className="mt-3 text-[11px] text-amber-300">{nowRecoError}</p>
                ) : null}
                {!nowRecoLoading ? (
                  <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-[10px] 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-[11px] 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-[10px] 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-[11px] font-semibold text-white">
                                {opt?.poi?.name || opt?.summary || `${nuAskUiStrings.optionN || 'Option'} ${altRowIndex + 2}`}
                              </p>
                              <p className="text-[10px] text-[#9CA3AF]">{formatDistanceLabel(opt?.poi?.distance_km) || nuAskUiStrings.nearbyLabel || 'Nearby'}</p>
                              {hasLiveReco && opt?.suggested_start_hhmm ? (
                                <p className="text-[10px] text-[#A7F3D0]">
                                  {nuAskUiStrings.bestSlotLabelShort || nuAskUiStrings.bestSlotLabel || 'Best'}: {opt.suggested_start_hhmm}
                                </p>
                              ) : null}
                              {hasLiveReco && opt?.action_hint ? (
                                <p className="text-[10px] 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-[7px] 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-[10px] 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-[10px] 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-[9px]">
                                <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-[10px] 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-[10px] 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-[11px] 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-[10px] 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 className="mt-4 grid grid-cols-3 gap-2">
                  <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-[11px] 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-[10px] 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={!hasLiveReco || !!nowRecoActionLoading || nowNextPlaceholder}
                    title={replaceNextDisabledReason || 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"
                  >
                    <span className="text-[11px] font-bold text-white">{nuAskUiStrings.replaceNext || 'Replace Next'}</span>
                    <span className="mt-0.5 truncate px-0.5 text-[9px] font-medium text-white/85">{nowNextCard.title}</span>
                  </button>
                  <button
                    type="button"
                    onClick={skipSelectedRecommendation}
                    disabled={!!nowRecoActionLoading}
                    className="flex flex-col items-center justify-center rounded-xl bg-[#27272A] py-2.5 text-center active:bg-[#3f3f46]"
                  >
                    <span className="text-[11px] font-bold text-white">{nuAskUiStrings.skip || 'Skip'}</span>
                    <span className="mt-0.5 text-[10px] font-medium text-[#9CA3AF]">
                      {nuAskUiStrings.notNow || 'Not now'}
                    </span>
                  </button>
                </div>
                {addHereDisabled && addHereDisabledBannerLines && addHereDisabledBannerLines.length > 0 ? (
                  <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-[10px] leading-snug text-amber-200/95 ${i > 0 ? 'mt-1' : ''}`}
                      >
                        {line}
                      </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-[10px] 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-[11px] 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-[11px] 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-[11px] 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-[10px] text-emerald-300">{nowRecoActionInfo}</p> : null}
                {nowRecoActionError ? <p className="mt-2 text-[10px] text-amber-300">{nowRecoActionError}</p> : null}
                <div className="mt-4 pt-3">
                  <p className="text-[12px] font-semibold text-[#D1D5DB]">
                    {nuAskUiStrings.whyHeading || 'Why this recommendation?'}
                  </p>
                  {hasLiveReco && recoWhyLines.length > 0 ? (
                    <ul className="mt-1.5 list-disc space-y-1 pl-4 text-[11px] leading-relaxed text-[#6B7280]">
                      {recoWhyLines.map((line, i) => (
                        <li key={`why-${i}`}>{line}</li>
                      ))}
                    </ul>
                  ) : (
                    <p className="mt-1.5 text-[11px] leading-relaxed text-[#6B7280]">
                      {hasLiveReco
                        ? nuAskUiStrings.whyFallbackPull ||
                          'Pull to refresh once more if decision details did not load.'
                        : nuAskUiStrings.whyFallbackWait ||
                          'Waiting for a live catalog-ranked option. Tap refresh after location is available.'}
                    </p>
                  )}
                </div>
              </div>
                </>
              )}
              </div>
                </div>
              )}
            </div>
              </div>
            </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 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>
            <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'}`}>{nowNextCard.time}</p>
                  <span className="min-w-[8px] shrink-0" aria-hidden />
                  <p className={`text-[9px] font-medium ${nextStandbyMode ? 'text-white/55' : 'text-white/90'}`}>{nextAfterCard.time}</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">
                  <div
                    className={`flex ${CORRIDOR_RAIL_H} flex-col justify-center rounded-xl p-2 text-center`}
                    style={{
                      border: nextStandbyMode ? '1px solid rgba(255,255,255,0.08)' : `2px solid ${C.accent}`,
                      boxShadow: nextStandbyMode
                        ? '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 : 1,
                      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(nowNextCard.icon, nextStandbyMode)}
                    </div>
                    <p className="mt-1 text-center text-[10px] font-bold leading-tight text-white">{nowNextCard.title}</p>
                    <p className="mt-0.5 line-clamp-2 text-[9px] font-medium text-[#9CA3AF]">{nowNextCard.sub}</p>
                  </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-white/[0.08] bg-[#11151d] p-2 text-center shadow-[0_8px_24px_-14px_rgba(0,0,0,0.9)]`}
                    style={{
                      opacity: nextStandbyMode ? 0.34 : 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(nextAfterCard.icon, nextStandbyMode)}
                    </div>
                    <p className="mt-1 line-clamp-2 text-[10px] font-semibold leading-tight text-white">{nextAfterCard.title}</p>
                    <p className="mt-0.5 line-clamp-2 text-[9px] text-[#9CA3AF]">{nextAfterCard.sub}</p>
                  </div>
                </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>
              {hasActiveTripContext && hasUpcomingActivity ? (
                <div className="overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d]">
                <div className="relative h-[152px] w-full overflow-hidden bg-[#0c1929]">
                  {nowNextImageUrl ? (
                    <img
                      src={nowNextImageUrl}
                      alt={asText(nowNextCard.title) || ''}
                      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(nowNextCard.icon, 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">{nowNextCard.title}</p>
                  <p className="mt-1 text-[11px] font-semibold text-[#9CA3AF]">
                    {nowNextCard.time}
                    {nowNextCard.sub ? ` · ${nowNextCard.sub}` : ''}
                    {nextHighlightDistanceLine
                      ? ` · ${nextHighlightDistanceLine}`
                      : ''}
                  </p>
                  <p className="mt-2 text-[13px] leading-relaxed text-[#9CA3AF]">
                    {String(nextHighlightBody || '').trim()
                      ? nextHighlightBody
                      : (hasActiveTripContext
                        ? (nuAskUiStrings.noLiveRecommendationYet || 'No upcoming activity yet')
                        : (nuAskUiStrings.errNoTrip || 'No active trip linked'))}
                  </p>
                  <div className="mt-4 flex items-stretch gap-3">
                    {nextStopDirectionsUrl ? (
                      <a
                        href={nextStopDirectionsUrl}
                        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>
                    )}
                    {nextStopDirectionsUrl ? (
                      <a
                        href={nextStopDirectionsUrl}
                        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'}
                      >
                        {nowNextCoords && 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 ${nowNextCoords && 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>
              ) : (
                <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">{nuAskUiStrings.panelNextTitle || 'NEXT'}</p>
                    <p className="mt-1 text-[11px] font-semibold text-[#9CA3AF]">—</p>
                    <p className="mt-2 text-[13px] leading-relaxed text-[#9CA3AF]">
                      {hasActiveTripContext
                        ? (nuAskUiStrings.noLiveRecommendationYet || 'No upcoming activity yet')
                        : (nuAskUiStrings.errNoTrip || 'No active trip linked')}
                    </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>

            {/* After that — same outer shell; inner card matches PAST journey tile (image band + text) */}
            <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.nextAfterThatHeading || 'After that'}
              </p>
              <div className="grid grid-cols-1 gap-2">
                {!hasActiveTripContext || !hasUpcomingActivity ? (
                  <div className="overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d] text-left shadow-[0_8px_24px_-14px_rgba(0,0,0,0.9)] opacity-80">
                    <div className="relative h-[86px] w-full overflow-hidden">
                      <div
                        className="absolute inset-0 flex items-center justify-center text-2xl"
                        style={{
                          background:
                            'linear-gradient(145deg, rgba(13,148,136,0.35) 0%, rgba(15,23,42,0.95) 78%)'
                        }}
                        aria-hidden
                      >
                        ◌
                      </div>
                    </div>
                    <div className="p-2">
                      <p className="text-[9px] font-semibold text-[#9CA3AF]">—</p>
                      <p className="mt-0.5 line-clamp-1 text-[10px] font-semibold text-white">
                        {nuAskUiStrings.panelNextTitle || 'NEXT'}
                      </p>
                      <p className="mt-0.5 line-clamp-1 text-[9px] text-[#9CA3AF]">
                        {hasActiveTripContext
                          ? (nuAskUiStrings.noLiveRecommendationYet || 'No upcoming activity yet')
                          : (nuAskUiStrings.errNoTrip || 'No active trip linked')}
                      </p>
                    </div>
                  </div>
                ) : null}
                {hasActiveTripContext && hasUpcomingActivity ? (
                <button
                  type="button"
                  onClick={() => {
                    const target = nextAfterCardTarget;
                    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={!nextAfterCardTarget}
                  className="overflow-hidden rounded-xl border border-white/[0.08] bg-[#11151d] text-left shadow-[0_8px_24px_-14px_rgba(0,0,0,0.9)] disabled:cursor-not-allowed disabled:opacity-75"
                  aria-label={nextAfterCardTarget ? `Open ${nextAfterCard.title}` : nextAfterCard.title}
                >
                  <div className="relative h-[86px] w-full overflow-hidden">
                    {nextAfterCard.imageUrl ? (
                      <img
                        src={nextAfterCard.imageUrl}
                        alt=""
                        className="h-full w-full object-cover"
                        onError={(e) => {
                          e.target.style.display = 'none';
                          const fb = e.target.nextElementSibling;
                          if (fb) {
                            fb.classList.remove('hidden');
                            fb.style.display = 'flex';
                          }
                        }}
                      />
                    ) : null}
                    <div
                      className={`absolute inset-0 items-center justify-center text-2xl ${nextAfterCard.imageUrl ? 'hidden' : 'flex'}`}
                      style={{
                        background:
                          'linear-gradient(145deg, rgba(13,148,136,0.35) 0%, rgba(15,23,42,0.95) 78%)'
                      }}
                      aria-hidden
                    >
                      {nextAfterCard.icon}
                    </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]">{nextAfterCard.time}</p>
                    <p className="mt-0.5 line-clamp-1 text-[10px] font-semibold text-white">{nextAfterCard.title}</p>
                    <p className="mt-0.5 line-clamp-1 text-[9px] text-[#9CA3AF]">{nextAfterCard.sub}</p>
                  </div>
                </button>
                ) : null}
              </div>
            </div>
            </div>
          </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 ? (
                <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={() => {
                          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);
                        }}
                        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-2 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>
                        <p className="relative z-[1] mt-0.5 line-clamp-2 w-full min-w-0 px-0.5 text-center text-[7px] leading-snug text-[#9CA3AF]">
                          {h.sub}
                        </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 === 1 && nowTvAskOpen) ? (
                  <>
                {nuAskLastAssistantText && nuAskLatestMounted ? (
                  <div
                    className={`overflow-hidden transition-[max-height,opacity,transform] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] ${
                      nuAskLatestVisible
                        ? nuAskLatestHolo
                          ? 'max-h-[min(50svh,520px)] opacity-100 translate-y-0'
                          : 'max-h-28 opacity-100 translate-y-0'
                        : 'max-h-0 opacity-0 -translate-y-1'
                    }`}
                  >
                    {nuAskLatestHolo ? (
                      <div className="relative 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">
                          <div className="flex 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 max-h-[min(38svh,340px)] overflow-y-auto whitespace-pre-wrap text-[12px] leading-relaxed text-[#e5e7eb]">
                            {nuAskLastAssistantText}
                          </p>
                          <div className="mt-3 grid 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"
                    onPointerDown={handleNuAskVoicePressStart}
                    onPointerUp={handleNuAskVoicePressEnd}
                    onPointerCancel={handleNuAskVoicePressEnd}
                    onPointerLeave={handleNuAskVoicePressEnd}
                    onContextMenu={(evt) => evt.preventDefault()}
                    disabled={nuAskInlineSending || nuAskInlineTyping || nuAskSttPhase === 'transcribing'}
                    title={
                      nuAskSttPhase === 'recording'
                        ? nuAskUiStrings.voiceRecordingHint || 'Release to send'
                        : nuAskSttPhase === 'transcribing'
                          ? nuAskUiStrings.voiceTranscribingHint || 'Transcribing…'
                          : nuAskUiStrings.voiceLimitHint || 'Hold to talk · max 20s'
                    }
                    className="touch-none inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-emerald-400/35 bg-emerald-500/12 text-[14px] text-emerald-100 transition enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50"
                    aria-label={nuAskUiStrings.ariaAskSylvan || 'Ask Sylvan'}
                  >
                    {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…'
                    ) : nuAskSttPhase === 'transcribing' ? (
                      nuAskUiStrings.voiceTranscribing || 'Transcribing…'
                    ) : (
                      nuAskUiStrings.voiceTapToTalk || 'Hold 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}
        {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}
          {!embeddedAppShell && window.InternalShellTwinNav
            ? React.createElement(window.InternalShellTwinNav, {
                variant: 'nuask',
                leftLabel: nuAskUiStrings.navPowerUp || 'Profile',
                rightLabel: nuAskUiStrings.navExplore || 'Explore',
                ariaLabel: nuAskUiStrings.corridorBottomNavAria || 'Power Up and Explore',
                staticTwinEdge: true
              })
            : null}
        </div>
      </div>
    );
  }

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