/**
 * ═══════════════════════════════════════════════════════════════════
 * Power Up AI Page Component (SPA)
 * ═══════════════════════════════════════════════════════════════════
 *
 * Architecture overview:
 * - Reads profile via GET /api/profile/me (Bearer); four **hub tiles** route the **TV plate** only (no Explore edits): green **Summary** · **blue** **Flow Finder** (`iframe`) · purple **Create Flow Plan** (`iframe` `/flow-plan-form`) · orange **Ask Sylvan** (`AskSylvanPage` + `embeddedExploreTv`, same embedded pattern as elsewhere).
 * - `postMessage`: `flow-finder` → complete switches TV to Flow Plan iframe; close → Summary. `flow-plan-form` → submitted → **post-plan welcome TV** once (`POWERUP_POST_PLAN_INTRO_SEEN_KEY`), then Summary + profile refresh. Flow Finder “already complete” tap → **fullscreen dim + SYS toast** (~4.8s).
 * - **Layout spotlight tour** (`SpotlightTour.jsx`): multi-step coach (menu → TV → hub tiles → twin nav); auto once per `user_key` for incomplete / pre–Flow Finder users; replay from Menu. Storage: `sf_pu_layout_tour_done_v1_*` via `spotlight-tour-storage.js`.
 * - Incomplete profiles: **language-first** setup in the Summary TV (step 1 → reload → step 2 details + photo), then Flow Finder quiz via hub iframe (not full-page redirect).
 * - Optional photo upload remains here when profile is complete but missing photo.
 *
 * Features:
 * - TV viewport: `tvPanel` — summary · Flow Finder · Flow Plan · Ask Sylvan · post-plan welcome; **`POWERUP_TV_LAYOUT.rimFill`** flex-fills for iframe/embed panels; summary uses **`rimSummaryGrow`** + **`SYLVAN_TECH_THEME.classes.tvRimMaxHeight`** so the rim fills viewport below the hero (personality scrolls inside). Summary **TV rim** uses **`POWERUP_TV_RIM_GRADIENT`** (emerald → teal → sky → violet → orange) in **`recoTopDefault`** gutter (`p-[2px]`); **inner TV plate** uses layered glass (neutral/slate rim on `#070a0c`). **`SYLVAN_TECH_THEME.classes.dockScrollPaddingBottom`** clears the fixed hub + twin nav; **`PowerUpBottomChrome`** stacks **`shellTwinDockOuter`** at **`z-[2]`** above **`InternalShellTwinNav`** (**`z-[1]`**) so twin rim glow sits visually behind the tiles (**`sf-twin-shell-nav-pulse`** unchanged). Top header keeps **`sf-power-up-header-edge-pulse`** (bottom edge). Hub band uses **`shellTwinDockOuter`** / **`shellTwinDockHubInner`** (aligned with Explore): **`shellTwinDockOuter`** top edge above the four tiles is **static** (no `sf-explore-header-edge-pulse` on that band). Flow Plan / Ask Sylvan embed modes use **`TV_EMBED_CHROME_CLASSES`**.
 * - Hero: optional **`visual_profile`** carousel — six-slide flow with **sentence-level conclusions only** (no raw tags/chips/matched_tags exposed); horizontal **`snap-x`** strip + dot pager; scroll sync maps **`scrollLeft`** to page; first/last wrap uses **`onVisualCarouselTouchEnd`** + **`vcWrapLockUntilRef`** (no DOM clone strip). Flow Shape keeps five dimensions as numbers + plain-language interpretation; Sylvan slide uses **outcome statements** derived from scores (no POI/tag vocabulary). **`visual_profile.archetype`** is an **LLM-produced short label** (Flow Pulse `generateVisualProfile`), not a fixed catalog of N types. Carousel slide backdrops: **travel-type** uses **`resolvePowerUpArchetypeHeroUrl(archetype)`** (regex on archetype only) plus a faint fixed **journey-drive** underlay and **moment** textures on the two travel-type cards (**travel-type** slide stacks the two cards with **`justify-center`** so they sit mid-viewport when space allows); **other slides** use fixed **`powerup-moment-*.png`** pairings per slide (editorial, not tag-derived) with **`renderPowerUpSlideImageBackdrop`** presets **`soft` · `chart` · `logic` · `modes` · `prose`** — slightly higher photo opacity and lighter scrims than earlier so background art reads through; **travel-type** keeps preset **`hero`** unchanged. Slides after Travel Type wrap each slide body in **`flex-1 justify-center`** for vertical centering when content is short; **Best modes** uses **`justify-start`** so tall mode cards stay scrollable from the top. **`/assets/powerup-nature-hero.png`** remains load fallback. **Why-profile** slide shows the **full** narrative HTML in the slide body (carousel `overflow-y-auto`); no inner card border or expand/collapse control. Dashboard sentences use **`PU_VISUAL_DASHBOARD_FALLBACK_*`** + **`ui:powerup_visual_dashboard_*`** SYS KV (rim carousel titles remain **`ui:powerup_visual_*`**); no `zh` branching in builders. Above the TV rim, the **display name · welcome** strip uses **`flex-col items-center justify-center py-3`**; summary TV chrome line **`heresWhatSylvanSees`** sits in a **`flex items-center justify-center`** band with **`text-center`** (gradient clip).
 * - One primary four-tile hub row (`max-w-[440px]`): **glassmorphism** rounded cards, circular neon icon wells, SYS labels (`ui:powerup_hub_*_title` / `_sub`, KV). Bottom chrome is twin nav only (no duplicate tiles; no inline copyright/legal strip).
 * - Full-screen intro overlay (~3s)
 * - **First-run vision overlay** (`window.FounderVisionOverlay`): once per `user_key` (`sf_pu_first_run_vision_v1_*`), after boot intro when `needsProfile`; copy from SYS `sys:ui:founder_overlay_*` + title `sys:ui:app_drawer_why_sylvanflow` (10 langs via `getSysMessage`). Layout tour waits until Founder vision is dismissed. Menu reopens same overlay anytime (`allowBackdropDismiss`). See **`docs/pillars/SYSTEM_MAP.md`** (Founder vision overlay).
 * - Legal hard exit helper for `/legal`
 *
 * Top header: menu · fluid title/subtitle (`clamp`) · profile orb, or **Skip** during layout tour (same slot).
 * Entry: ~3s full-screen intro (logo + grid/scan + fray dots) then fade out.
 */

(function() {
  'use strict';

  const { useState, useEffect, useRef, useMemo, useCallback, createElement } = React;
  // ✅ GATEWAY MIGRATION: Use SF_API_BASE from shared-api-config.js (no duplicate declaration)
  const SF_API_BASE = window.SF_API_BASE;

  /** One-time founder / mission framing before incomplete-profile form (per opaque `user_key`). */
  const FIRST_RUN_VISION_SEEN_PREFIX = 'sf_pu_first_run_vision_v1_';
  /** After Flow Plan form submit, one-time welcome TV — dismissed via Continue (Power Up only). */
  const POWERUP_POST_PLAN_INTRO_SEEN_KEY = 'sf_powerup_post_plan_intro_seen_v1';
  const POWERUP_POST_PLAN_INTRO_FALLBACK = {
    title: "You're in — Sylvan knows you now",
    lead:
      'Personal decision-making is tuned to your profile. Here is how to get the most from the app.',
    detail:
      'SylvanFlow uses your Flow Finder profile and Flow Plan to personalize timing, pacing, and suggestions.\n\nUse the four tiles below this screen — Summary, Flow Finder, Create Flow Plan, and Ask Sylvan — anytime.',
    hotkeys:
      'Summary — Profile\nFlow Finder — Quiz\nCreate Flow Plan — Trip\nAsk Sylvan — Chat',
    continueCta: 'Continue to Summary',
    videoUrl: ''
  };

  /**
   * TV chrome row (Flow Plan / Ask Sylvan): must match app language when SYS KV misses or loads late.
   * Keys mirror ui:powerup_tv_flow_plan_chrome_title | _back_label | ui:powerup_tv_ask_sylvan_chrome_title.
   */
  const POWERUP_TV_CHROME_FALLBACK_BY_LANG = {
    en: {
      flowPlanChromeTitle: 'Create New Flow Plan',
      flowPlanBackLabel: 'Summary',
      askSylvanChromeTitle: 'Ask Sylvan'
    },
    'zh-CN': {
      flowPlanChromeTitle: '新建 Flow Plan',
      flowPlanBackLabel: '摘要',
      askSylvanChromeTitle: '问 Sylvan'
    },
    ja: {
      flowPlanChromeTitle: '新規 Flow Plan',
      flowPlanBackLabel: '概要',
      askSylvanChromeTitle: 'Sylvanに質問'
    },
    ko: {
      flowPlanChromeTitle: '새 Flow Plan',
      flowPlanBackLabel: '요약',
      askSylvanChromeTitle: 'Sylvan에게 물어보기'
    },
    th: {
      flowPlanChromeTitle: 'สร้าง Flow Plan ใหม่',
      flowPlanBackLabel: 'สรุป',
      askSylvanChromeTitle: 'ถาม Sylvan'
    },
    ar: {
      flowPlanChromeTitle: 'إنشاء Flow Plan جديد',
      flowPlanBackLabel: 'الملخص',
      askSylvanChromeTitle: 'اسأل Sylvan'
    },
    id: {
      flowPlanChromeTitle: 'Buat Flow Plan Baru',
      flowPlanBackLabel: 'Ringkasan',
      askSylvanChromeTitle: 'Tanya Sylvan'
    },
    ms: {
      flowPlanChromeTitle: 'Cipta Flow Plan Baharu',
      flowPlanBackLabel: 'Ringkasan',
      askSylvanChromeTitle: 'Tanya Sylvan'
    },
    vi: {
      flowPlanChromeTitle: 'Tạo Flow Plan mới',
      flowPlanBackLabel: 'Tóm tắt',
      askSylvanChromeTitle: 'Hỏi Sylvan'
    },
    fil: {
      flowPlanChromeTitle: 'Gumawa ng Bagong Flow Plan',
      flowPlanBackLabel: 'Summary',
      askSylvanChromeTitle: 'Tanungin si Sylvan'
    }
  };

  function resolvePowerUpTvChromeFallback(langTag) {
    const raw = String(langTag || 'en').trim();
    const lower = raw.toLowerCase();
    const map = POWERUP_TV_CHROME_FALLBACK_BY_LANG;
    if (map[raw]) return map[raw];
    if (lower.startsWith('zh-tw') || lower === 'zh-hant') return map['zh-CN'];
    if (lower.startsWith('zh')) return map['zh-CN'];
    if (lower.startsWith('ja')) return map.ja;
    if (lower.startsWith('ko')) return map.ko;
    if (lower.startsWith('th')) return map.th;
    if (lower.startsWith('ar')) return map.ar;
    if (lower.startsWith('id')) return map.id;
    if (lower.startsWith('ms')) return map.ms;
    if (lower.startsWith('vi')) return map.vi;
    if (lower.startsWith('fil') || lower === 'tl') return map.fil;
    return map.en;
  }

  /**
   * Rich Profile carousel copy — local fallbacks when SYS misses (10 locales).
   * Mirrors `resolvePowerUpTvChromeFallback` language resolution.
   */
  const POWERUP_VISUAL_CAROUSEL_EXTRA_FALLBACK_BY_LANG = {
    en: {
      travelTypeTitle: 'Travel Type',
      travelDnaSubtitleLine: 'Travel DNA',
      flowShapeTitle: 'Flow Shape',
      insightMomentsTitle: 'Insight Moments',
      bestModesTitle: 'Best Travel Modes',
      swipeHint: 'Swipe to explore',
      shapeIndexLabel: 'Shape index',
      flowFinderCaption: 'Based on your Flow Finder profile',
      strongestSignalPrefix: 'Strongest signal',
      whenThisFits: 'When this fits you',
      readFullProfile: 'Read full profile',
      hideFullProfile: 'Hide full profile',
      insightsEmptyLead: 'Highlight cards appear here when your profile includes them.'
    },
    'zh-CN': {
      travelTypeTitle: '旅行类型',
      travelDnaSubtitleLine: '旅行 DNA',
      flowShapeTitle: '你的 Flow 形状',
      insightMomentsTitle: '洞察时刻',
      bestModesTitle: '最适合你的旅行方式',
      swipeHint: '左右滑动查看更多',
      shapeIndexLabel: '综合指数',
      flowFinderCaption: '根据您的 Flow Finder 测验生成',
      strongestSignalPrefix: '最强信号',
      whenThisFits: '何时适合你',
      readFullProfile: '完整解读',
      hideFullProfile: '收起完整解读',
      insightsEmptyLead: '当有个性化洞察卡片时，将显示在这里。'
    },
    ja: {
      travelTypeTitle: '旅行タイプ',
      travelDnaSubtitleLine: 'トラベルDNA',
      flowShapeTitle: 'あなたの Flow の形',
      insightMomentsTitle: '洞察の瞬間',
      bestModesTitle: 'あなたに合う旅行モード',
      swipeHint: 'スワイプでもっと見る',
      shapeIndexLabel: 'シェイプ指数',
      flowFinderCaption: 'Flow Finder の結果に基づきます',
      strongestSignalPrefix: '最も強いシグナル',
      whenThisFits: 'こんなときに合う',
      readFullProfile: '全文を読む',
      hideFullProfile: '全文を閉じる',
      insightsEmptyLead: 'ハイライトがある場合にここに表示されます。'
    },
    ko: {
      travelTypeTitle: '여행 유형',
      travelDnaSubtitleLine: '여행 DNA',
      flowShapeTitle: '당신의 Flow 형태',
      insightMomentsTitle: '인사이트 순간',
      bestModesTitle: '당신에게 맞는 여행 모드',
      swipeHint: '스와이프하여 더 보기',
      shapeIndexLabel: '형태 지수',
      flowFinderCaption: 'Flow Finder 결과를 바탕으로 함',
      strongestSignalPrefix: '가장 강한 신호',
      whenThisFits: '이럴 때 잘 맞음',
      readFullProfile: '전체 프로필 읽기',
      hideFullProfile: '접기',
      insightsEmptyLead: '프로필에 하이라이트가 있으면 여기에 표시됩니다.'
    },
    th: {
      travelTypeTitle: 'สไตล์การเดินทาง',
      travelDnaSubtitleLine: 'DNA การเดินทาง',
      flowShapeTitle: 'รูปแบบ Flow ของคุณ',
      insightMomentsTitle: 'ช่วงเวลาเชิงลึก',
      bestModesTitle: 'โหมดการเดินทางที่เหมาะกับคุณ',
      swipeHint: 'ปัดเพื่อดูเพิ่มเติม',
      shapeIndexLabel: 'ดัชนีรูปแบบ',
      flowFinderCaption: 'อิงจากแบบทดสอบ Flow Finder',
      strongestSignalPrefix: 'สัญญาณที่โดดเด่นที่สุด',
      whenThisFits: 'เหมาะเมื่อ',
      readFullProfile: 'อ่านโปรไฟล์เต็ม',
      hideFullProfile: 'ย่อ',
      insightsEmptyLead: 'เมื่อมีไฮไลต์ที่พร้อมแสดง จะปรากฏที่นี่'
    },
    ar: {
      travelTypeTitle: 'نوع السفر',
      travelDnaSubtitleLine: 'حمض السفر النووي',
      flowShapeTitle: 'شكل Flow الخاص بك',
      insightMomentsTitle: 'لحظات الرؤية',
      bestModesTitle: 'أنماط السفر الأنسب لك',
      swipeHint: 'مرّر لعرض المزيد',
      shapeIndexLabel: 'مؤشر الشكل',
      flowFinderCaption: 'استنادًا إلى ملف Flow Finder',
      strongestSignalPrefix: 'أقوى إشارة',
      whenThisFits: 'عندما يناسبك',
      readFullProfile: 'اقرأ الملف الكامل',
      hideFullProfile: 'إخفاء الملف الكامل',
      insightsEmptyLead: 'ستظهر البطاقات هنا عند توفرها في ملفك.'
    },
    id: {
      travelTypeTitle: 'Tipe perjalanan',
      travelDnaSubtitleLine: 'DNA perjalanan',
      flowShapeTitle: 'Bentuk Flow Anda',
      insightMomentsTitle: 'Momen wawasan',
      bestModesTitle: 'Mode perjalanan yang pas untuk Anda',
      swipeHint: 'Geser untuk jelajahi',
      shapeIndexLabel: 'Indeks bentuk',
      flowFinderCaption: 'Berdasarkan profil Flow Finder Anda',
      strongestSignalPrefix: 'Sinyal terkuat',
      whenThisFits: 'Saat ini cocok untuk Anda',
      readFullProfile: 'Baca profil lengkap',
      hideFullProfile: 'Sembunyikan profil lengkap',
      insightsEmptyLead: 'Kartu sorotan akan muncul di sini jika tersedia.'
    },
    ms: {
      travelTypeTitle: 'Jenis perjalanan',
      travelDnaSubtitleLine: 'DNA perjalanan',
      flowShapeTitle: 'Bentuk Flow anda',
      insightMomentsTitle: 'Detik wawasan',
      bestModesTitle: 'Mod perjalanan yang sesuai untuk anda',
      swipeHint: 'Leret untuk teroka',
      shapeIndexLabel: 'Indeks bentuk',
      flowFinderCaption: 'Berdasarkan profil Flow Finder anda',
      strongestSignalPrefix: 'Isyarat terkuat',
      whenThisFits: 'Bila sesuai untuk anda',
      readFullProfile: 'Baca profil penuh',
      hideFullProfile: 'Sembunyikan profil penuh',
      insightsEmptyLead: 'Kad sorotan akan muncul di sini jika tersedia.'
    },
    vi: {
      travelTypeTitle: 'Kiểu du lịch',
      travelDnaSubtitleLine: 'DNA du lịch',
      flowShapeTitle: 'Hình dạng Flow của bạn',
      insightMomentsTitle: 'Khoảnh khắc thấu hiểu',
      bestModesTitle: 'Chế độ du lịch phù hợp nhất',
      swipeHint: 'Vuốt để xem thêm',
      shapeIndexLabel: 'Chỉ số hình dạng',
      flowFinderCaption: 'Dựa trên hồ sơ Flow Finder của bạn',
      strongestSignalPrefix: 'Tín hiệu mạnh nhất',
      whenThisFits: 'Khi nào phù hợp với bạn',
      readFullProfile: 'Đọc hồ sơ đầy đủ',
      hideFullProfile: 'Thu gọn hồ sơ',
      insightsEmptyLead: 'Thẻ điểm nhấn sẽ hiển thị tại đây khi có trong hồ sơ.'
    },
    fil: {
      travelTypeTitle: 'Uri ng byahe',
      travelDnaSubtitleLine: 'DNA ng biyahe',
      flowShapeTitle: 'Ang hugis ng Flow mo',
      insightMomentsTitle: 'Mga insight na sandali',
      bestModesTitle: 'Pinakangkop na travel mode para sa iyo',
      swipeHint: 'Mag-swipe para tumuklas',
      shapeIndexLabel: 'Shape index',
      flowFinderCaption: 'Batay sa Flow Finder profile mo',
      strongestSignalPrefix: 'Pinakamalakas na senyas',
      whenThisFits: 'Kapag bagay sa iyo',
      readFullProfile: 'Basahin ang buong profile',
      hideFullProfile: 'Itago ang buong profile',
      insightsEmptyLead: 'Lilitaw dito ang mga highlight card kapag available na.'
    }
  };

  /** Interpolate `{name}` placeholders in dashboard copy (SYS / fallback). */
  function interpolateDashboardTemplate(str, vars) {
    if (str == null || typeof str !== 'string') return '';
    let out = str;
    const v = vars || {};
    const keys = Object.keys(v);
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i];
      const val = v[k] != null ? String(v[k]) : '';
      out = out.split(`{${k}}`).join(val);
    }
    return out;
  }

  /**
   * Dashboard copy — **10 locales** (offline fallbacks). KV `ui:powerup_visual_*` overrides via useEffect merge.
   * Do not branch on `zh` in helpers; read strings from `L` only.
   */
  const PU_VISUAL_DASHBOARD_FALLBACK_EN = {
    profileOverviewTitle: 'Profile overview',
    travelTypeTitle: 'Your travel type',
    travelTypeMeaningTitle: 'What this means',
    travelTypePracticalLine:
      'This shapes which trip styles and pacing will feel most satisfying for you — so you can plan for maximum enjoyment.',
    travelDnaSlideTitle: 'Your travel DNA',
    dnaGroupValued: 'What you truly prioritise',
    dnaGroupDraining: 'What tends to wear you down',
    dnaGroupRewarding: 'What makes the trip feel worthwhile',
    dnaEmpty: 'More personalised conclusions appear as your profile narrative fills in.',
    coreStyleLabel: 'Core style',
    interestsLabel: 'Interests',
    practicalNeedsLabel: 'Practical needs',
    strongestSignalPrefix: 'Strongest signal',
    weakestSignalPrefix: 'Weakest signal',
    flowInterpretLead: 'Your rhythm leans on',
    flowInterpretTail: '— ease off that dimension when plans get dense.',
    sylvanSelectionTitle: 'How Sylvan chooses for you',
    svSectionRetain: 'What Sylvan tends to preserve for you',
    svSectionCautious: 'What Sylvan treats carefully',
    svSectionBalance: 'How Sylvan balances your trip',
    svSectionPrioritize: 'What Sylvan tends to preserve for you',
    svSectionDeprioritize: 'What Sylvan treats carefully',
    svSectionMustRespect: 'How Sylvan balances your trip',
    svEmptyPrioritize: 'These picks tune automatically from your Flow Shape and profile story.',
    svEmptyDeprioritize: 'When your profile spells out friction points, they show up here.',
    svEmptyMustRespect: 'Balance rules tighten as your profile adds detail.',
    modeEmphasisLead: 'What you tend to experience',
    modeAdjustRhythmLead: 'How Sylvan adjusts pacing',
    whySlideTitle: 'Why Sylvan reads you this way',
    whyLead: 'Based on your Flow Finder responses, preferences, and travel rhythm.',
    whyCollapseShow: 'Full interpretation',
    whyCollapseHide: 'Hide full interpretation',
    whyTeaserFallback: 'Open the full interpretation to see how your answers and rhythm inform this read.',
    whyChipFlowFinder: 'Flow Finder responses',
    whyChipTags: 'Preference tendencies',
    whyChipRhythm: 'Travel rhythm',
    /** When no narrative prose exists in profile KV / visual_profile */
    whyProfileNarrativeEmpty:
      'Your narrative summary is not on this profile yet. Finish Flow Finder or wait a moment for sync — then your write-up will appear here.',
    travelMeaningWeakLow:
      'You relax into travel when the plan has a clear skeleton — novelty feels better when it is bounded.',
    travelMeaningWeakNorm:
      'Structured exploration with breathing room tends to feel better than constant improvisation.',
    travelMeaningDomHigh:
      'Comfort and meaningful stops weigh more for you than chasing volume.',
    travelMeaningPad0:
      'Calm pacing and a comfortable environment help you actually enjoy the trip.',
    travelMeaningPad1:
      'The goal is fewer rushed pivots and more moments that feel intentional.',
    dnaValuedComfortCulture:
      'You value stops that feel restorative and culturally rich over simply packing more in.',
    dnaDrainingPaceNovelty:
      'Highly improvised days with constant switching can drain you quickly.',
    dnaRewardingNoveltyCulture:
      'Trips feel worthwhile when novelty comes with context and story.',
    dnaPadValued1:
      'What matters most is how the day feels — not how many boxes you check.',
    dnaPadValued2:
      'Steady pacing helps you notice detail and atmosphere.',
    dnaPadDraining1:
      'Noise, chaos, or endless replanning wears you down faster.',
    dnaPadDraining2:
      'Long stretches of hurry shrink how good things feel.',
    dnaPadRewarding1:
      'Moments with texture and memory beat a tally of places.',
    dnaPadRewarding2:
      'Authentic-feeling experiences outweigh symbolic check-ins.',
    sylvanRetainComfortPace:
      'A steady structure and manageable pace through the day.',
    sylvanRetainCultureDepth:
      'Time for depth, story, and places that feel real — not only scenic.',
    sylvanRetainNoRush:
      'Days that do not depend on constant rushing between stops.',
    sylvanCautiousImprov:
      'Highly improvised, noisy, or zig‑zag itineraries.',
    sylvanCautiousPacked:
      'Schedules that are packed edge‑to‑edge with no recovery.',
    sylvanCautiousPivot:
      'Constant last‑minute pivots that keep you in fire‑drill mode.',
    sylvanBalanceSafetyNovelty:
      'Balance safety with fresh moments.',
    sylvanBalanceRestExplore:
      'Balance rest with meaningful exploration.',
    sylvanBalancePlanDiscovery:
      'Balance a planned backbone with room for discovery.',
    sylvanNnLine: 'Honouring boundaries you’ve stated: {nn}',
    tripInterpretBody:
      '{highLabels} skew higher on your profile; {lowLabels} skew lower. Depth with a stable skeleton beats constant last‑minute pivots and rushed days.',
    tripInterpretLabelSep: ', ',
    modeWhenFitFallback: 'This mode fits when your day naturally tilts toward “{mode}” pacing.',
    modeRhythmBody:
      'Keeps shifts within a range your energy and attention can carry.',
    modeExperienceBody:
      'A more continuous feel — less hurry, more reasons to stay present.'
  };

  const PU_VISUAL_DASHBOARD_FALLBACK_ZH = {
    profileOverviewTitle: '档案概览',
    travelTypeTitle: '你的旅行类型',
    travelTypeMeaningTitle: '这代表什么',
    travelTypePracticalLine: '这代表你更适合什么样的旅行安排，从而尽量拉高旅途满意度。',
    travelDnaSlideTitle: '你的旅行 DNA',
    dnaGroupValued: '你真正重视',
    dnaGroupDraining: '容易消耗你的体验',
    dnaGroupRewarding: '让旅程值得的元素',
    dnaEmpty: '档案解读更完整后，这里会有更具体的结论。',
    coreStyleLabel: '核心风格',
    interestsLabel: '兴趣标签',
    practicalNeedsLabel: '实用需求',
    strongestSignalPrefix: '最强信号',
    weakestSignalPrefix: '最弱维度',
    flowInterpretLead: '您的节奏更偏向',
    flowInterpretTail: '——行程密集时请留意该维度。',
    sylvanSelectionTitle: 'Sylvan 会如何帮你选',
    svSectionRetain: 'Sylvan 会优先帮你保留',
    svSectionCautious: 'Sylvan 会谨慎处理',
    svSectionBalance: 'Sylvan 会帮你平衡',
    svSectionPrioritize: 'Sylvan 会优先帮你保留',
    svSectionDeprioritize: 'Sylvan 会谨慎处理',
    svSectionMustRespect: 'Sylvan 会帮你平衡',
    svEmptyPrioritize: '这些取向会从 Flow 形状与解读文字里自动对齐。',
    svEmptyDeprioritize: '档案里若写到容易不适的情境，会反映在这里。',
    svEmptyMustRespect: '行程越长、信息越完整，这里的平衡规则越清晰。',
    modeEmphasisLead: '你会得到什么体验',
    modeAdjustRhythmLead: 'Sylvan 会怎样调整节奏',
    whySlideTitle: '为什么 Sylvan 这样认为',
    whyLead: '基于 Flow Finder 回答、偏好倾向、旅行节奏等综合判断。',
    whyCollapseShow: '完整解读',
    whyCollapseHide: '收起完整解读',
    whyTeaserFallback: '展开完整解读，查看测验回答与节奏如何形成这份判断。',
    whyChipFlowFinder: 'Flow Finder 回答',
    whyChipTags: '偏好倾向',
    whyChipRhythm: '旅行节奏',
    whyProfileNarrativeEmpty:
      '暂无完整文字解读。完成 Flow Finder 测验或稍等同步后，您的全文解读将显示于此。',
    travelMeaningWeakLow:
      '你需要一定的秩序感与预先安排，旅途中的“可控的新鲜”会更让你安心。',
    travelMeaningWeakNorm: '你更适合有计划、有节奏的探索方式，而不是全天无序穿插。',
    travelMeaningDomHigh: '你会更重视旅途中的舒适度与文化感受，而不只是匆匆打卡。',
    travelMeaningPad0: '你更容易在节奏稳定、环境舒适的行程中放松下来。',
    travelMeaningPad1: '理想旅程不必行程越多越好，而是每个停留都能留下清晰感受。',
    dnaValuedComfortCulture:
      '你会更珍惜舒适与文化深度兼备的停留，而不是一味拉长行程表。',
    dnaDrainingPaceNovelty:
      '过度临时、频繁换场或长时间赶路，容易很快消耗你的兴致。',
    dnaRewardingNoveltyCulture:
      '当旅程里既有新鲜感也有故事感，你会更觉得“值得”。',
    dnaPadValued1: '你真正重视的是让自己舒服的节律，而不是堆叠景点数量。',
    dnaPadValued2: '稳定节奏下，你更能欣赏细节与氛围。',
    dnaPadDraining1: '嘈杂、无序或反复改计划的安排，往往让你更快疲惫。',
    dnaPadDraining2: '长时间高压赶路会让体验打折。',
    dnaPadRewarding1: '能留下清晰感受与记忆的片段，会让这趟旅程更值得。',
    dnaPadRewarding2: '故事感与真实体验比“去过”更重要。',
    sylvanRetainComfortPace: '行程中的秩序感与可控节奏。',
    sylvanRetainCultureDepth: '有文化深度或真实体验感的停留与时间。',
    sylvanRetainNoRush: '不需要过度赶路的一天式安排。',
    sylvanCautiousImprov: '过度临时、嘈杂或需要频繁改变方向的安排。',
    sylvanCautiousPacked: '太赶、太满、几乎没有留白或休息段的日程。',
    sylvanCautiousPivot: '频繁推翻既定路线、让你一直处于“临场救火”的状态。',
    sylvanBalanceSafetyNovelty: '在安全感与新鲜感之间取得平衡。',
    sylvanBalanceRestExplore: '在舒适休息与深度探索之间取得平衡。',
    sylvanBalancePlanDiscovery: '在计划好的骨架与临场发现之间取得平衡。',
    sylvanNnLine: '会尊重你在档案里写明的边界：{nn}',
    tripInterpretBody:
      '「{highLabels}」倾向较高；「{lowLabels}」相对较低。适合有计划的深度安排，尽量避免行程临时大改与过度赶路。',
    tripInterpretLabelSep: '、',
    modeWhenFitFallback: '当行程节奏与「{mode}」相符时，这一套会更合拍。',
    modeRhythmBody: '会把变动控制在合理范围，让你的体力与注意力跟得上。',
    modeExperienceBody:
      '整体体验更连贯：少一点赶路感，多一点你愿意停留的理由。'
  };

  /** Offline dashboard copy per locale — non‑Chinese locales use English until KV supplies translation. */
  const PU_VISUAL_DASHBOARD_FALLBACK_BY_LANG = {
    en: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    'zh-CN': { ...PU_VISUAL_DASHBOARD_FALLBACK_EN, ...PU_VISUAL_DASHBOARD_FALLBACK_ZH },
    ja: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    ko: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    th: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    ar: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    id: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    ms: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    vi: PU_VISUAL_DASHBOARD_FALLBACK_EN,
    fil: PU_VISUAL_DASHBOARD_FALLBACK_EN
  };

  function pickDashboardLocaleKey(lower, raw) {
    const r = String(raw || '').trim();
    if (r && PU_VISUAL_DASHBOARD_FALLBACK_BY_LANG[r]) return r;
    if (lower.startsWith('zh-tw') || lower === 'zh-hant') return 'zh-CN';
    if (lower.startsWith('zh')) return 'zh-CN';
    if (lower.startsWith('ja')) return 'ja';
    if (lower.startsWith('ko')) return 'ko';
    if (lower.startsWith('th')) return 'th';
    if (lower.startsWith('ar')) return 'ar';
    if (lower.startsWith('id')) return 'id';
    if (lower.startsWith('ms')) return 'ms';
    if (lower.startsWith('vi')) return 'vi';
    if (lower.startsWith('fil') || lower === 'tl') return 'fil';
    return 'en';
  }

  function resolvePowerUpVisualCarouselExtraFallback(langTag) {
    const raw = String(langTag || 'en').trim();
    const lower = raw.toLowerCase();
    const map = POWERUP_VISUAL_CAROUSEL_EXTRA_FALLBACK_BY_LANG;
    let picked = map[raw];
    if (!picked && (lower.startsWith('zh-tw') || lower === 'zh-hant')) picked = map['zh-CN'];
    if (!picked && lower.startsWith('zh')) picked = map['zh-CN'];
    if (!picked && lower.startsWith('ja')) picked = map.ja;
    if (!picked && lower.startsWith('ko')) picked = map.ko;
    if (!picked && lower.startsWith('th')) picked = map.th;
    if (!picked && lower.startsWith('ar')) picked = map.ar;
    if (!picked && lower.startsWith('id')) picked = map.id;
    if (!picked && lower.startsWith('ms')) picked = map.ms;
    if (!picked && lower.startsWith('vi')) picked = map.vi;
    if (!picked && (lower.startsWith('fil') || lower === 'tl')) picked = map.fil;
    if (!picked) picked = map.en;

    const dashKey = pickDashboardLocaleKey(lower, raw);
    const dash =
      PU_VISUAL_DASHBOARD_FALLBACK_BY_LANG[dashKey] || PU_VISUAL_DASHBOARD_FALLBACK_BY_LANG.en;
    return { ...dash, ...picked };
  }

  /** Keys merged from `POWERUP_VISUAL_CAROUSEL_EXTRA_FALLBACK_BY_LANG` — do not override via `ui:powerup_visual_dashboard_*`. */
  const POWERUP_VISUAL_CAROUSEL_RIM_KEY_SET = new Set(
    Object.keys(POWERUP_VISUAL_CAROUSEL_EXTRA_FALLBACK_BY_LANG.en)
  );

  /** SYS id `ui:powerup_visual_dashboard_<snake>` for offline dashboard bundle keys. */
  function powerupVisualDashboardKeyToSysId(camelKey) {
    const snake = String(camelKey).replace(/[A-Z]/g, (m) => '_' + m.toLowerCase());
    return `ui:powerup_visual_dashboard_${snake}`;
  }

  /** Flow Finder tile tapped after quiz done: fullscreen SYS toast duration (legacy strip +2s). */
  const POWERUP_FF_ALREADY_COMPLETE_OVERLAY_MS = 4800;

  /** Birth-place roster (same as Flow Finder profile gate). */
  const POWERUP_PROFILE_COUNTRIES = [
    'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan',
    'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia',
    'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi',
    'Cambodia', 'Cameroon', 'Canada', 'Cape Verde', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia',
    'Comoros', 'Congo', 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic',
    'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic',
    'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini', 'Ethiopia',
    'Fiji', 'Finland', 'France',
    'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana',
    'Haiti', 'Honduras', 'Hungary',
    'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy',
    'Jamaica', 'Japan', 'Jordan',
    'Kazakhstan', 'Kenya', 'Kiribati', 'Kosovo', 'Kuwait', 'Kyrgyzstan',
    'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg',
    'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius',
    'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar',
    'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea',
    'North Macedonia', 'Norway',
    'Oman',
    'Pakistan', 'Palau', 'Palestine', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal',
    'Qatar',
    'Romania', 'Russia', 'Rwanda',
    'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino',
    'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore',
    'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain',
    'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria',
    'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago',
    'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu',
    'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan',
    'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam',
    'Yemen',
    'Zambia', 'Zimbabwe'
  ];

  const POWERUP_LANG_OPTIONS = [
    { value: 'en', label: 'English' },
    { value: 'zh-CN', label: '中文 (Chinese)' },
    { value: 'ja', label: '日本語 (Japanese)' },
    { value: 'ko', label: '한국어 (Korean)' },
    { value: 'th', label: 'ไทย (Thai)' },
    { value: 'ar', label: 'العربية (Arabic)' },
    { value: 'id', label: 'Bahasa Indonesia' },
    { value: 'ms', label: 'Bahasa Melayu' },
    { value: 'vi', label: 'Tiếng Việt (Vietnamese)' },
    { value: 'fil', label: 'Filipino' }
  ];

  function powerUpProfileLanguageConfirmed(p) {
    if (!p) return false;
    if (p.profile_language_confirmed === true) return true;
    return !!(p.preferred_language && p.onboarding_state === 'lang_selected');
  }

  /** Hub → TV: green Summary · blue Flow Finder · purple Create Flow Plan · orange Ask Sylvan (Power Up only). */
  const POWERUP_HUB_SLOT_SPECS = [
    {
      key: 'profile-summary',
      icon: 'route',
      title: 'Summary',
      sub: 'Profile',
      glow: 'rgba(34,197,94,0.35)',
      ring: 'rgba(52,211,153,0.4)',
      interactive: true
    },
    {
      key: 'flow-finder',
      icon: 'clipboard',
      title: 'Flow Finder',
      sub: 'Quiz',
      glow: 'rgba(56,189,248,0.35)',
      ring: 'rgba(125,211,252,0.4)',
      interactive: true
    },
    {
      key: 'flow-plan',
      icon: 'timeline',
      title: 'Create Flow Plan',
      sub: 'Trip',
      glow: 'rgba(167,139,250,0.35)',
      ring: 'rgba(196,181,253,0.4)',
      interactive: true
    },
    {
      key: 'ask-sylvan',
      icon: 'chat',
      title: 'Ask Sylvan',
      sub: 'Chat',
      glow: 'rgba(248,130,18,0.35)',
      ring: 'rgba(251,162,52,0.4)',
      interactive: true
    }
  ];

  /** Inner TV plate — glass depth only; **no** duplicate white stroke (outer **`recoTopDefault`** rim carries the rainbow gutter). */
  const POWERUP_TV_PLATE_GLOW = 'rgba(148,163,184,0.32)';
  const POWERUP_TV_PLATE_BOX_SHADOW = [
    'inset 0 1px 0 rgba(255,255,255,0.14)',
    'inset 0 14px 32px -10px rgba(0,0,0,0.35)',
    'inset 0 -36px 48px -12px rgba(0,0,0,0.52)',
    `0 16px 44px -16px ${POWERUP_TV_PLATE_GLOW}`,
    `0 0 40px -14px rgba(148,163,184,0.2)`,
    '0 1px 0 rgba(0,0,0,0.45)'
  ].join(', ');

  const POWERUP_TV_PLATE_BASE =
    'relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-[13px] border-0 bg-[#070a0c] backdrop-blur-lg';
  /** Summary / post-plan welcome — flex-1 when inside `rimSummaryGrow` so the TV uses viewport space below the greeting */
  const POWERUP_TV_PLATE_SUMMARY =
    'relative flex max-h-full min-h-0 flex-1 flex-col overflow-hidden rounded-[13px] border-0 bg-[#070a0c] backdrop-blur-lg';

  const POWERUP_TV_BOUNDED_MAX =
    (typeof window !== 'undefined' &&
      window.SYLVAN_TECH_THEME?.classes?.tvRimMaxHeight) ||
    'max-h-[min(82svh,calc(100dvh-9.25rem-env(safe-area-inset-bottom)))]';

  /**
   * TV plate — `rimFill` fills for iframe/embed modes; `rimSummaryGrow` fills summary/post-plan in the scroll column;
   * `rimFitContent` caps height when content-height plates are needed.
   * @see docs/setup/NUASK_RESPONSIVE_CONTRACT.md
   */
  const POWERUP_TV_LAYOUT = {
    rimFill: 'flex min-h-0 flex-1 flex-col',
    /** Summary / welcome TV: height follows content, bounded by viewport — no empty plate below short copy */
    rimFitContent: `flex h-fit w-full ${POWERUP_TV_BOUNDED_MAX} shrink-0 flex-col overflow-hidden`,
    /** Summary / post-plan personality plate: grows into space below welcome (scroll column flex child). */
    rimSummaryGrow: `flex min-h-0 flex-1 flex-col overflow-hidden w-full ${POWERUP_TV_BOUNDED_MAX}`,
    plateEmbed: POWERUP_TV_PLATE_BASE,
    plateSummary: POWERUP_TV_PLATE_SUMMARY,
    postPlanVideoMax:
      'max-h-[min(40svh,280px)] md:max-h-[min(44svh,380px)] lg:max-h-[min(46svh,440px)] w-full object-cover'
  };

  /** TV rim — multi-hue gradient aligned with hub tiles (mock hotbar), not flat teal. */
  const POWERUP_TV_RIM_GRADIENT =
    'linear-gradient(128deg, rgba(34,197,94,0.72) 0%, rgba(45,226,197,0.82) 22%, rgba(56,189,248,0.62) 44%, rgba(139,92,246,0.68) 68%, rgba(248,130,18,0.58) 100%)';

  /** Section label gradient per slide — breaks monotonous flat teal. */
  function visualCarouselSlideTitleClass(slideId) {
    const m = {
      'travel-type':
        'bg-gradient-to-r from-emerald-300 via-teal-300 to-cyan-300 bg-clip-text text-transparent',
      'travel-dna':
        'bg-gradient-to-r from-teal-300 via-cyan-300 to-sky-400 bg-clip-text text-transparent',
      'flow-shape':
        'bg-gradient-to-r from-sky-400 via-violet-300 to-fuchsia-400 bg-clip-text text-transparent',
      'sylvan-selection-logic':
        'bg-gradient-to-r from-violet-300 via-fuchsia-400 to-pink-400 bg-clip-text text-transparent',
      'best-modes':
        'bg-gradient-to-r from-emerald-300 via-teal-400 to-cyan-300 bg-clip-text text-transparent',
      'why-profile':
        'bg-gradient-to-r from-violet-300 via-teal-300 to-emerald-300 bg-clip-text text-transparent'
    };
    return (
      m[slideId] ||
      'bg-gradient-to-r from-teal-300 via-cyan-300 to-teal-200 bg-clip-text text-transparent'
    );
  }

  /** Default hero when archetype does not match a family or mapped PNG fails to load (repo: `pages/assets/powerup-nature-hero.png`). */
  const POWERUP_SHARED_NATURE_HERO = '/assets/powerup-nature-hero.png';

  /** Shipped hero PNG basenames under `pages/assets/` — resolver never returns other names (avoids 404s). */
  const POWERUP_HERO_ASSET_ALLOWLIST = new Set([
    'powerup-hero-grounded.png',
    'powerup-hero-explorer.png',
    'powerup-hero-urban.png',
    'powerup-hero-culture.png',
    'powerup-hero-planner.png',
    'powerup-hero-sensory.png',
    'powerup-hero-restorative.png',
    'powerup-hero-adventurous.png'
  ]);

  /** Curated “moment” art for carousel slides / travel-type cards (not the TV chrome). Archetype-independent; no tags. */
  const POWERUP_MOMENT_ASSET_ALLOWLIST = new Set([
    'powerup-moment-dna-deck.png',
    'powerup-moment-explorer-summit.png',
    'powerup-moment-urban-rooftop.png',
    'powerup-moment-culture-temple.png',
    'powerup-moment-planner-map.png',
    'powerup-moment-sensory-dining.png',
    'powerup-moment-quiet-lake.png',
    'powerup-moment-journey-drive.png'
  ]);

  const POWERUP_MOMENT_SLIDE_PRIMARY = {
    'travel-dna': 'powerup-moment-dna-deck.png',
    'flow-shape': 'powerup-moment-planner-map.png',
    'sylvan-selection-logic': 'powerup-moment-culture-temple.png',
    'best-modes': 'powerup-moment-urban-rooftop.png',
    'why-profile': 'powerup-moment-quiet-lake.png'
  };

  /** Faint motion layer under the archetype family photo on the travel-type slide only. */
  const POWERUP_MOMENT_TRAVEL_TYPE_UNDERLAY = 'powerup-moment-journey-drive.png';
  const POWERUP_MOMENT_TRAVEL_TYPE_INNER_ARCHETYPE = 'powerup-moment-explorer-summit.png';
  const POWERUP_MOMENT_TRAVEL_TYPE_INNER_MEANING = 'powerup-moment-sensory-dining.png';

  /**
   * Archetype text → shipped hero PNG. Multiple logical families may share one file until dedicated art exists.
   * Order matters: more specific patterns before broad ones. Selection uses **`visual_profile.archetype` text only** (no tags, POI, or quiz vocabulary).
   */
  const POWERUP_ARCHETYPE_HERO_RULES = [
    {
      re: /planner|structured|methodical|order|route|计划|规划|秩序|细节|有序|路线/i,
      file: 'powerup-hero-planner.png'
    },
    {
      re: /culture|heritage|history|museum|depth|文化|历史|深度|古迹|博物/i,
      file: 'powerup-hero-culture.png'
    },
    {
      re: /nightlife|evening|vibrant|夜生活|热闹|活跃|夜晚|夜间|夜景|夜店|派对|夜色|夜幕/i,
      file: 'powerup-hero-urban.png'
    },
    {
      re: /urban|city|metro|skyline|都市|城市|都会|城心/i,
      file: 'powerup-hero-urban.png'
    },
    {
      re: /local|neighbourhood|neighborhood|community|social|people|人文|本地|街区|市井|社区|社群|邻家/i,
      file: 'powerup-hero-culture.png'
    },
    {
      re: /romantic|refined|premium|elegant|boutique|luxury|浪漫|精致|高级|优雅|奢华|品质/i,
      file: 'powerup-hero-sensory.png'
    },
    {
      re: /food|sensory|taste|cafe|café|dining|餐|咖啡|美食|感官|味觉|品味|小吃|料理/i,
      file: 'powerup-hero-sensory.png'
    },
    {
      re: /restorative|healing|slow|chill|quiet|疗愈|慢|安静|宁静|平静|修心/i,
      file: 'powerup-hero-restorative.png'
    },
    {
      re: /comfort|comfortable|relaxed|safe|舒适|安心|放松|安全感/i,
      file: 'powerup-hero-restorative.png'
    },
    {
      re: /waterfront|scenic|\blake\b|\briver\b|水岸|湖滨|湖畔|海边|河岸|湖景|海景|河景|水景|风景|山水|湖光|自然风光/i,
      file: 'powerup-hero-restorative.png'
    },
    {
      re: /family|gentle|easy|kid|亲子|家庭|温和|轻松|带娃/i,
      file: 'powerup-hero-grounded.png'
    },
    {
      re: /grounded|steady|stable|reliable|沉稳|稳重|踏实|可靠/i,
      file: 'powerup-hero-grounded.png'
    },
    {
      re: /photography|memory|moments|lens|摄影|回忆|画面感|记录|快门/i,
      file: 'powerup-hero-explorer.png'
    },
    {
      re: /flexible|spontaneous|free-flowing|adaptive|灵活|随性|自由|变化/i,
      file: 'powerup-hero-adventurous.png'
    },
    {
      re: /adventur|bold|daring|energetic|energy|冒险|大胆|能量|活力/i,
      file: 'powerup-hero-adventurous.png'
    },
    {
      re: /explorer|discovery|discoverer|curious|探索|探者|发现|好奇/i,
      file: 'powerup-hero-explorer.png'
    }
  ];

  function resolvePowerUpArchetypeHeroUrl(archetype) {
    const t = String(archetype || '').trim();
    if (!t) return POWERUP_SHARED_NATURE_HERO;
    for (let i = 0; i < POWERUP_ARCHETYPE_HERO_RULES.length; i++) {
      const rule = POWERUP_ARCHETYPE_HERO_RULES[i];
      if (rule.re.test(t) && POWERUP_HERO_ASSET_ALLOWLIST.has(rule.file)) {
        return `/assets/${rule.file}`;
      }
    }
    return POWERUP_SHARED_NATURE_HERO;
  }

  function resolvePowerUpMomentAssetUrl(basename) {
    const b = String(basename || '').trim();
    if (!b || !POWERUP_MOMENT_ASSET_ALLOWLIST.has(b)) return POWERUP_SHARED_NATURE_HERO;
    return `/assets/${b}`;
  }

  /**
   * Carousel slide backdrop: travel-type uses archetype-mapped family hero; other slides use fixed moment art.
   * Moment filenames are editorial (per slide container), not inferred from tags or POI.
   */
  function resolvePowerUpSlideBackdropPrimary(slideId, archetype) {
    if (slideId === 'travel-type') return resolvePowerUpArchetypeHeroUrl(archetype);
    const file = POWERUP_MOMENT_SLIDE_PRIMARY[slideId];
    return resolvePowerUpMomentAssetUrl(file);
  }

  function powerupBackdropImgOnError(e) {
    const el = e.currentTarget;
    const fb = POWERUP_SHARED_NATURE_HERO;
    if (el.dataset.puHeroFb === '1') {
      el.style.display = 'none';
      return;
    }
    if (el.currentSrc && !String(el.currentSrc).includes('powerup-nature-hero.png')) {
      el.dataset.puHeroFb = '1';
      el.src = fb;
    } else {
      el.style.display = 'none';
    }
  }

  /**
   * Photo backdrop for visual_profile carousel slides inside the Summary TV rim (not iframe chrome).
   * @param {'hero'|'soft'|'chart'|'logic'|'modes'|'prose'} variant
   * @param {string} [archetype] — from `visual_profile.archetype` only (travel-type slide)
   * @param {string} slideId — selects per-slide moment art when not travel-type
   */
  function renderPowerUpSlideImageBackdrop(variant, archetype, slideId) {
    /** Photo `op` + scrim alphas — travel-type uses `hero`; other slides use softer scrims + higher `op` so moment art reads through a bit more. */
    const presets = {
      hero: { op: 0.38, scrim: 'from-[#030806]/68 via-[#060908]/84 to-[#070a0c]/92', radial: true },
      soft: { op: 0.21, scrim: 'from-[#030808]/78 via-[#070a0c]/86 to-[#070a0c]/90', radial: false },
      chart: { op: 0.16, scrim: 'from-[#030806]/82 via-[#070a0c]/88 to-[#05070a]/92', radial: true },
      logic: { op: 0.18, scrim: 'from-[#030806]/78 via-[#070a0c]/86 to-[#030806]/84', radial: false },
      modes: { op: 0.21, scrim: 'from-[#030806]/76 via-[#060908]/86 to-[#070a0c]/90', radial: false },
      prose: { op: 0.12, scrim: 'from-[#030806]/86 via-[#070a0c]/92 to-[#070a0c]/94', radial: false }
    };
    const cfg = presets[variant] || presets.soft;
    const primarySrc = resolvePowerUpSlideBackdropPrimary(slideId, archetype);
    const underlaySrc =
      slideId === 'travel-type' && POWERUP_MOMENT_ASSET_ALLOWLIST.has(POWERUP_MOMENT_TRAVEL_TYPE_UNDERLAY)
        ? resolvePowerUpMomentAssetUrl(POWERUP_MOMENT_TRAVEL_TYPE_UNDERLAY)
        : null;
    return (
      <>
        <div className="pointer-events-none absolute inset-0 z-[1] overflow-hidden" aria-hidden>
          {underlaySrc ? (
            <img
              src={underlaySrc}
              alt=""
              decoding="async"
              className="absolute inset-0 z-0 h-full w-full object-cover"
              style={{ opacity: Math.min(0.16, cfg.op * 0.42) }}
              onError={powerupBackdropImgOnError}
            />
          ) : null}
          <img
            src={primarySrc}
            alt=""
            decoding="async"
            className="absolute inset-0 z-[1] h-full w-full object-cover"
            style={{ opacity: cfg.op }}
            onError={powerupBackdropImgOnError}
          />
          <div
            className={`absolute inset-0 z-[2] bg-gradient-to-b ${cfg.scrim}`}
            aria-hidden
          />
          {cfg.radial ? (
            <div
              className="pointer-events-none absolute inset-0 z-[3] bg-[radial-gradient(ellipse_90%_70%_at_50%_0%,rgba(45,226,197,0.07),transparent_55%)]"
              aria-hidden
            />
          ) : null}
        </div>
      </>
    );
  }

  function powerupVcBackdropVariantForSlide(slideId) {
    const m = {
      'travel-type': 'hero',
      'travel-dna': 'soft',
      'flow-shape': 'chart',
      'sylvan-selection-logic': 'logic',
      'best-modes': 'modes',
      'why-profile': 'prose'
    };
    return m[slideId] || 'soft';
  }

  function powerupVcSlideAmbientClass(vcIdx) {
    const i = Number(vcIdx) % 4;
    const layers = [
      'bg-[radial-gradient(ellipse_100%_80%_at_50%_-15%,rgba(139,92,246,0.15),transparent_52%),radial-gradient(ellipse_70%_50%_at_100%_100%,rgba(45,226,197,0.09),transparent_50%)]',
      'bg-[radial-gradient(ellipse_100%_80%_at_50%_-15%,rgba(34,197,94,0.13),transparent_52%),radial-gradient(ellipse_70%_50%_at_0%_100%,rgba(56,189,248,0.11),transparent_50%)]',
      'bg-[radial-gradient(ellipse_100%_80%_at_50%_-15%,rgba(248,130,18,0.12),transparent_52%),radial-gradient(ellipse_70%_50%_at_100%_80%,rgba(167,139,250,0.1),transparent_50%)]',
      'bg-[radial-gradient(ellipse_100%_80%_at_50%_-15%,rgba(56,189,248,0.14),transparent_52%),radial-gradient(ellipse_70%_50%_at_0%_80%,rgba(244,114,182,0.08),transparent_50%)]'
    ];
    return layers[i];
  }

  function PowerUpMockHotbarIcon({ spec }) {
    const k = spec && spec.icon;
    const sw = 1.62;
    const cnBase = 'h-[18px] w-[18px] shrink-0 ';
    const cn =
      k === 'route' || k === 'plus'
        ? cnBase + 'text-emerald-200'
        : k === 'clipboard'
          ? cnBase + 'text-sky-200'
          : k === 'timeline'
            ? cnBase + 'text-violet-200'
            : k === 'chat'
              ? cnBase + 'text-amber-200'
              : cnBase + 'text-[#2DE2C5]/90';
    if (k === 'route') {
      return (
        <svg className={cn} viewBox="0 0 24 24" fill="none" aria-hidden>
          <rect x="5.5" y="4.75" width="13" height="14.5" rx="2.25" stroke="currentColor" strokeWidth={sw} />
          <circle cx="12" cy="9.4" r="2.35" fill="none" stroke="currentColor" strokeWidth={sw} />
          <path
            d="M8.2 17.1c0-2.1 1.7-3.45 3.8-3.45s3.8 1.35 3.8 3.45"
            stroke="currentColor"
            strokeWidth={sw}
            strokeLinecap="round"
          />
          <path
            d="M8.35 12.1h7.3M8.35 13.55h4.5"
            stroke="currentColor"
            strokeWidth={sw * 0.75}
            strokeLinecap="round"
            opacity={0.58}
          />
        </svg>
      );
    }
    if (k === 'plus') {
      return (
        <svg className={cn} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} aria-hidden>
          <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
        </svg>
      );
    }
    if (k === 'clipboard') {
      return (
        <svg className={cn} viewBox="0 0 24 24" fill="none" aria-hidden>
          <path
            d="M9.25 4.75h5.5a1.25 1.25 0 011.25 1.25V6.5H8V6a1.25 1.25 0 011.25-1.25z"
            stroke="currentColor"
            strokeWidth={sw}
            strokeLinejoin="round"
          />
          <path
            d="M8.25 6.5h7.5a1.75 1.75 0 011.75 1.75v9.5a2 2 0 01-2 2H8.5a2 2 0 01-2-2V8.25a1.75 1.75 0 011.75-1.75z"
            stroke="currentColor"
            strokeWidth={sw}
            strokeLinejoin="round"
          />
          <path
            d="M9.65 11.35l1.25 1.25 2.55-2.55M9.65 15.1l1.3 1.3 2.45-2.45"
            stroke="currentColor"
            strokeWidth={sw}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      );
    }
    if (k === 'timeline') {
      return (
        <svg className={cn} viewBox="0 0 24 24" fill="none" aria-hidden>
          <path
            d="M8.35 17.35L10.85 14.1l2.9 1.85 2.85-3.15 2.55 1.7L21 13.75"
            stroke="currentColor"
            strokeWidth={sw}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
          <circle cx="8.35" cy="17.35" r="1.38" fill="none" stroke="currentColor" strokeWidth={sw * 0.9} />
          <circle cx="21" cy="13.75" r="1.38" fill="none" stroke="currentColor" strokeWidth={sw * 0.9} />
          <path
            d="M21 12.37V7.35a1.05 1.05 0 011.05-1.05h0"
            stroke="currentColor"
            strokeWidth={sw * 0.78}
            strokeLinecap="round"
            opacity={0.58}
          />
        </svg>
      );
    }
    if (k === 'chat') {
      return (
        <svg className={cn} viewBox="0 0 24 24" fill="none" aria-hidden>
          <path
            d="M7.1 7.85h9.8a1.95 1.95 0 011.95 1.95v4.35a1.95 1.95 0 01-1.95 1.95h-4.95L8.05 18.35v-2.25H7.1a1.95 1.95 0 01-1.95-1.95V9.8a1.95 1.95 0 011.95-1.95z"
            stroke="currentColor"
            strokeWidth={sw}
            strokeLinejoin="round"
          />
          <path
            d="M9.35 11.65h5.3M9.35 13.55h3.4"
            stroke="currentColor"
            strokeWidth={sw * 0.78}
            strokeLinecap="round"
            opacity={0.58}
          />
        </svg>
      );
    }
    return null;
  }

  /** One horizontal row (`grid-cols-4`) — premium glass hub cards; labels from `hubSlotCopy` / SYS. */
  function PowerUpHubTileRow({ onHubSlotPress, hubSlotCopy }) {
    return (
      <div className="mx-auto grid w-full max-w-[440px] grid-cols-4 gap-1 min-w-0 [touch-action:manipulation]">
        {POWERUP_HUB_SLOT_SPECS.map((spec, hubIdx) => {
          const interactive = spec.interactive && typeof onHubSlotPress === 'function';
          const tileTitle = (hubSlotCopy && hubSlotCopy[spec.key]?.title) || spec.title;
          const tileSub = (hubSlotCopy && hubSlotCopy[spec.key]?.sub) || spec.sub;
          return (
            <button
              key={spec.key}
              type="button"
              tabIndex={interactive ? 0 : -1}
              aria-disabled={!interactive}
              aria-label={`${tileTitle}. ${tileSub}`}
              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 ${
                interactive
                  ? 'motion-safe:active:scale-[0.97] motion-safe:hover:brightness-110'
                  : 'opacity-80'
              }`}
              style={{
                background: [
                  `radial-gradient(96% 52% at 50% 108%, ${spec.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 ${spec.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 ${spec.glow}`,
                  '0 1px 0 rgba(0,0,0,0.42)'
                ].join(', ')
              }}
              onClick={(e) => {
                e.preventDefault();
                if (interactive) onHubSlotPress(spec);
              }}
            >
              <div
                className="pointer-events-none absolute inset-x-0 bottom-0 h-[42%] opacity-[0.28]"
                style={{
                  background: `linear-gradient(to top, ${spec.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, ${spec.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%, ${spec.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: spec.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 ${spec.ring}, 0 0 28px -8px ${spec.glow}`
                  }}
                >
                  <div
                    className="pointer-events-none absolute inset-0 rounded-full opacity-[0.82]"
                    style={{
                      background: `radial-gradient(circle at 50% 24%, ${spec.glow}, transparent 54%)`
                    }}
                    aria-hidden
                  />
                  <span className="relative z-[1] flex items-center justify-center">
                    <PowerUpMockHotbarIcon spec={spec} />
                  </span>
                </div>
              </div>
              <p className="relative z-[1] w-full min-w-0 truncate px-0.5 text-center text-[9px] font-semibold leading-tight tracking-tight text-white/95">
                {tileTitle}
              </p>
              <p className="relative z-[1] mt-0.5 w-full min-w-0 truncate px-0.5 text-[7px] leading-snug text-zinc-400">
                {tileSub}
              </p>
            </button>
          );
        })}
      </div>
    );
  }

  /** Full-screen dim when user taps Flow Finder after quiz is already done — SYS copy, auto-dismiss. */
  function PowerUpFlowFinderCompleteOverlay({ message }) {
    if (!message) return null;
    return (
      <div
        className="fixed inset-0 z-[265] flex items-center justify-center bg-black/78 px-5 backdrop-blur-[2px]"
        role="dialog"
        aria-modal="true"
        aria-live="polite"
      >
        <div className="max-w-[min(92vw,22rem)] rounded-2xl border border-[#2DE2C5]/40 bg-[#0c1018]/98 px-5 py-7 text-center shadow-[0_0_56px_-12px_rgba(45,226,197,0.5)]">
          <p className="text-base font-semibold leading-relaxed text-teal-50 sm:text-lg">{message}</p>
        </div>
      </div>
    );
  }

  /** Full-screen SYS copy when Create Flow Plan is blocked (active or upcoming itinerary). */
  function PowerUpFlowPlanBlockedOverlay({ message, onDismiss }) {
    if (!message) return null;
    return (
      <div
        className="fixed inset-0 z-[265] flex items-center justify-center bg-black/78 px-5 backdrop-blur-[2px]"
        role="dialog"
        aria-modal="true"
        aria-live="polite"
      >
        <div className="max-w-[min(92vw,22rem)] rounded-2xl border border-violet-400/35 bg-[#0c1018]/98 px-5 py-7 text-center shadow-[0_0_56px_-12px_rgba(167,139,250,0.45)]">
          <p className="text-base font-semibold leading-relaxed text-slate-100 sm:text-lg">{message}</p>
          <button
            type="button"
            className="mt-6 w-full min-h-[44px] rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 px-4 py-2.5 text-sm font-semibold text-white shadow-lg active:brightness-95"
            onClick={onDismiss}
          >
            OK
          </button>
        </div>
      </div>
    );
  }

  /** Full-screen branded intro — SylvanFlow logo, ~2.5s hold + 0.5s fade (3s total). */
  function PowerUpIntroOverlay({ fading }) {
    return (
      <div
        className={`fixed inset-0 z-[100] flex flex-col items-center justify-center overflow-hidden bg-[#020508] transition-opacity duration-500 ease-out ${
          fading ? 'pointer-events-none opacity-0' : 'opacity-100'
        }`}
        role="presentation"
        aria-hidden={fading}
      >
        <style>{`
          @keyframes sf-powerup-intro-grid-drift {
            0% { background-position: 0 0; }
            100% { background-position: 48px 48px; }
          }
          @keyframes sf-powerup-intro-scan {
            0% { transform: translateY(-120%); opacity: 0.2; }
            45% { opacity: 0.65; }
            100% { transform: translateY(120vh); opacity: 0.15; }
          }
        `}</style>
        <div
          className="pointer-events-none absolute inset-0 opacity-[0.15]"
          style={{
            backgroundImage:
              'linear-gradient(rgba(45,226,197,0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(45,226,197,0.08) 1px, transparent 1px)',
            backgroundSize: '48px 48px',
            animation: 'sf-powerup-intro-grid-drift 22s linear infinite'
          }}
          aria-hidden
        />
        <div
          className="pointer-events-none absolute inset-0"
          style={{
            background: 'radial-gradient(ellipse 85% 60% at 50% 40%, rgba(45,226,197,0.16), transparent 65%)'
          }}
          aria-hidden
        />
        <div
          className="pointer-events-none absolute inset-0 overflow-hidden"
          aria-hidden
        >
          <div
            className="absolute left-0 right-0 h-[32%] bg-gradient-to-b from-[#2DE2C5]/20 via-[#2DE2C5]/10 to-transparent"
            style={{ animation: 'sf-powerup-intro-scan 2.6s ease-in-out infinite' }}
          />
        </div>
        <div className="relative z-[1] flex flex-col items-center px-6">
          <div
            className="rounded-[22px] border border-[#2DE2C5]/40 bg-black/50 px-8 py-7 backdrop-blur-md"
            style={{ animation: 'nuask-edge-refract-ring 6.4s ease-in-out infinite' }}
          >
            <img
              src="/assets/sylvanflow-logo.png"
              alt=""
              className="mx-auto h-24 w-auto max-w-[min(280px,72vw)] object-contain sm:h-32"
              style={{
                filter:
                  'drop-shadow(0 0 26px rgba(45,226,197,0.5)) drop-shadow(0 0 56px rgba(34,211,238,0.22))'
              }}
            />
          </div>
          <p className="mt-8 font-mono text-[9px] uppercase tracking-[0.42em] text-[#2DE2C5]/80">Initializing SylvanFlow</p>
          <div className="mt-4 flex gap-1.5" aria-hidden>
            {[0, 1, 2, 3, 4].map((i) => (
              <span
                key={`pu-intro-dot-${i}`}
                className="h-1 w-4 rounded-full bg-gradient-to-r from-[#2DE2C5]/25 to-[#2DE2C5]/90"
                style={{
                  animation: 'nuask-fray-pulse 1.05s ease-in-out infinite',
                  animationDelay: `${i * 0.1}s`
                }}
              />
            ))}
          </div>
        </div>
      </div>
    );
  }

  /** Top chrome: menu · fluid title · profile orb OR spotlight Skip (same slot — avoids overlap with fixed overlays). */
  function PowerUpMatchedHeader({
    displayName,
    profilePhotoUrl,
    headerTitle,
    headerSubtitle,
    menuButtonRef,
    tourActive,
    onTourDismiss
  }) {
    const openDrawer = () => {
      window.dispatchEvent(new CustomEvent('sylvanflow-open-drawer'));
    };
    const goProfile = () => {
      if (window.appState && typeof window.appState.set === 'function') {
        window.appState.set({ view: 'power-up' });
      }
    };
    const initial = (displayName || 'U').trim().charAt(0).toUpperCase() || 'U';
    return (
      <header
        className={`sf-power-up-header-edge-pulse relative flex-shrink-0 border-b border-white/[0.06] px-2 pb-2 pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-4 ${
          tourActive ? 'z-[275]' : 'z-[2]'
        }`}
      >
        <div
          className={
            tourActive && typeof onTourDismiss === 'function'
              ? 'grid grid-cols-[2.5rem_minmax(0,1fr)_auto] items-center gap-x-2'
              : 'grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2'
          }
        >
          <button
            ref={menuButtonRef}
            type="button"
            onClick={openDrawer}
            className="flex h-10 w-10 shrink-0 items-center justify-center justify-self-start rounded-lg active:bg-white/10"
            aria-label="Menu"
          >
            <img
              src={
                (typeof window !== 'undefined' &&
                  window.SF_TAB_GLYPH_SRC &&
                  window.SF_TAB_GLYPH_SRC['flow-profile']) ||
                '/assets/nav-profile-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 justify-self-center px-0.5 text-center">
            <h1
              className="font-['Space_Grotesk',system-ui,sans-serif] font-bold uppercase leading-tight tracking-[0.06em] text-[#2DE2C5] sm:tracking-[0.08em]"
              style={{ fontSize: 'clamp(0.95rem, 4.2vw, 1.375rem)' }}
            >
              {headerTitle}
            </h1>
            {headerSubtitle && String(headerSubtitle).trim() ? (
              <p
                className="mt-0.5 font-normal leading-snug text-[#9CA3AF] sm:mt-1"
                style={{ fontSize: 'clamp(0.7rem, 3.1vw, 0.8125rem)' }}
              >
                {headerSubtitle}
              </p>
            ) : null}
          </div>
          {tourActive && typeof onTourDismiss === 'function' ? (
            <button
              type="button"
              onClick={onTourDismiss}
              className="max-w-[5.25rem] shrink-0 justify-self-end rounded-lg border border-white/25 bg-[#0c1018]/98 px-2 py-2 text-[10px] font-semibold leading-tight text-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.45)] sm:max-w-none sm:px-3 sm:text-xs"
              aria-label="Skip tutorial"
            >
              Skip
            </button>
          ) : (
            <button
              type="button"
              onClick={goProfile}
              className="flex h-10 w-10 shrink-0 items-center justify-center justify-self-end overflow-hidden rounded-full ring-2 ring-[#2DE2C5] ring-offset-2 ring-offset-black"
              style={{
                backgroundColor: profilePhotoUrl ? '#0a0a0a' : '#2DE2C5',
                boxShadow: '0 0 22px rgba(45,226,197,0.35)'
              }}
              aria-label={displayName || 'Profile'}
            >
              {profilePhotoUrl ? (
                <img
                  src={profilePhotoUrl}
                  alt=""
                  className="h-full w-full object-cover"
                  onError={(e) => {
                    e.target.style.display = 'none';
                    const par = e.target.parentElement;
                    if (par) par.style.backgroundColor = '#2DE2C5';
                    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' }}
              >
                {initial}
              </span>
            </button>
          )}
        </div>
      </header>
    );
  }

  /** Mirrors Flow Pulse validateVisualProfile — gates carousel vs legacy summary HTML. */
  function isValidVisualProfileForPowerUp(vp) {
    if (!vp || typeof vp !== 'object') return false;
    if (typeof vp.archetype !== 'string' || !vp.archetype.trim()) return false;
    if (typeof vp.one_liner !== 'string' || !vp.one_liner.trim()) return false;
    const s = vp.scores;
    if (!s || typeof s !== 'object') return false;
    const keys = ['pace', 'novelty', 'comfort', 'culture_depth', 'flexibility'];
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i];
      if (!(k in s)) return false;
      const v = s[k];
      const n = typeof v === 'number' ? v : parseInt(String(v), 10);
      if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0 || n > 100) return false;
    }
    if (!Array.isArray(vp.chips) || !Array.isArray(vp.insights) || !Array.isArray(vp.modes)) return false;
    return true;
  }

  /** Highest-scoring dimension among the five Flow Shape scores (stable tie-break: key order). */
  function getDominantScoreKey(scores) {
    const keys = ['pace', 'novelty', 'comfort', 'culture_depth', 'flexibility'];
    let bestKey = keys[0];
    let bestN = -1;
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i];
      const raw = scores && scores[k];
      const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10) || 0;
      if (n > bestN) {
        bestN = n;
        bestKey = k;
      }
    }
    return bestKey;
  }

  /** Lowest-scoring dimension (stable tie-break: first min in key order). */
  function getWeakestScoreKey(scores) {
    const keys = ['pace', 'novelty', 'comfort', 'culture_depth', 'flexibility'];
    let weakKey = keys[0];
    let weakN = 101;
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i];
      const raw = scores && scores[k];
      const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10) || 0;
      if (n < weakN) {
        weakN = n;
        weakKey = k;
      }
    }
    return weakKey;
  }

  const VISUAL_SCORE_LABELS = {
    pace: 'Pace',
    novelty: 'Novelty',
    comfort: 'Comfort',
    culture_depth: 'Culture',
    flexibility: 'Flexibility'
  };

  /** Pentagon chart score bars — teal / identity palette. */
  function getVisualScoreBarClass(scoreKey) {
    const m = {
      pace:
        'bg-gradient-to-r from-sky-300 via-cyan-400 to-sky-400 shadow-[0_0_18px_-2px_rgba(56,189,248,0.55)] ring-1 ring-sky-400/35',
      novelty:
        'bg-gradient-to-r from-blue-400 via-indigo-400 to-sky-400 shadow-[0_0_18px_-2px_rgba(96,165,250,0.52)] ring-1 ring-blue-400/35',
      comfort:
        'bg-gradient-to-r from-emerald-400 via-green-400 to-teal-400 shadow-[0_0_18px_-2px_rgba(52,211,153,0.55)] ring-1 ring-emerald-400/35',
      culture_depth:
        'bg-gradient-to-r from-violet-400 via-purple-500 to-fuchsia-500 shadow-[0_0_18px_-2px_rgba(167,139,250,0.55)] ring-1 ring-violet-400/35',
      flexibility:
        'bg-gradient-to-r from-amber-400 via-orange-400 to-teal-400 shadow-[0_0_18px_-2px_rgba(251,191,36,0.48)] ring-1 ring-amber-400/35'
    };
    return m[scoreKey] || m.flexibility;
  }

  function getModeBarAndDotClasses(modeLabel) {
    const t = String(modeLabel || '').toLowerCase();
    if (/\bcomfort\b|ease|relax|calm|stable|cozy|gentle/.test(t)) {
      return {
        bar: 'bg-gradient-to-r from-emerald-400 via-teal-400 to-green-400 shadow-[0_0_22px_-3px_rgba(52,211,153,0.58)] ring-1 ring-emerald-400/35',
        dot: 'bg-emerald-400 shadow-[0_0_12px_3px_rgba(52,211,153,0.65)] ring-2 ring-emerald-300/40'
      };
    }
    if (/discovery|explore|novel|adventure|curious|new/.test(t)) {
      return {
        bar: 'bg-gradient-to-r from-sky-400 via-cyan-400 to-blue-400 shadow-[0_0_22px_-3px_rgba(56,189,248,0.58)] ring-1 ring-sky-400/35',
        dot: 'bg-sky-400 shadow-[0_0_12px_3px_rgba(56,189,248,0.65)] ring-2 ring-sky-300/40'
      };
    }
    if (/culture|deep culture|heritage|authentic|museum|depth/.test(t)) {
      return {
        bar: 'bg-gradient-to-r from-violet-400 via-purple-500 to-fuchsia-600 shadow-[0_0_22px_-3px_rgba(167,139,250,0.58)] ring-1 ring-violet-400/35',
        dot: 'bg-violet-400 shadow-[0_0_12px_3px_rgba(167,139,250,0.65)] ring-2 ring-violet-300/40'
      };
    }
    return {
      bar: 'bg-gradient-to-r from-teal-400 via-cyan-400 to-emerald-400 shadow-[0_0_22px_-3px_rgba(45,226,197,0.52)] ring-1 ring-teal-400/35',
      dot: 'bg-teal-400 shadow-[0_0_12px_3px_rgba(45,226,197,0.55)] ring-2 ring-teal-300/40'
    };
  }

  /**
   * Compact pentagon radar for Flow Shape — `visual_profile.scores` only.
   * Labels + values sit at each vertex (no separate legend row) so the slide stays shorter.
   */
  function PowerUpVisualFlowRadar({ scores, labelForKey }) {
    const keys = ['pace', 'novelty', 'comfort', 'culture_depth', 'flexibility'];
    const cx = 82;
    const cy = 78;
    const R = 36;
    const labelR = R + 22;
    const verts = keys.map((_, i) => {
      const ang = -Math.PI / 2 + (i * 2 * Math.PI) / 5;
      return [cx + R * Math.cos(ang), cy + R * Math.sin(ang)];
    });
    const innerPts = keys.map((k, i) => {
      const ang = -Math.PI / 2 + (i * 2 * Math.PI) / 5;
      const raw = scores && scores[k];
      const v = typeof raw === 'number' ? raw : parseInt(String(raw), 10) || 0;
      const rr = (Math.max(0, Math.min(100, v)) / 100) * R;
      return [cx + rr * Math.cos(ang), cy + rr * Math.sin(ang)];
    });
    const pathD =
      'M ' + innerPts.map((p) => p[0].toFixed(2) + ',' + p[1].toFixed(2)).join(' L ') + ' Z';
    return (
      <div className="flex min-w-0 flex-col items-center justify-center">
        <svg
          viewBox="0 0 164 156"
          className="mx-auto h-[min(118px,28vw)] w-full max-w-[200px] shrink-0"
          aria-hidden
        >
          {keys.map((_, i) => (
            <line
              key={`gl-${i}`}
              x1={cx}
              y1={cy}
              x2={verts[i][0]}
              y2={verts[i][1]}
              stroke="rgba(45,226,197,0.22)"
              strokeWidth={1}
            />
          ))}
          <polygon
            points={verts.map((p) => p[0] + ',' + p[1]).join(' ')}
            fill="none"
            stroke="rgba(45,226,197,0.38)"
            strokeWidth={1.25}
          />
          <path
            d={pathD}
            fill="rgba(45,226,197,0.22)"
            stroke="#2DE2C5"
            strokeWidth={2}
            strokeLinejoin="round"
            style={{ filter: 'drop-shadow(0 0 14px rgba(45,226,197,0.45))' }}
          />
          <circle cx={cx} cy={cy} r={3.5} fill="#2DE2C5" opacity={0.95} />
          {keys.map((k, i) => {
            const ang = -Math.PI / 2 + (i * 2 * Math.PI) / 5;
            const tx = cx + labelR * Math.cos(ang);
            const ty = cy + labelR * Math.sin(ang);
            const raw = scores && scores[k];
            const num = typeof raw === 'number' ? raw : parseInt(String(raw), 10) || 0;
            const lab =
              typeof labelForKey === 'function' ? String(labelForKey(k) || k) : String(k);
            const labShow = lab.length > 15 ? lab.slice(0, 14) + '…' : lab;
            return (
              <text
                key={`vlab-${k}`}
                x={tx}
                y={ty}
                textAnchor="middle"
                dominantBaseline="middle"
                fill="rgb(203 213 225 / 0.95)"
                style={{ fontSize: '6.25px', fontWeight: 700 }}
              >
                <tspan x={tx} dy="-0.55em" fill="rgb(226 232 240 / 0.98)" style={{ fontSize: '8.25px' }}>
                  {num}
                </tspan>
                <tspan x={tx} dy="1.12em" fill="rgb(153 194 184 / 0.92)" style={{ fontSize: '6.25px' }}>
                  {labShow}
                </tspan>
              </text>
            );
          })}
        </svg>
      </div>
    );
  }

  /** Flatten traveler_preference / nested suggestion fields to readable lines (no JSON braces). */
  function flattenPreferenceValue(val) {
    if (val == null) return '';
    if (typeof val === 'string') return val.trim();
    if (typeof val === 'number' || typeof val === 'boolean') return String(val);
    if (Array.isArray(val))
      return val
        .map((x) => flattenPreferenceValue(x))
        .filter(Boolean)
        .join(' · ');
    if (typeof val === 'object') {
      const parts = [];
      const keys = Object.keys(val);
      for (let i = 0; i < keys.length; i++) {
        const inner = flattenPreferenceValue(val[keys[i]]);
        if (inner) parts.push(inner);
      }
      return parts.join(' · ');
    }
    return '';
  }

  /**
   * Block verbatim internal vocabulary / tag plumbing from appearing in the public carousel.
   * Returns true when `s` should NOT be shown as-is (use a generic template instead).
   */
  function isUnsafePublicProfileText(s) {
    if (s == null || typeof s !== 'string') return true;
    const t = s.trim();
    if (t.length < 2) return true;
    if (
      /tag_key|target_poi|tags_reference|TravelerPreference|traveler_preference|AI\s*Hint|AIHint|matched_tags|personality_tags/i.test(
        t
      )
    )
      return true;
    if (/Bazi|BaZi|八字|命理|天干|地支/i.test(t)) return true;
    if (/\bcategory\b|\bcategories\b/i.test(t) && /target|poi|tag/i.test(t)) return true;
    if (/\s[\w-]{3,}_([\w-]{3,}_)+[\w-]*/.test(t)) return true;
    if (/^\s*[\[{][\s\S]*[}\]]\s*$/.test(t)) return true;
    if (/\|[^|]{1,40}\|/.test(t) && t.length < 80) return true;
    return false;
  }

  /** Returns trimmed prose or '' when unsafe / empty. Optional fallback string when rejected. */
  function safePublicProfileSentence(raw, fallbackWhenRejected) {
    const x = raw != null ? String(raw).trim().replace(/\s+/g, ' ') : '';
    if (!x) return '';
    if (isUnsafePublicProfileText(x)) {
      return fallbackWhenRejected != null && typeof fallbackWhenRejected === 'string'
        ? fallbackWhenRejected
        : '';
    }
    return x;
  }

  function sentenceSplitProse(raw) {
    const t = String(raw || '')
      .replace(/\s+/g, ' ')
      .trim();
    if (!t) return [];
    const parts = t.split(/(?<=[。！？!?])\s+/).map((s) => s.trim()).filter(Boolean);
    return parts.length ? parts : [t];
  }

  /** Meaning lines for Travel Type — sentences only; never chips/tags. `L` = merged dashboard locale bundle. */
  function buildTravelTypeMeaningStatements(visualProfile, L) {
    const out = [];
    const push = (raw, fb) => {
      if (out.length >= 3) return;
      const s = safePublicProfileSentence(raw, fb);
      const line = s || (typeof fb === 'string' ? fb : '');
      if (!line || out.some((x) => x === line)) return;
      out.push(line.length > 220 ? line.slice(0, 217).trim() + '…' : line);
    };

    const fp = visualProfile?.full_profile;
    if (typeof fp === 'string' && fp.trim()) {
      const sents = sentenceSplitProse(fp);
      for (let i = 0; i < sents.length && out.length < 3; i++) push(sents[i], '');
    }

    const insights = visualProfile?.insights;
    if (Array.isArray(insights)) {
      for (let i = 0; i < insights.length && out.length < 3; i++) {
        const ins = insights[i];
        const merged = [ins && ins.title, ins && ins.text].filter(Boolean).join(' — ');
        push(merged, '');
      }
    }

    const scores = visualProfile?.scores;
    if (scores && out.length < 3) {
      const hk = getDominantScoreKey(scores);
      const wk = getWeakestScoreKey(scores);
      const nh =
        typeof scores[hk] === 'number' ? scores[hk] : parseInt(String(scores[hk]), 10) || 0;
      const nl =
        typeof scores[wk] === 'number' ? scores[wk] : parseInt(String(scores[wk]), 10) || 0;
      push('', nl < 45 ? L.travelMeaningWeakLow : L.travelMeaningWeakNorm);
      if (out.length < 3 && nh >= 60) push('', L.travelMeaningDomHigh);
    }

    const ol = visualProfile?.one_liner;
    if (typeof ol === 'string' && ol.trim()) push(ol, '');

    while (out.length < 2) {
      push('', out.length === 0 ? L.travelMeaningPad0 : L.travelMeaningPad1);
    }

    return out.slice(0, 3);
  }

  /** Travel DNA — three panels of 1–2 sentences; derived from insights/profile prose/scores only. */
  function buildTravelDnaPanels(visualProfile, L) {
    const valued = [];
    const draining = [];
    const rewarding = [];
    const pushOne = (arr, raw, maxLen) => {
      const cap = maxLen || 200;
      const s = safePublicProfileSentence(raw, '');
      if (!s) return;
      const line = s.length > cap ? s.slice(0, cap - 1).trim() + '…' : s;
      if (arr.includes(line)) return;
      arr.push(line);
    };

    const insights = visualProfile?.insights;
    if (Array.isArray(insights)) {
      for (let i = 0; i < insights.length; i++) {
        const ins = insights[i];
        const blob = [ins && ins.title, ins && ins.text].filter(Boolean).join(' ');
        const sf = safePublicProfileSentence(blob.replace(/\s+/g, ' ').trim(), '');
        if (!sf) continue;
        if (/疲劳|拥挤|嘈杂|赶路|混乱|压力|过载|rush|crowd|noise|hectic|overload/i.test(sf))
          pushOne(draining, sf, 200);
        else if (/值得|风景|惊喜|日落|回忆|memorable|worth|meaningful/i.test(sf))
          pushOne(rewarding, sf, 200);
        else pushOne(valued, sf, 200);
      }
    }

    const fps = sentenceSplitProse(visualProfile?.full_profile || '');
    for (let i = 0; i < fps.length && valued.length < 2; i++) pushOne(valued, fps[i], 200);

    const scores = visualProfile?.scores;
    if (scores) {
      const n = (k) => {
        const v = scores[k];
        return typeof v === 'number' ? v : parseInt(String(v), 10) || 0;
      };
      if (valued.length < 2 && (n('comfort') >= 55 || n('culture_depth') >= 55)) {
        pushOne(valued, L.dnaValuedComfortCulture, 200);
      }
      if (draining.length < 2 && (n('pace') >= 65 || n('novelty') >= 70)) {
        pushOne(draining, L.dnaDrainingPaceNovelty, 200);
      }
      if (rewarding.length < 2 && n('novelty') >= 50 && n('culture_depth') >= 50) {
        pushOne(rewarding, L.dnaRewardingNoveltyCulture, 200);
      }
    }

    const pad = (arr, lines) => {
      let i = 0;
      while (arr.length < 1 && i < lines.length) {
        pushOne(arr, lines[i], 200);
        i++;
      }
    };

    pad(valued, [L.dnaPadValued1, L.dnaPadValued2]);
    pad(draining, [L.dnaPadDraining1, L.dnaPadDraining2]);
    pad(rewarding, [L.dnaPadRewarding1, L.dnaPadRewarding2]);

    return {
      valued: valued.slice(0, 2),
      draining: draining.slice(0, 2),
      rewarding: rewarding.slice(0, 2)
    };
  }

  /** Outcome-level copy for Sylvan slide — no raw hints / preferences / POI vocabulary. */
  function buildSylvanOutcomePanels(personality, visualProfile, L) {
    const sc = visualProfile?.scores;
    const n = (k) => {
      const v = sc && sc[k];
      return typeof v === 'number' ? v : parseInt(String(v || 0), 10) || 0;
    };

    const retain = [];
    const cautious = [];
    const balance = [];

    if (n('comfort') >= 52 || n('pace') <= 48) retain.push(L.sylvanRetainComfortPace);
    if (n('culture_depth') >= 52) retain.push(L.sylvanRetainCultureDepth);
    retain.push(L.sylvanRetainNoRush);

    cautious.push(L.sylvanCautiousImprov);
    cautious.push(L.sylvanCautiousPacked);
    if (n('flexibility') <= 45) cautious.push(L.sylvanCautiousPivot);

    balance.push(L.sylvanBalanceSafetyNovelty);
    balance.push(L.sylvanBalanceRestExplore);
    balance.push(L.sylvanBalancePlanDiscovery);

    const nnRaw = personality?.non_negotiables;
    const nnFlat = Array.isArray(nnRaw)
      ? nnRaw.map((x) => String(x).trim()).filter(Boolean).join('；')
      : flattenPreferenceValue(nnRaw);
    const nnSafe = safePublicProfileSentence(nnFlat, '');
    if (nnSafe && nnSafe.length <= 140)
      balance.push(interpolateDashboardTemplate(L.sylvanNnLine, { nn: nnSafe }));

    return {
      retain: retain.slice(0, 4),
      cautious: cautious.slice(0, 4),
      balance: balance.slice(0, 5)
    };
  }

  /** Trip-behaviour interpretation — not a pseudo “science” index. */
  function buildTripSatisfactionInterpretation(scores, labelForKey, L) {
    const keys = ['pace', 'novelty', 'comfort', 'culture_depth', 'flexibility'];
    const rows = keys.map((k) => {
      const raw = scores && scores[k];
      const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10) || 0;
      return { k, n, lab: typeof labelForKey === 'function' ? labelForKey(k) : k };
    });
    rows.sort((a, b) => b.n - a.n);
    const hi = rows.slice(0, 2);
    const lo = rows.slice(-2);
    const sep = L.tripInterpretLabelSep || ', ';
    const hiLabs = hi.map((x) => x.lab).join(sep);
    const loLabs = lo.map((x) => x.lab).join(sep);
    return interpolateDashboardTemplate(L.tripInterpretBody, { highLabels: hiLabs, lowLabels: loLabs });
  }

  /** Fixed six-slide satisfaction flow (when `visual_profile` validates). */
  function buildVisualCarouselSlideIds(visualProfile) {
    if (!visualProfile || !isValidVisualProfileForPowerUp(visualProfile)) return [];
    return [
      'travel-type',
      'travel-dna',
      'flow-shape',
      'sylvan-selection-logic',
      'best-modes',
      'why-profile'
    ];
  }

  /**
   * Merge narrative fields for the “why profile” carousel slide so KV prose is never skipped
   * when `visual_profile.full_profile` is empty (common — the write-up often lives in `personality_summary_raw`).
   */
  function pickReadFullNarrativeSource(profile, visualProfile) {
    const seen = new Set();
    const parts = [];
    const add = (raw) => {
      const s = raw != null ? String(raw).trim() : '';
      if (!s || seen.has(s)) return;
      seen.add(s);
      parts.push(s);
    };
    add(visualProfile?.full_profile);
    add(profile?.personality_summary_raw);
    if (profile?.personality && typeof profile.personality === 'object') {
      add(profile.personality.summary);
      add(profile.personality.personality_summary_raw);
    }
    return parts.join('\n\n');
  }

  /** Twin nav + optional hub dock — hub **`z-[2]`** stacks above twin (**`z-[1]`**) so twin upward rim glow reads behind the four tiles (same **`sf-twin-shell-nav-pulse`** as other immersive surfaces). */
  function PowerUpBottomChrome({ navTwinNuAsk, navTwinExplore, hubDock, tourTwinWrapRef }) {
    const TwinNav = typeof window !== 'undefined' ? window.InternalShellTwinNav : null;
    const dockOuter =
      window.SYLVAN_TECH_THEME?.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)]';
    const dockHub =
      window.SYLVAN_TECH_THEME?.classes?.shellTwinDockHubInner || 'px-3 sm:px-4 pb-0.5 pt-0.5';
    return (
      <div className="pointer-events-auto fixed bottom-0 left-0 right-0 z-[48] flex flex-col">
        {hubDock ? (
          <div className={`relative z-[2] min-w-0 ${dockOuter}`}>
            <div className={dockHub}>{hubDock}</div>
          </div>
        ) : null}
        {TwinNav ? (
          <div ref={tourTwinWrapRef} className="relative z-[1] min-w-0">
            <TwinNav
              variant="powerup"
              leftLabel={navTwinExplore || 'Explore'}
              rightLabel={navTwinNuAsk || 'NOW WHAT'}
              staticTwinEdge
            />
          </div>
        ) : null}
      </div>
    );
  }

  /** Legacy SYS/KV still serves `PowerUpAI` — map to Flow Profile (non-Latin unchanged). */
  function normalizeTabPowerUpLabel(raw) {
    const s = String(raw || '').trim();
    if (!s) return '';
    const compact = s.replace(/[\s_-]/g, '');
    if (/^powerupai$/i.test(compact)) return 'Flow Profile';
    return s;
  }

  function PowerUpAIPage() {
    const FounderVision = typeof window !== 'undefined' ? window.FounderVisionOverlay : null;
    const TV_EMBED = window.TV_EMBED_CHROME_CLASSES;
    const [profile, setProfile] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [success, setSuccess] = useState(null);
    
    // Form state
    const [formData, setFormData] = useState({
      name: "",
      phone: "",
      dob: "",
      birth_place: "",
      gender: "",
      preferred_language: "en",
      photo: null
    });
    const [submitting, setSubmitting] = useState(false);
    const [langSubmitting, setLangSubmitting] = useState(false);
    
    // ✅ PROFILE-PHOTO: State for authenticated photo blob URL
    const [userAvatarUrl, setUserAvatarUrl] = useState(null);
    const avatarObjectUrlRef = useRef(null);
    
    // ✅ UI LANGUAGE AWARENESS: State for language-aware UI strings
    const [uiStrings, setUiStrings] = useState({
      welcomeToSylvanFlow: 'Welcome to SylvanFlow™',
      heresWhatSylvanSees: "HERE'S WHAT SYLVAN SEES IN YOU",
      navTwinNuAsk: 'NOW WHAT',
      navTwinExplore: 'Explore',
      headerTitle: 'Flow Profile',
      headerSubtitle: '',
      hubSummaryTitle: 'Summary',
      hubSummarySub: 'Profile',
      hubFlowFinderTitle: 'Flow Finder',
      hubFlowFinderSub: 'Quiz',
      hubFlowPlanTitle: 'Create Flow Plan',
      hubFlowPlanSub: 'Trip',
      hubAskSylvanTitle: 'Ask Sylvan',
      hubAskSylvanSub: 'Chat'
    });

    /** Full-screen logo intro on mount (~3s: 2.5s hold + 0.5s fade). */
    const [introActive, setIntroActive] = useState(true);
    const [introFading, setIntroFading] = useState(false);
    /** Multi-step layout tour (menu → TV → hub → twin nav). */
    const [layoutTourOpen, setLayoutTourOpen] = useState(false);
    const [layoutTourForce, setLayoutTourForce] = useState(false);
    const [layoutTourAwaitOpen, setLayoutTourAwaitOpen] = useState(false);
    const [layoutTourDontShow, setLayoutTourDontShow] = useState(false);
    const PROFILE_FORM_UI_DEFAULTS = {
      langTitle: 'Choose your language',
      langBody: "We'll show the rest of profile setup in your preferred language.",
      langLabel: 'Preferred language',
      langContinue: 'Continue',
      langSaving: 'Saving…',
      detailsTitle: 'Tell SylvanFlow a bit about you',
      detailsBody:
        "We ask once. These details power accurate itineraries and safety; then you'll take the travel style quiz.",
      languageLockedPrefix: 'Language:',
      labelName: 'Name',
      labelPhone: 'Phone',
      labelDob: 'Date of birth',
      labelBirthPlace: 'Place of birth',
      labelGender: 'Gender',
      labelPhoto: 'Profile photo',
      phonePlaceholder: '+65 … include country code',
      selectCountry: 'Select country…',
      selectOption: 'Select…',
      genderMale: 'Male',
      genderFemale: 'Female',
      genderOther: 'Other',
      genderPreferNot: 'Prefer not to say',
      photoHint: 'JPEG or PNG. Required before the quiz.',
      submit: 'Save profile & start Flow Finder',
      submitSaving: 'Saving…'
    };
    const [profileFormUi, setProfileFormUi] = useState({ ...PROFILE_FORM_UI_DEFAULTS });
    const [layoutTourLabels, setLayoutTourLabels] = useState({
      skip: 'Skip',
      next: 'Next',
      done: 'Done',
      dontShowAgain: "Don't show this again",
      menuTitle: 'Menu',
      menuBody: 'Open settings, language, legal, feedback, and sign out. Replay this tutorial anytime from Menu → Tutorial.',
      tvTitle: 'Your main screen',
      tvBody: 'Your profile summary, Flow Finder quiz, Flow Plan, and Ask Sylvan appear here — one at a time.',
      hubTitle: 'Quick actions',
      hubBody: 'Switch what appears above — explore your style, build a plan, or chat for trip insight.',
      twinTitle: 'Move between apps',
      twinBody: 'Go to NOW WHAT for moment-by-moment decisions, or Explore for itineraries and destinations.'
    });
    const tourMenuRef = useRef(null);
    const tourTvRef = useRef(null);
    const tourHubRef = useRef(null);
    const tourTwinRef = useRef(null);
    /** One-time mission copy after boot intro when profile basics are still missing (per user_key in localStorage). */
    const [firstRunVisionOpen, setFirstRunVisionOpen] = useState(false);
    /** TV plate mode: summary markdown · iframe Flow Finder · iframe Flow Plan form · embedded Ask Sylvan (Power Up only). */
    const [tvPanel, setTvPanel] = useState('summary');
    const [postPlanIntro, setPostPlanIntro] = useState(() => ({ ...POWERUP_POST_PLAN_INTRO_FALLBACK }));

    /** SYS_MESSAGES — TV prereq / loading / hub gate line; English fallbacks if KV miss */
    const [tvStrings, setTvStrings] = useState({
      prereq: '',
      loading: '',
      hubGate: '',
      flowPlanChromeTitle: '',
      flowPlanBackLabel: '',
      askSylvanChromeTitle: ''
    });

    /** Visual_profile carousel copy — SYS `ui:powerup_visual_*` (10 locales); English fallbacks if KV miss */
    const [visualCarouselUi, setVisualCarouselUi] = useState({
      travelTypeTitle: 'Travel Type',
      travelDnaSubtitle: 'Travel DNA',
      flowShapeTitle: 'Flow Shape',
      insightMomentsTitle: 'Insight Moments',
      bestModesTitle: 'Best Travel Modes',
      swipeHint: 'Swipe to explore',
      shapeIndexLabel: 'Shape index',
      scorePace: 'Pace',
      scoreNovelty: 'Novelty',
      scoreComfort: 'Comfort',
      scoreCulture: 'Culture',
      scoreFlexibility: 'Flexibility'
    });

    /** SYS `ui:powerup_visual_dashboard_*` — merged into carousel dashboard bundle (rim keys excluded). */
    const [dashboardSysOverrides, setDashboardSysOverrides] = useState({});

    const resolvedVisualScoreLabels = useMemo(
      () => ({
        pace: visualCarouselUi.scorePace || VISUAL_SCORE_LABELS.pace,
        novelty: visualCarouselUi.scoreNovelty || VISUAL_SCORE_LABELS.novelty,
        comfort: visualCarouselUi.scoreComfort || VISUAL_SCORE_LABELS.comfort,
        culture_depth: visualCarouselUi.scoreCulture || VISUAL_SCORE_LABELS.culture_depth,
        flexibility: visualCarouselUi.scoreFlexibility || VISUAL_SCORE_LABELS.flexibility
      }),
      [visualCarouselUi]
    );

    /** Same language resolution as tvStrings SYS load — avoids English chrome flash before profile loads. */
    const tvChromeFbInline = useMemo(
      () =>
        resolvePowerUpTvChromeFallback(
          profile?.preferred_language ||
            formData.preferred_language ||
            window.SylvanFlowState?.getLanguage?.() ||
            'en'
        ),
      [profile?.preferred_language, formData.preferred_language]
    );

    /** Rich carousel labels — offline fallbacks + rim extras + SYS dashboard KV (rim SYS keys unchanged). */
    const visualCarouselExtraFb = useMemo(() => {
      const lang =
        profile?.preferred_language ||
        formData.preferred_language ||
        window.SylvanFlowState?.getLanguage?.() ||
        'en';
      return {
        ...resolvePowerUpVisualCarouselExtraFallback(lang),
        ...dashboardSysOverrides
      };
    }, [profile?.preferred_language, formData.preferred_language, dashboardSysOverrides]);

    const loadProfileRef = useRef(async () => {});
    const tvOverlayTimerRef = useRef(null);
    /** Full-screen message when blue Flow Finder tile is tapped after quiz is already complete. */
    const [tvFfCompleteOverlay, setTvFfCompleteOverlay] = useState(null);
    const [flowPlanBlockedMessage, setFlowPlanBlockedMessage] = useState(null);
    /** Compact visual_profile carousel (Pages only; optional PERSONALITY_KV field). */
    const [visualCarouselPage, setVisualCarouselPage] = useState(0);
    /** Horizontal snap scroller — same pattern as NuAsk (`snap-x` · native overflow scroll). */
    const visualCarouselScrollRef = useRef(null);
    const scrollCarouselRafRef = useRef(null);
    /** Slide count — synced after `visualCarouselSlides` useMemo (same render). */
    const visualCarouselSlidesRef = useRef([]);
    const vcTouchStartXRef = useRef(null);
    /** After wrap scroll starts, ignore scroll-derived page updates briefly so `Math.round(scrollLeft/w)` does not snap to an intermediate slide mid-animation. */
    const vcWrapLockUntilRef = useRef(0);

    const syncCarouselPageFromScroll = useCallback(() => {
      const el = visualCarouselScrollRef.current;
      if (!el) return;
      if (Date.now() < vcWrapLockUntilRef.current) return;
      const w = el.clientWidth || 1;
      const len = Math.max(1, visualCarouselSlidesRef.current.length || 1);
      const maxScroll = Math.max(0, (el.scrollWidth || 0) - (el.clientWidth || 0));
      const x = el.scrollLeft;
      if (x <= 8) {
        setVisualCarouselPage((p) => (p === 0 ? p : 0));
        return;
      }
      if (maxScroll > 0 && x >= maxScroll - 8) {
        const last = len - 1;
        setVisualCarouselPage((p) => (p === last ? p : last));
        return;
      }
      const i = Math.round(x / w);
      const clamped = Math.max(0, Math.min(len - 1, i));
      setVisualCarouselPage((p) => (p === clamped ? p : clamped));
    }, []);

    const onVisualCarouselScroll = useCallback(() => {
      if (scrollCarouselRafRef.current != null) return;
      scrollCarouselRafRef.current = requestAnimationFrame(() => {
        scrollCarouselRafRef.current = null;
        syncCarouselPageFromScroll();
      });
    }, [syncCarouselPageFromScroll]);

    /** Last slide: swipe left (next) → wrap to first. First slide: swipe right (prev) → wrap to last. */
    const onVisualCarouselTouchStart = useCallback((e) => {
      const t = e.touches && e.touches[0];
      vcTouchStartXRef.current = t ? t.clientX : null;
    }, []);

    const onVisualCarouselTouchEnd = useCallback((e) => {
      const start = vcTouchStartXRef.current;
      vcTouchStartXRef.current = null;
      const ch = e.changedTouches && e.changedTouches[0];
      if (start == null || !ch) return;
      const dx = ch.clientX - start;
      const threshold = 48;
      const el = visualCarouselScrollRef.current;
      const slides = visualCarouselSlidesRef.current;
      const len = slides.length;
      if (!el || len < 2) return;
      const w = el.clientWidth || 1;
      const maxScroll = Math.max(0, (el.scrollWidth || 0) - (el.clientWidth || 0));
      let page = Math.round(el.scrollLeft / w);
      if (el.scrollLeft <= 8) page = 0;
      else if (maxScroll > 0 && el.scrollLeft >= maxScroll - 8) page = len - 1;
      else page = Math.max(0, Math.min(len - 1, page));
      if (page === len - 1 && dx < -threshold) {
        vcWrapLockUntilRef.current = Date.now() + 520;
        setVisualCarouselPage(0);
        return;
      }
      if (page === 0 && dx > threshold) {
        vcWrapLockUntilRef.current = Date.now() + 520;
        setVisualCarouselPage(len - 1);
      }
    }, []);

    useEffect(() => {
      return () => {
        if (scrollCarouselRafRef.current != null) {
          cancelAnimationFrame(scrollCarouselRafRef.current);
          scrollCarouselRafRef.current = null;
        }
      };
    }, []);

    // ✅ FIX: Identity comes from Bearer token via authenticatedFetch, not from localStorage email
    // Removed userId dependency - authenticatedFetch works with session token only
    // Identity is derived server-side from Authorization Bearer token (opaque userKey)
    // Profile.user_key is the canonical identity field (user_id is deprecated)

    // ✅ DEBUG: Log when component renders (admin-only, sanitized)
    if (window.uiLogger) {
      window.uiLogger.uiLog('[POWERUP] render', { 
        hasProfile: !!profile, 
        profileOnboardingState: profile?.onboarding_state,
        profileUserKeyLength: profile?.user_key?.length || 0,
        loading,
        // ✅ PROFILE-PHOTO: Log photo field status
        hasProfilePhotoUrl: !!profile?.profile_photo_url,
        hasProfilePhotoR2Key: !!profile?.profile_photo_r2_key,
        hasPhotoUrl: !!profile?.photo_url,
        shouldShowUpload: (!profile?.profile_photo_url && !profile?.profile_photo_r2_key && !profile?.photo_url)
      });
    }

    // ✅ PROFILE-PHOTO: Cleanup object URL on unmount
    useEffect(() => {
      return () => {
        if (avatarObjectUrlRef.current) {
          URL.revokeObjectURL(avatarObjectUrlRef.current);
          avatarObjectUrlRef.current = null;
        }
      };
    }, []);

    useEffect(() => {
      setVisualCarouselPage(0);
    }, [profile?.user_key, profile?.personality?.visual_profile]);

    useEffect(() => {
      window.SYLVAN_TECH_THEME?.ensureKeyframes?.();
      const fadeAt = setTimeout(() => setIntroFading(true), 2500);
      const hideAt = setTimeout(() => setIntroActive(false), 3000);
      return () => {
        clearTimeout(fadeAt);
        clearTimeout(hideAt);
      };
    }, []);

    useEffect(() => {
      let cancelled = false;
      const run = async () => {
        const lang =
          profile?.preferred_language ||
          formData.preferred_language ||
          window.SylvanFlowState?.getLanguage?.() ||
          'en';
        const chromeFb = resolvePowerUpTvChromeFallback(lang);
        const vx = resolvePowerUpVisualCarouselExtraFallback(lang);
        try {
          const [
            prereq,
            loadingMsg,
            hubGate,
            flowPlanChromeTitle,
            flowPlanBackLabel,
            askSylvanChromeTitle,
            travelTypeTitle,
            travelDnaSubtitle,
            flowShapeTitle,
            insightMomentsTitle,
            bestModesTitle,
            swipeHint,
            shapeIndexLabel,
            scorePace,
            scoreNovelty,
            scoreComfort,
            scoreCulture,
            scoreFlexibility
          ] = await Promise.all([
            window.getSysMessage?.('ui:powerup_tv_complete_flow_finder_first', lang),
            window.getSysMessage?.('ui:powerup_tv_profile_summary_loading', lang),
            window.getSysMessage?.('ui:powerup_hub_profile_gate_hint', lang),
            window.getSysMessage?.('ui:powerup_tv_flow_plan_chrome_title', lang),
            window.getSysMessage?.('ui:powerup_tv_flow_plan_back_label', lang),
            window.getSysMessage?.('ui:powerup_tv_ask_sylvan_chrome_title', lang),
            window.getSysMessage?.('ui:powerup_visual_travel_type_title', lang),
            window.getSysMessage?.('ui:powerup_visual_travel_dna_subtitle', lang),
            window.getSysMessage?.('ui:powerup_visual_flow_shape_title', lang),
            window.getSysMessage?.('ui:powerup_visual_insight_moments_title', lang),
            window.getSysMessage?.('ui:powerup_visual_best_modes_title', lang),
            window.getSysMessage?.('ui:powerup_visual_swipe_hint', lang),
            window.getSysMessage?.('ui:powerup_visual_shape_index_label', lang),
            window.getSysMessage?.('ui:powerup_visual_score_pace', lang),
            window.getSysMessage?.('ui:powerup_visual_score_novelty', lang),
            window.getSysMessage?.('ui:powerup_visual_score_comfort', lang),
            window.getSysMessage?.('ui:powerup_visual_score_culture', lang),
            window.getSysMessage?.('ui:powerup_visual_score_flexibility', lang)
          ]);
          if (cancelled) return;
          setTvStrings({
            prereq:
              prereq ||
              'Tap the blue Flow Finder tile below — your quiz opens in this screen; your summary appears here when you\'re done.',
            loading: loadingMsg || 'Preparing your profile summary…',
            hubGate:
              hubGate ||
              'A few quick basics, then your travel-style quiz — tap the blue Flow Finder tile below.',
            flowPlanChromeTitle:
              (flowPlanChromeTitle && String(flowPlanChromeTitle).trim()) || chromeFb.flowPlanChromeTitle,
            flowPlanBackLabel:
              (flowPlanBackLabel && String(flowPlanBackLabel).trim()) || chromeFb.flowPlanBackLabel,
            askSylvanChromeTitle:
              (askSylvanChromeTitle && String(askSylvanChromeTitle).trim()) ||
              chromeFb.askSylvanChromeTitle
          });
          setVisualCarouselUi({
            travelTypeTitle:
              (travelTypeTitle && String(travelTypeTitle).trim()) || vx.travelTypeTitle,
            travelDnaSubtitle:
              (travelDnaSubtitle && String(travelDnaSubtitle).trim()) || vx.travelDnaSubtitleLine,
            flowShapeTitle:
              (flowShapeTitle && String(flowShapeTitle).trim()) || vx.flowShapeTitle,
            insightMomentsTitle:
              (insightMomentsTitle && String(insightMomentsTitle).trim()) || vx.insightMomentsTitle,
            bestModesTitle:
              (bestModesTitle && String(bestModesTitle).trim()) || vx.bestModesTitle,
            swipeHint: (swipeHint && String(swipeHint).trim()) || vx.swipeHint,
            shapeIndexLabel:
              (shapeIndexLabel && String(shapeIndexLabel).trim()) || vx.shapeIndexLabel,
            scorePace: (scorePace && String(scorePace).trim()) || 'Pace',
            scoreNovelty: (scoreNovelty && String(scoreNovelty).trim()) || 'Novelty',
            scoreComfort: (scoreComfort && String(scoreComfort).trim()) || 'Comfort',
            scoreCulture: (scoreCulture && String(scoreCulture).trim()) || 'Culture',
            scoreFlexibility:
              (scoreFlexibility && String(scoreFlexibility).trim()) || 'Flexibility'
          });
          const dashKeys = Object.keys(PU_VISUAL_DASHBOARD_FALLBACK_EN).filter(
            (k) => !POWERUP_VISUAL_CAROUSEL_RIM_KEY_SET.has(k)
          );
          const dashMsgs = await Promise.all(
            dashKeys.map((k) => window.getSysMessage?.(powerupVisualDashboardKeyToSysId(k), lang))
          );
          const dashOvr = {};
          for (let i = 0; i < dashKeys.length; i++) {
            const m = dashMsgs[i];
            if (m && String(m).trim()) dashOvr[dashKeys[i]] = String(m).trim();
          }
          if (!cancelled) setDashboardSysOverrides(dashOvr);
        } catch (_) {
          if (!cancelled) {
            const vx = resolvePowerUpVisualCarouselExtraFallback(lang);
            setTvStrings({
              prereq:
                'Tap the blue Flow Finder tile below — your quiz opens in this screen; your summary appears here when you\'re done.',
              loading: 'Preparing your profile summary…',
              hubGate:
                'A few quick basics, then your travel-style quiz — tap the blue Flow Finder tile below.',
              flowPlanChromeTitle: chromeFb.flowPlanChromeTitle,
              flowPlanBackLabel: chromeFb.flowPlanBackLabel,
              askSylvanChromeTitle: chromeFb.askSylvanChromeTitle
            });
            setVisualCarouselUi({
              travelTypeTitle: vx.travelTypeTitle,
              travelDnaSubtitle: vx.travelDnaSubtitleLine,
              flowShapeTitle: vx.flowShapeTitle,
              insightMomentsTitle: vx.insightMomentsTitle,
              bestModesTitle: vx.bestModesTitle,
              swipeHint: vx.swipeHint,
              shapeIndexLabel: vx.shapeIndexLabel,
              scorePace: 'Pace',
              scoreNovelty: 'Novelty',
              scoreComfort: 'Comfort',
              scoreCulture: 'Culture',
              scoreFlexibility: 'Flexibility'
            });
            setDashboardSysOverrides({});
          }
        }
      };
      run();
      const onLang = () => run();
      window.SylvanFlowState?.on?.('language', onLang);
      return () => {
        cancelled = true;
        window.SylvanFlowState?.off?.('language', onLang);
      };
    }, [profile?.preferred_language, formData.preferred_language]);

    useEffect(() => {
      let cancelled = false;
      const run = async () => {
        const lang =
          profile?.preferred_language || window.SylvanFlowState?.getLanguage?.() || 'en';
        const FB = POWERUP_POST_PLAN_INTRO_FALLBACK;
        try {
          const [title, lead, detail, hotkeys, continueCta, videoUrl] = await Promise.all([
            window.getSysMessage?.('ui:powerup_post_plan_intro_title', lang),
            window.getSysMessage?.('ui:powerup_post_plan_intro_lead', lang),
            window.getSysMessage?.('ui:powerup_post_plan_intro_detail', lang),
            window.getSysMessage?.('ui:powerup_post_plan_intro_hotkeys', lang),
            window.getSysMessage?.('ui:powerup_post_plan_intro_continue', lang),
            window.getSysMessage?.('ui:powerup_post_plan_intro_video_url', lang)
          ]);
          if (cancelled) return;
          setPostPlanIntro({
            title: (title && String(title).trim()) || FB.title,
            lead: (lead && String(lead).trim()) || FB.lead,
            detail: (detail && String(detail).trim()) || FB.detail,
            hotkeys: (hotkeys && String(hotkeys).trim()) || FB.hotkeys,
            continueCta: (continueCta && String(continueCta).trim()) || FB.continueCta,
            videoUrl: (videoUrl != null ? String(videoUrl) : '').trim()
          });
        } catch (_) {
          if (!cancelled) setPostPlanIntro({ ...POWERUP_POST_PLAN_INTRO_FALLBACK });
        }
      };
      run();
      const onLang = () => run();
      window.SylvanFlowState?.on?.('language', onLang);
      return () => {
        cancelled = true;
        window.SylvanFlowState?.off?.('language', onLang);
      };
    }, [profile?.preferred_language]);

    // ✅ PROFILE-PHOTO: Fetch profile photo when profile changes
    useEffect(() => {
      // Reset avatar URL when profile changes
      if (avatarObjectUrlRef.current) {
        URL.revokeObjectURL(avatarObjectUrlRef.current);
        avatarObjectUrlRef.current = null;
      }
      setUserAvatarUrl(null);
      
      if (!profile) {
        return;
      }
      
      // ✅ PROFILE-PHOTO: If profile_photo_url is already a public URL, use it directly
      if (profile.profile_photo_url && !profile.profile_photo_url.includes('/api/profile-photo/')) {
        setUserAvatarUrl(profile.profile_photo_url);
        return;
      }
      
      // ✅ PROFILE-PHOTO: Use cached data URL (persists across refresh)
      // ✅ TASK 2.2: Force fetch if profile has r2_key but photoUrl is null
      if (profile.profile_photo_r2_key || profile.profile_photo_url?.includes('/api/profile-photo/')) {
        const loadPhoto = async () => {
          try {
            // Check cache first
            const CACHE_KEY = 'profile_photo_cache_v1';
            const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
            const r2Key = profile.profile_photo_r2_key;
            
            if (r2Key) {
              try {
                const cached = localStorage.getItem(CACHE_KEY);
                if (cached) {
                  const cacheData = JSON.parse(cached);
                  if (cacheData.profile_photo_r2_key === r2Key) {
                    const cacheAge = Date.now() - (cacheData.cachedAt || 0);
                    if (cacheAge < CACHE_MAX_AGE_MS) {
                      // Cache hit - use immediately
                      if (window.uiLogger) {
                        window.uiLogger.uiLog(`[POWERUP] [PROFILE-PHOTO] Cache hit (age: ${Math.floor(cacheAge / 1000 / 60)} min)`);
                      }
                      setUserAvatarUrl(cacheData.dataUrl);
                      // Refetch in background to refresh cache
                      fetchPhotoInBackground();
                      return;
                    } else {
                      // Cache expired
                      localStorage.removeItem(CACHE_KEY);
                    }
                  }
                }
              } catch (cacheError) {
                if (window.uiLogger) {
                  window.uiLogger.uiWarn('[POWERUP] [PROFILE-PHOTO] Cache read error:', cacheError.message || cacheError);
                }
              }
            }
            
            // Cache miss - fetch and cache
            await fetchPhotoInBackground();
          } catch (error) {
            if (window.uiLogger) {
              window.uiLogger.uiWarn('[POWERUP] [PROFILE-PHOTO] Error loading photo:', error.message || error);
            }
            setUserAvatarUrl(null);
          }
        };
        
        const fetchPhotoInBackground = async () => {
          try {
            const res = await window.SylvanFlowAuth.authenticatedFetch(
              `${SF_API_BASE}/api/profile-photo/self`
            );
            
            // ✅ UI-BUG-02 FIX: Check status before reading blob (204 = no content)
            if (res.status === 204) {
              return;
            }
            
            if (res.ok) {
              const blob = await res.blob();
              if (blob.size === 0 || !blob.type.startsWith('image/')) {
                if (window.uiLogger) {
                  window.uiLogger.uiWarn('[POWERUP] [PROFILE-PHOTO] Invalid blob', { size: blob.size, type: blob.type });
                }
                return;
              }
              
              // Convert to data URL
              const reader = new FileReader();
              reader.onloadend = () => {
                const dataUrl = reader.result;
                const r2Key = profile.profile_photo_r2_key;
                if (r2Key && dataUrl) {
                  // Cache it
                  try {
                    const cacheData = {
                      profile_photo_r2_key: r2Key,
                      dataUrl: dataUrl,
                      cachedAt: Date.now()
                    };
                    localStorage.setItem('profile_photo_cache_v1', JSON.stringify(cacheData));
                    if (window.uiLogger) {
                      window.uiLogger.uiLog('[POWERUP] [PROFILE-PHOTO] Cached (size: ' + blob.size + ' bytes)');
                    }
                  } catch (cacheError) {
                    if (window.uiLogger) {
                      window.uiLogger.uiWarn('[POWERUP] [PROFILE-PHOTO] Cache write error:', cacheError.message || cacheError);
                    }
                  }
                }
                setUserAvatarUrl(dataUrl);
              };
              reader.onerror = () => {
                if (window.uiLogger) {
                  window.uiLogger.uiWarn('[POWERUP] [PROFILE-PHOTO] FileReader error');
                }
                setUserAvatarUrl(null);
              };
              reader.readAsDataURL(blob);
            } else {
              if (window.uiLogger) {
                window.uiLogger.uiWarn('[POWERUP] [PROFILE-PHOTO] Failed to fetch photo:', res.status);
              }
              setUserAvatarUrl(null);
            }
          } catch (error) {
            if (window.uiLogger) {
              window.uiLogger.uiWarn('[POWERUP] [PROFILE-PHOTO] Error fetching photo:', error.message || error);
            }
            setUserAvatarUrl(null);
          }
        };
        
        loadPhoto();
      }
      
      // ✅ TASK 2.3: Hydration log (ONE-TIME per page load, admin-only, sanitized)
      if (window.uiLogger) {
        window.uiLogger.uiLog(`[PROFILE_PHOTO] hydration`, {
          hasProfile: !!profile,
          hasR2Key: !!profile?.profile_photo_r2_key,
          fetchAttempted: !!(profile.profile_photo_r2_key || profile.profile_photo_url?.includes('/api/profile-photo/')),
          fetchStatus: userAvatarUrl ? 'success' : 'pending'
        });
      }
    }, [profile?.profile_photo_r2_key, profile?.profile_photo_url]);

    useEffect(() => {
      // ✅ FIX: Identity guarantee log - once per page load (admin-only, sanitized)
      const sessionToken = localStorage.getItem('sylvanflow_session_token');
      if (window.uiLogger) {
        window.uiLogger.uiLog('[IDENTITY][POWERUP] source=Bearer_only tokenLength=' + (sessionToken?.length || 0));
      }
      
      // Get profile from state store if available
      if (window.SylvanFlowState && window.SylvanFlowState.isProfileLoaded()) {
        const cachedProfile = window.SylvanFlowState.getProfile();
        if (cachedProfile) {
          // ✅ PROFILE-PHOTO: Log photo fields from cache (admin-only, sanitized)
          if (window.uiLogger) {
            window.uiLogger.uiLog('[POWERUP] profile from cache:', { 
              onboarding_state: cachedProfile.onboarding_state,
              hasName: !!cachedProfile.name,
              hasProfilePhotoUrl: !!cachedProfile.profile_photo_url,
              hasProfilePhotoR2Key: !!cachedProfile.profile_photo_r2_key,
              hasPhotoUrl: !!cachedProfile.photo_url,
              hasPhotoFields: !!(cachedProfile.profile_photo_url || cachedProfile.profile_photo_r2_key || cachedProfile.photo_url)
            });
          }
          setProfile(cachedProfile);
          setLoading(false);
          // ✅ P0-1 EVIDENCE: Log cached profile snapshot (for evidence capture, admin-only, sanitized)
          // Note: This is from cache, not API. For API evidence, clear cache or force reload.
          if (window.uiLogger) {
            window.uiLogger.uiLog("[PROFILE_SNAPSHOT]", {
              source: "cache",
              ok: true,
              keys: ["cached"],
              hasProfile_raw: null,
              hasProfile_nested: null,
              hasProfile_canonical: !!cachedProfile,
              state: null,
              code: null,
              dataKeys: [],
              profileKeysCount: cachedProfile ? Object.keys(cachedProfile).length : 0
            });
          }
          // ✅ PROFILE-PHOTO: Still load from API to get latest photo fields (cache might be stale)
          // But don't block UI - show cached profile immediately, then update when API responds
          loadProfile().catch(err => {
            if (window.uiLogger) {
              window.uiLogger.uiWarn('[POWERUP] Failed to refresh profile from API:', err.message || err);
            }
          });
          return;
        }
      }
      loadProfile();
    }, []);

    // ✅ UI strings: load from KV in background — never block the page (fetch can stall; defaults are in useState)
    useEffect(() => {
      const loadUiStrings = async () => {
        const language = window.SylvanFlowState?.getLanguage() || 'en';
        
        try {
          const [
            welcomeToSylvanFlow,
            heresWhatSylvanSees,
            navTwinNuAsk,
            navTwinExplore,
            headerTitle,
            hubSummaryTitle,
            hubSummarySub,
            hubFlowFinderTitle,
            hubFlowFinderSub,
            hubFlowPlanTitle,
            hubFlowPlanSub,
            hubAskSylvanTitle,
            hubAskSylvanSub
          ] = await Promise.all([
            window.getSysMessage('ui:welcome_to_sylvanflow', language),
            window.getSysMessage('ui:heres_what_sylvan_sees', language),
            window.getSysMessage('ui:tab_nu_ask_sylvan', language),
            window.getSysMessage('ui:tab_explore', language),
            window.getSysMessage('ui:tab_powerupai', language),
            window.getSysMessage('ui:powerup_hub_summary_title', language),
            window.getSysMessage('ui:powerup_hub_summary_sub', language),
            window.getSysMessage('ui:powerup_hub_flow_finder_title', language),
            window.getSysMessage('ui:powerup_hub_flow_finder_sub', language),
            window.getSysMessage('ui:powerup_hub_flow_plan_title', language),
            window.getSysMessage('ui:powerup_hub_flow_plan_sub', language),
            window.getSysMessage('ui:powerup_hub_ask_sylvan_title', language),
            window.getSysMessage('ui:powerup_hub_ask_sylvan_sub', language)
          ]);
          
          setUiStrings({
            welcomeToSylvanFlow: welcomeToSylvanFlow || 'Welcome to SylvanFlow™',
            heresWhatSylvanSees: heresWhatSylvanSees || "HERE'S WHAT SYLVAN SEES IN YOU",
            navTwinNuAsk: navTwinNuAsk || 'NOW WHAT',
            navTwinExplore: navTwinExplore || 'Explore',
            headerTitle: normalizeTabPowerUpLabel(headerTitle) || 'Flow Profile',
            headerSubtitle: '',
            hubSummaryTitle: hubSummaryTitle || 'Summary',
            hubSummarySub: hubSummarySub || 'Profile',
            hubFlowFinderTitle: hubFlowFinderTitle || 'Flow Finder',
            hubFlowFinderSub: hubFlowFinderSub || 'Quiz',
            hubFlowPlanTitle: hubFlowPlanTitle || 'Create Flow Plan',
            hubFlowPlanSub: hubFlowPlanSub || 'Trip',
            hubAskSylvanTitle: hubAskSylvanTitle || 'Ask Sylvan',
            hubAskSylvanSub: hubAskSylvanSub || 'Chat'
          });
        } catch (error) {
          if (window.uiLogger) {
            window.uiLogger.uiWarn('[POWERUP] Error loading UI strings:', error.message || error);
          }
        }
      };
      
      loadUiStrings();
      
      // Re-fetch when language changes
      const handleLanguageChange = () => {
        loadUiStrings();
      };
      
      if (window.SylvanFlowState?.on) {
        window.SylvanFlowState.on('language', handleLanguageChange);
        return () => {
          window.SylvanFlowState.off('language', handleLanguageChange);
        };
      }
    }, []);

    useEffect(() => {
      let cancelled = false;
      const lang =
        profile?.preferred_language ||
        formData.preferred_language ||
        window.SylvanFlowState?.getLanguage?.() ||
        '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;
        }
      };
      const D = PROFILE_FORM_UI_DEFAULTS;
      (async () => {
        const entries = await Promise.all([
          ['langTitle', 'ui:powerup_profile_lang_title', D.langTitle],
          ['langBody', 'ui:powerup_profile_lang_body', D.langBody],
          ['langLabel', 'ui:powerup_profile_lang_label', D.langLabel],
          ['langContinue', 'ui:powerup_profile_lang_continue', D.langContinue],
          ['langSaving', 'ui:powerup_profile_lang_saving', D.langSaving],
          ['detailsTitle', 'ui:powerup_profile_details_title', D.detailsTitle],
          ['detailsBody', 'ui:powerup_profile_details_body', D.detailsBody],
          ['languageLockedPrefix', 'ui:powerup_profile_language_locked_prefix', D.languageLockedPrefix],
          ['labelName', 'ui:powerup_profile_label_name', D.labelName],
          ['labelPhone', 'ui:powerup_profile_label_phone', D.labelPhone],
          ['labelDob', 'ui:powerup_profile_label_dob', D.labelDob],
          ['labelBirthPlace', 'ui:powerup_profile_label_birth_place', D.labelBirthPlace],
          ['labelGender', 'ui:powerup_profile_label_gender', D.labelGender],
          ['labelPhoto', 'ui:powerup_profile_label_photo', D.labelPhoto],
          ['phonePlaceholder', 'ui:powerup_profile_phone_placeholder', D.phonePlaceholder],
          ['selectCountry', 'ui:powerup_profile_select_country', D.selectCountry],
          ['selectOption', 'ui:powerup_profile_select_option', D.selectOption],
          ['genderMale', 'ui:powerup_profile_gender_male', D.genderMale],
          ['genderFemale', 'ui:powerup_profile_gender_female', D.genderFemale],
          ['genderOther', 'ui:powerup_profile_gender_other', D.genderOther],
          ['genderPreferNot', 'ui:powerup_profile_gender_prefer_not', D.genderPreferNot],
          ['photoHint', 'ui:powerup_profile_photo_hint', D.photoHint],
          ['submit', 'ui:powerup_profile_submit', D.submit],
          ['submitSaving', 'ui:powerup_profile_submit_saving', D.submitSaving]
        ].map(async ([prop, key, fb]) => [prop, await load(key, fb)]));
        if (cancelled) return;
        setProfileFormUi(Object.fromEntries(entries));
      })();
      return () => {
        cancelled = true;
      };
    }, [profile?.preferred_language, formData.preferred_language]);

    const loadProfile = async () => {
      // ✅ FIX: userId is optional - authenticatedFetch works with session token only
      // Do NOT return early if userId is missing - backend derives identity from token
      
      try {
        setLoading(true);
        setError(null);
        
        const res = await window.SylvanFlowAuth.authenticatedFetch(
          `${SF_API_BASE}/api/profile/me`
        );
        
        // Handle auth response (backend now returns HTTP 200 for all states, never 404)
        window.SylvanFlowAuth.handleAuthResponse(res);
        
        if (!res.ok) {
          throw new Error(`Failed to load profile: ${res.status}`);
        }
        
        const data = await res.json();
        
        // ✅ P0-1: Normalize profile response using canonical function
        const normalized = window.normalizeProfileResponse ? window.normalizeProfileResponse(data) : {
          ok: data.ok === true,
          profile: data.data?.profile || data.profile,
          state: data.state,
          code: data.code,
          hasProfile: !!(data.data?.profile || data.profile)
        };
        
        // ✅ FIX: Handle PROFILE_INCOMPLETE state (bootstrap profile from PIN verification)
        if (normalized.state === "PROFILE_INCOMPLETE" && normalized.profile) {
          setProfile(normalized.profile);
          setFormData({
            name: normalized.profile.name || normalized.profile.user_name || "",
            phone: normalized.profile.phone || "",
            dob: normalized.profile.dob || "",
            birth_place: normalized.profile.birth_place || "",
            gender: normalized.profile.gender || "",
            preferred_language: normalized.profile.preferred_language || "en",
            photo: null
          });
          // Update state store
          if (window.SylvanFlowState) {
            window.SylvanFlowState.setProfile(normalized.profile);
          }
          setLoading(false);
          return;
        }
        
        // ✅ FIX: Handle PROFILE_MISSING state (first-time user, no profile at all)
        if (normalized.state === "PROFILE_MISSING" || normalized.code === "PROFILE_MISSING") {
          setProfile(null);
          setError(null);
          setLoading(false);
          return;
        }
        
        // ✅ Handle complete profile
        if (normalized.ok && normalized.profile) {
          // ✅ PROFILE-PHOTO: Log photo fields when profile loads (admin-only, sanitized)
          if (window.uiLogger) {
            window.uiLogger.uiLog('[POWERUP] Profile loaded with photo fields:', {
              hasProfilePhotoUrl: !!normalized.profile.profile_photo_url,
              hasProfilePhotoR2Key: !!normalized.profile.profile_photo_r2_key,
              hasPhotoUrl: !!normalized.profile.photo_url,
              allPhotoFieldsNull: (!normalized.profile.profile_photo_url && !normalized.profile.profile_photo_r2_key && !normalized.profile.photo_url)
            });
          }
          
          setProfile(normalized.profile);
          setFormData({
            name: normalized.profile.name || normalized.profile.user_name || "",
            phone: normalized.profile.phone || "",
            dob: normalized.profile.dob || "",
            birth_place: normalized.profile.birth_place || "",
            gender: normalized.profile.gender || "",
            preferred_language: normalized.profile.preferred_language || "en",
            photo: null
          });
          // Update state store
          if (window.SylvanFlowState) {
            window.SylvanFlowState.setProfile(normalized.profile);
          }
        } else {
          setProfile(null);
        }
      } catch (e) {
        if (window.uiLogger) {
          window.uiLogger.uiError("Profile load error:", e.message || e);
        }
        setError(e.message || "Unable to load profile");
      } finally {
        setLoading(false);
      }
    };

    loadProfileRef.current = loadProfile;

    const flowFinderIframeSrc = useMemo(() => {
      const preferredLanguage = profile?.preferred_language || formData.preferred_language || 'en';
      const params = new URLSearchParams();
      params.set('lang', preferredLanguage);
      params.set('embed', 'powerup');
      return `/flow-finder?${params.toString()}`;
    }, [profile?.preferred_language, formData.preferred_language]);

    const flowPlanFormSrc = useMemo(() => {
      const preferredLanguage = profile?.preferred_language || formData.preferred_language || 'en';
      const params = new URLSearchParams();
      params.set('lang', preferredLanguage);
      params.set('embed', 'tv');
      return `/flow-plan-form?${params.toString()}`;
    }, [profile?.preferred_language, formData.preferred_language]);

    const openFlowPlanTvIfAllowed = useCallback(async () => {
      const Guard = typeof window !== 'undefined' ? window.flowPlanCreateGuard : null;
      const lang =
        profile?.preferred_language ||
        formData.preferred_language ||
        (typeof window !== 'undefined' && window.SylvanFlowState?.getLanguage?.()) ||
        'en';
      if (!Guard || typeof Guard.fetchItinerariesForFlowPlanGuard !== 'function') {
        setFlowPlanBlockedMessage(null);
        setTvPanel('flowPlan');
        return;
      }
      try {
        const list = await Guard.fetchItinerariesForFlowPlanGuard();
        const block = Guard.evaluateFlowPlanCreateBlockFromList(list, Guard.clientCalendarDayYMD());
        if (block && typeof Guard.getFlowPlanBlockedMessage === 'function') {
          const msg = await Guard.getFlowPlanBlockedMessage(block, lang);
          setFlowPlanBlockedMessage(msg || null);
          return;
        }
      } catch (_) {
        /* fail open */
      }
      setFlowPlanBlockedMessage(null);
      setTvPanel('flowPlan');
    }, [profile?.preferred_language, formData.preferred_language]);

    useEffect(() => {
      const onMsg = (ev) => {
        if (ev.origin !== window.location.origin) return;
        const d = ev.data;
        if (!d || typeof d !== 'object') return;
        if (d.source === 'flow-finder') {
          if (d.type === 'sf-profile-saved' || d.type === 'sf-flow-finder-complete') {
            if (d.type === 'sf-flow-finder-complete') {
              void openFlowPlanTvIfAllowed();
            }
            const fn = loadProfileRef.current;
            if (typeof fn === 'function') void fn();
            return;
          }
          if (d.type === 'sf-flow-finder-close') {
            setTvPanel('summary');
          }
          return;
        }
        if (d.source === 'flow-plan-form' && d.type === 'sylvanflow-flow-plan-submitted') {
          const fn = loadProfileRef.current;
          if (typeof fn === 'function') void fn();
          let introSeen = false;
          try {
            introSeen = localStorage.getItem(POWERUP_POST_PLAN_INTRO_SEEN_KEY) === '1';
          } catch (_) {}
          if (!introSeen) {
            setTvPanel('postPlanWelcome');
          } else {
            setTvPanel('summary');
          }
        }
      };
      window.addEventListener('message', onMsg);
      return () => window.removeEventListener('message', onMsg);
    }, [openFlowPlanTvIfAllowed]);

    useEffect(() => {
      return () => {
        if (tvOverlayTimerRef.current) {
          window.clearTimeout(tvOverlayTimerRef.current);
          tvOverlayTimerRef.current = null;
        }
      };
    }, []);

    useEffect(() => {
      const flowDone =
        profile?.onboarding_state === 'flow_finder_completed' ||
        profile?.onboarding_state === 'first_itinerary_created';
      const hasSum = !!(
        profile?.personality_summary_raw &&
        String(profile.personality_summary_raw).trim()
      );
      if (!profile || !flowDone || hasSum) return undefined;
      let n = 0;
      const id = window.setInterval(() => {
        n += 1;
        if (n > 45) {
          window.clearInterval(id);
          return;
        }
        const fn = loadProfileRef.current;
        if (typeof fn === 'function') fn();
      }, 4000);
      return () => window.clearInterval(id);
    }, [profile?.onboarding_state, profile?.personality_summary_raw, profile?.user_key]);

    const displayName =
      window.SYLVAN_TECH_THEME?.normalizeProfileDisplayName?.(
        profile?.name || profile?.user_name,
        'Traveler'
      ) || 'Traveler';

    const hubSlotCopy = useMemo(
      () => ({
        'profile-summary': { title: uiStrings.hubSummaryTitle, sub: uiStrings.hubSummarySub },
        'flow-finder': { title: uiStrings.hubFlowFinderTitle, sub: uiStrings.hubFlowFinderSub },
        'flow-plan': { title: uiStrings.hubFlowPlanTitle, sub: uiStrings.hubFlowPlanSub },
        'ask-sylvan': { title: uiStrings.hubAskSylvanTitle, sub: uiStrings.hubAskSylvanSub }
      }),
      [
        uiStrings.hubSummaryTitle,
        uiStrings.hubSummarySub,
        uiStrings.hubFlowFinderTitle,
        uiStrings.hubFlowFinderSub,
        uiStrings.hubFlowPlanTitle,
        uiStrings.hubFlowPlanSub,
        uiStrings.hubAskSylvanTitle,
        uiStrings.hubAskSylvanSub
      ]
    );

    const flowFinderCompleted =
      profile?.onboarding_state === 'flow_finder_completed' ||
      profile?.onboarding_state === 'first_itinerary_created';
    const hasPersonalityBlock = !!(
      profile?.personality_summary_raw &&
      String(profile.personality_summary_raw).trim()
    );
    const hasPersonality = hasPersonalityBlock;
    /** Insert paragraph breaks for long single-block summaries (Chinese/Latin sentence ends). */
    const injectParagraphBreaksForPersonality = (text) => {
      if (!text || typeof text !== 'string') return text;
      const t = text.trim();
      if (/\n\n/.test(t)) return t;
      const DIV = '\u0001';
      const alt = t
        .replace(/([。！？.!?])/g, '$1' + DIV)
        .split(DIV)
        .map((s) => s.trim())
        .filter(Boolean);
      if (alt.length <= 1) return t;
      const paras = [];
      let buf = '';
      for (let i = 0; i < alt.length; i++) {
        const next = buf ? buf + ' ' + alt[i] : alt[i];
        if (next.length >= 200 && buf) {
          paras.push(buf.trim());
          buf = alt[i];
        } else {
          buf = next;
        }
      }
      if (buf) paras.push(buf.trim());
      return paras.filter(Boolean).join('\n\n');
    };

    const personalityTextRaw = profile?.personality_summary_raw || '';

    // ✅ PROFILE-SUMMARY: Convert markdown to HTML (bold formatting + paragraph breaks)
    const convertMarkdownToHTML = (text) => {
      if (!text) return '';
      // First, convert double line breaks to paragraph markers
      let html = text.replace(/\n\n+/g, '|||PARAGRAPH_BREAK|||');
      // Convert single line breaks to spaces (for line continuation within paragraphs)
      html = html.replace(/\n/g, ' ');
      // Convert **text** to <strong>text</strong>
      html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
      // Split by paragraph markers and wrap each paragraph
      const paragraphs = html.split('|||PARAGRAPH_BREAK|||')
        .map(para => para.trim())
        .filter(para => para);
      // Wrap each paragraph in <p> tags with spacing
      return paragraphs.map(para => `<p class="mb-3 last:mb-0">${para}</p>`).join('');
    };
    
    const personalityTextHTML = hasPersonalityBlock
      ? convertMarkdownToHTML(injectParagraphBreaksForPersonality(personalityTextRaw))
      : '';

    const visualProfile = profile?.personality?.visual_profile;
    const showVisualCarousel =
      flowFinderCompleted &&
      visualProfile &&
      isValidVisualProfileForPowerUp(visualProfile);
    const readFullRaw =
      showVisualCarousel ? pickReadFullNarrativeSource(profile, visualProfile) : '';
    const readFullProfileHTML =
      showVisualCarousel && readFullRaw && String(readFullRaw).trim()
        ? convertMarkdownToHTML(injectParagraphBreaksForPersonality(readFullRaw))
        : '';

    const personalityBundle = profile?.personality;
    const travelTypeMeaningPb = useMemo(
      () => buildTravelTypeMeaningStatements(visualProfile, visualCarouselExtraFb),
      [visualProfile, visualCarouselExtraFb]
    );

    const travelDnaPanelsPb = useMemo(
      () => buildTravelDnaPanels(visualProfile, visualCarouselExtraFb),
      [visualProfile, visualCarouselExtraFb]
    );

    const sylvanOutcomePb = useMemo(
      () => buildSylvanOutcomePanels(personalityBundle, visualProfile, visualCarouselExtraFb),
      [personalityBundle, visualProfile, visualCarouselExtraFb]
    );

    const visualCarouselSlides = useMemo(
      () => buildVisualCarouselSlideIds(visualProfile),
      [visualProfile]
    );
    visualCarouselSlidesRef.current = visualCarouselSlides;

    useEffect(() => {
      if (!visualCarouselSlides.length) return;
      setVisualCarouselPage((p) => Math.min(p, visualCarouselSlides.length - 1));
    }, [visualCarouselSlides.length, visualCarouselSlides.join(',')]);

    useEffect(() => {
      const el = visualCarouselScrollRef.current;
      if (!el || !visualCarouselSlides.length) return;
      const w = el.clientWidth;
      if (!w) return;
      const target = visualCarouselPage * w;
      if (Math.abs(el.scrollLeft - target) > 4) {
        const dist = Math.abs(el.scrollLeft - target);
        const behavior = dist > w * 1.25 ? 'auto' : 'smooth';
        el.scrollTo({ left: target, behavior });
      }
    }, [visualCarouselPage, visualCarouselSlides.length]);

    const ST =
      typeof window !== 'undefined' && window.SYLVAN_TECH_THEME ? window.SYLVAN_TECH_THEME : null;
    const dockScrollPad =
      ST?.classes?.dockScrollPaddingBottom ||
      'pb-[max(0.75rem,calc(9.25rem+env(safe-area-inset-bottom)))]';
    const topPlanRing =
      ST?.layout?.recoTopDefault || 'relative overflow-hidden rounded-2xl p-[2px]';
    const btnPrimaryFull =
      ST?.classes?.btnPrimaryFull ||
      'w-full min-h-[44px] rounded-xl font-semibold text-black bg-gradient-to-r from-teal-400 to-teal-300 shadow-[0_0_22px_-6px_rgba(45,226,197,0.65)] hover:brightness-110 active:brightness-95 disabled:opacity-40 disabled:cursor-not-allowed transition';

    const effectivePostPlanDetail =
      (postPlanIntro.detail && postPlanIntro.detail.trim()) ||
      POWERUP_POST_PLAN_INTRO_FALLBACK.detail;
    const effectivePostPlanHotkeys =
      (postPlanIntro.hotkeys && postPlanIntro.hotkeys.trim()) ||
      POWERUP_POST_PLAN_INTRO_FALLBACK.hotkeys;
    const postPlanDetailParas = effectivePostPlanDetail.split(/\n\n+/).map((p) => p.trim()).filter(Boolean);
    const postPlanHotkeyLines = effectivePostPlanHotkeys
      .split(/\n+/)
      .map((l) => l.trim())
      .filter(Boolean);
    const postPlanVideoSrc = (() => {
      const u = (postPlanIntro.videoUrl || '').trim();
      if (!u) return null;
      if (/^https?:\/\//i.test(u) || u.startsWith('/')) return u;
      return null;
    })();

    const needsProfile =
      !profile ||
      !(profile.name || profile.user_name) ||
      !profile.dob ||
      !profile.birth_place ||
      !profile.gender ||
      !profile.phone;

    const profileSetupPhase = needsProfile
      ? powerUpProfileLanguageConfirmed(profile)
        ? 'details'
        : 'language'
      : null;

    const lockedLanguageLabel =
      POWERUP_LANG_OPTIONS.find((o) => o.value === (profile?.preferred_language || formData.preferred_language))?.label ||
      profile?.preferred_language ||
      formData.preferred_language ||
      'en';

    useEffect(() => {
      if (!profileSetupPhase) return;
      if (tvPanel !== 'summary') setTvPanel('summary');
    }, [profileSetupPhase, tvPanel]);

    useEffect(() => {
      if (loading || introActive) return;
      if (!profile?.user_key) return;
      if (!needsProfile || profileSetupPhase) return;
      try {
        if (localStorage.getItem(`${FIRST_RUN_VISION_SEEN_PREFIX}${profile.user_key}`) === '1') return;
      } catch (_) {
        return;
      }
      setFirstRunVisionOpen(true);
    }, [loading, introActive, profile?.user_key, needsProfile, profileSetupPhase]);

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

    const beginLayoutTour = useCallback((force) => {
      if (force) {
        setIntroActive(false);
        setIntroFading(true);
      }
      setLayoutTourForce(!!force);
      setLayoutTourAwaitOpen(true);
    }, []);

    const closeLayoutTour = useCallback(
      (meta) => {
        const wasForced = layoutTourForce;
        setLayoutTourOpen(false);
        setLayoutTourForce(false);
        setLayoutTourAwaitOpen(false);
        const uk = profile?.user_key || (STS && STS.readUserKey());
        if (uk && STS && (meta?.dontShowAgain || !wasForced)) {
          STS.markPuLayoutTourAutoDone(uk);
        }
        setLayoutTourDontShow(false);
      },
      [profile?.user_key, layoutTourForce]
    );

    const layoutTourSteps = useMemo(
      () => [
        {
          resolveTarget: () => tourMenuRef.current,
          title: layoutTourLabels.menuTitle,
          body: layoutTourLabels.menuBody
        },
        {
          resolveTarget: () => tourTvRef.current,
          title: layoutTourLabels.tvTitle,
          body: layoutTourLabels.tvBody
        },
        {
          resolveTarget: () => tourHubRef.current,
          title: layoutTourLabels.hubTitle,
          body: layoutTourLabels.hubBody
        },
        {
          resolveTarget: () => tourTwinRef.current,
          title: layoutTourLabels.twinTitle,
          body: layoutTourLabels.twinBody
        }
      ],
      [layoutTourLabels]
    );

    useEffect(() => {
      let cancelled = false;
      const lang =
        profile?.preferred_language ||
        formData.preferred_language ||
        window.SylvanFlowState?.getLanguage?.() ||
        '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, menuTitle, menuBody, tvTitle, tvBody, hubTitle, hubBody, twinTitle, twinBody] =
          await Promise.all([
            load('ui:pu_tour_skip', 'Skip'),
            load('ui:pu_tour_next', 'Next'),
            load('ui:pu_tour_done', 'Done'),
            load('ui:pu_tour_dont_show_again', layoutTourLabels.dontShowAgain),
            load('ui:pu_tour_menu_title', 'Menu'),
            load('ui:pu_tour_menu_body', layoutTourLabels.menuBody),
            load('ui:pu_tour_tv_title', 'Your main screen'),
            load('ui:pu_tour_tv_body', layoutTourLabels.tvBody),
            load('ui:pu_tour_hub_title', 'Quick actions'),
            load('ui:pu_tour_hub_body', layoutTourLabels.hubBody),
            load('ui:pu_tour_twin_title', 'Move between apps'),
            load('ui:pu_tour_twin_body', layoutTourLabels.twinBody)
          ]);
        if (cancelled) return;
        setLayoutTourLabels({
          skip,
          next,
          done,
          dontShowAgain,
          menuTitle,
          menuBody,
          tvTitle,
          tvBody,
          hubTitle,
          hubBody,
          twinTitle,
          twinBody
        });
      })();
      return () => {
        cancelled = true;
      };
    }, [profile?.preferred_language, formData.preferred_language, profile?.user_key]);

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

    useEffect(() => {
      if (!layoutTourAwaitOpen || loading) return undefined;
      const t = window.setTimeout(() => {
        setLayoutTourOpen(true);
        setLayoutTourAwaitOpen(false);
      }, 150);
      return () => window.clearTimeout(t);
    }, [layoutTourAwaitOpen, loading]);

    useEffect(() => {
      if (introActive || loading || firstRunVisionOpen || layoutTourForce || profileSetupPhase) return;
      if (!STS || !profile?.user_key) return;
      if (STS.isPuLayoutTourAutoDone(profile.user_key)) return;
      if (needsProfile) return;
      if (!flowFinderCompleted) return;
      try {
        if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
          STS.markPuLayoutTourAutoDone(profile.user_key);
          return;
        }
      } catch (_) {}
      const t = window.setTimeout(() => setLayoutTourOpen(true), 400);
      return () => window.clearTimeout(t);
    }, [
      introActive,
      loading,
      firstRunVisionOpen,
      layoutTourForce,
      profile?.user_key,
      profile?.onboarding_state,
      needsProfile,
      profileSetupPhase,
      flowFinderCompleted
    ]);

    if (window.uiLogger) {
      window.uiLogger.uiLog('[POWERUP] profile gate (Power Up TV: language then details)', {
        needsProfile,
        hasProfile: !!profile,
        hasProfilePhotoUrl: !!profile?.profile_photo_url,
        hasProfilePhotoR2Key: !!profile?.profile_photo_r2_key
      });
    }

    const validatePhoneNumber = (phone) => {
      if (!phone) return { valid: false, error: "Phone number is required" };
      
      const cleaned = phone.replace(/\s+/g, "");
      
      if (!cleaned.startsWith("+")) {
        return { valid: false, error: "Phone number must start with country code (e.g., +1, +65, +86)" };
      }
      
      const digitsOnly = cleaned.substring(1);
      if (!/^\d+$/.test(digitsOnly)) {
        return { valid: false, error: "Phone number can only contain digits after the country code" };
      }
      
      if (cleaned.length < 8) {
        return { valid: false, error: "Phone number is too short. Please include country code and full number" };
      }
      if (cleaned.length > 18) {
        return { valid: false, error: "Phone number is too long. Please check your input" };
      }
      
      if (digitsOnly.length < 8) {
        return { valid: false, error: "Phone number appears incomplete. Please include full number with country code" };
      }
      
      return { valid: true, error: null };
    };

    const handleLanguageSubmit = async (e) => {
      try {
        if (e) e.preventDefault();
      } catch (_) {}
      const preferred_language = (formData.preferred_language || 'en').trim();
      try {
        setLangSubmitting(true);
        setError(null);
        const fd = new FormData();
        fd.append('preferred_language', preferred_language);
        fd.append('profile_write_mode', 'confirm_preferred_language');
        const res = await window.SylvanFlowAuth.authenticatedFetch(`${SF_API_BASE}/api/profile`, {
          method: 'POST',
          body: fd
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok || !data.ok) {
          const msg = data.error || data.message || data.data?.message || 'Could not save language. Please try again.';
          throw new Error(typeof msg === 'string' ? msg : 'Could not save language. Please try again.');
        }
        if (window.SylvanFlowState) {
          window.SylvanFlowState.setLanguage(preferred_language);
        }
        // SF_ALLOW_HARDEXIT_REDIRECT
        window.location.href = '/power-up';
      } catch (err) {
        setError(err.message || 'Could not save language. Please try again.');
      } finally {
        setLangSubmitting(false);
      }
    };

    const handleSubmit = async (e) => {
      // ✅ HARD INSTRUMENTATION: Log immediately with timestamp (admin-only, sanitized)
      if (window.uiLogger) {
        window.uiLogger.uiLog("[POWERUP] handleSubmit called", { hasEvent: !!e, ts: Date.now() });
      }
      
      // ✅ Safe preventDefault
      try { 
        if (e) {
          e.preventDefault?.();
        }
      } catch (err) {
        if (window.uiLogger) {
          window.uiLogger.uiWarn('[POWERUP] preventDefault failed:', err.message || err);
        }
      }
      
      // ✅ TEMP DEBUG: Log form submission attempt (admin-only, sanitized)
      if (window.uiLogger) {
        window.uiLogger.uiLog('[POWERUP] handleSubmit called', {
          formData: {
            hasName: !!formData.name,
            nameLength: formData.name?.length || 0,
            hasPhone: !!formData.phone,
            phoneLength: formData.phone?.length || 0,
            hasDob: !!formData.dob,
            hasBirthPlace: !!formData.birth_place,
            hasGender: !!formData.gender,
            preferred_language: formData.preferred_language
          },
          // ✅ FIX: Removed userId - identity comes from Bearer token only
        });
      }
      
      if (!formData.name || !formData.phone || !formData.dob || !formData.birth_place || !formData.gender) {
        if (window.uiLogger) {
          window.uiLogger.uiLog('[POWERUP] Validation failed - missing required fields');
        }
        setError("Please fill in all required fields");
        return;
      }

      const phoneValidation = validatePhoneNumber(formData.phone);
      if (!phoneValidation.valid) {
        setError(phoneValidation.error);
        return;
      }

      const hasExistingPhoto = !!(
        profile?.profile_photo_r2_key ||
        profile?.profile_photo_url ||
        profile?.photo_url
      );
      if (!formData.photo && !hasExistingPhoto) {
        setError('Please upload a profile photo to continue.');
        return;
      }

      try {
        setSubmitting(true);
        setError(null);
        
        // ✅ FIX: Track if profile was already complete BEFORE submission
        // This prevents redirect when user is only uploading a photo on an already-complete profile
        const wasProfileCompleteBefore = profile?.onboarding_state === 'first_itinerary_created' || 
                                        profile?.onboarding_state === 'profile_complete' ||
                                        (profile && !needsProfile);
        
        const formDataToSend = new FormData();
        formDataToSend.append("name", formData.name);
        formDataToSend.append("dob", formData.dob);
        formDataToSend.append("birth_place", formData.birth_place);
        formDataToSend.append("gender", formData.gender);
        formDataToSend.append("phone", formData.phone);
        formDataToSend.append("preferred_language", formData.preferred_language || "en");
        formDataToSend.append("profile_write_mode", "complete_profile");
        if (formData.photo) {
          formDataToSend.append("profile_photo", formData.photo);
        }

        // ✅ Log payload right before network call (admin-only, sanitized)
        if (window.uiLogger) {
          window.uiLogger.uiLog("[POWERUP] submitting payload", { 
            hasName: !!formData.name,
            nameLength: formData.name?.length || 0,
            hasPhone: !!formData.phone,
            phoneLength: formData.phone?.length || 0,
            hasDob: !!formData.dob,
            hasGender: !!formData.gender,
            hasBirthPlace: !!formData.birth_place,
            preferred_language: formData.preferred_language,
            hasPhoto: !!formData.photo,
            photoNameLength: formData.photo?.name?.length || 0,
            photoSize: formData.photo?.size || null,
            photoType: formData.photo?.type || null
          });
        }

        // ✅ FIX: Remove user_id from URL - backend derives identity from session token
        // Use /api/profile/me or /api/profile without user_id query param
        const profileUrl = `${SF_API_BASE}/api/profile`;
        if (window.uiLogger) {
          window.uiLogger.uiLog('[POWERUP] Submitting profile to:', profileUrl, '(identity from Bearer token)');
        }
        
        const res = await window.SylvanFlowAuth.authenticatedFetch(
          profileUrl,
          {
            method: "POST",
            body: formDataToSend
          }
        );
        
        if (window.uiLogger) {
          window.uiLogger.uiLog('[POWERUP] Profile submit response status:', res.status);
        }
        
        // Only handle auth response if not 2xx (success)
        if (res.status < 200 || res.status >= 300) {
          window.SylvanFlowAuth.handleAuthResponse(res);
        }

        if (!res.ok) {
          const errorData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
          throw new Error(errorData.error || "Failed to save profile");
        }

        const data = await res.json();
        if (window.uiLogger) {
          window.uiLogger.uiLog('[POWERUP] Profile submit response data:', {
            ok: data.ok,
            hasProfile: !!data.profile,
            hasDataProfile: !!data.data?.profile,
            code: data.code,
            state: data.state,
            hasError: !!data.error
          });
        }
        
        // ✅ FIX: Handle error responses (ERR_INTERNAL, ERR_VALIDATION, etc.)
        if (!data.ok) {
          const errorMessage = data.error || data.data?.message || data.reason || "Failed to save profile";
          if (window.uiLogger) {
            window.uiLogger.uiError('[POWERUP] Profile save failed:', {
              ok: data.ok,
              code: data.code,
              state: data.state,
              reason: data.reason,
              error: data.error,
              message: data.data?.message
            });
          }
          throw new Error(errorMessage);
        }

        if (data.photo_upload_failed) {
          const photoMsg =
            'Profile saved, but your photo could not be stored. Try again with a smaller JPEG or PNG, or check your connection.';
          setError(photoMsg);
          if (window.uiLogger) {
            window.uiLogger.uiWarn('[POWERUP] photo_upload_failed from API', {
              photo_upload_failed: true
            });
          }
        }
        
        // ✅ FIX: Handle both response structures:
        // 1. Direct: {ok: true, profile: {...}}
        // 2. Governed: {ok: true, data: {profile: {...}}}
        const responseProfile = data.profile || data.data?.profile;
        
        if (!responseProfile) {
          if (window.uiLogger) {
            window.uiLogger.uiError('[POWERUP] Invalid response structure - profile missing:', {
              ok: data.ok,
              hasProfile: !!data.profile,
              hasDataProfile: !!data.data?.profile
            });
          }
          throw new Error("Invalid response from server - profile not saved");
        }
        
        // ✅ FIX: Preserve onboarding_state from previous profile if response doesn't include it
        // The API response might not always include onboarding_state, so we preserve it from the existing profile
        const updatedProfile = {
          ...responseProfile,
          onboarding_state: responseProfile.onboarding_state || profile?.onboarding_state
        };
        
        setProfile(updatedProfile);
        if (window.SylvanFlowState) {
          window.SylvanFlowState.setProfile(updatedProfile);
        }
        
        // ✅ FIX: Profile is already updated from submit response, no need to reload
        // The profile from response is already the latest, and we've updated state store
        // Reloading would be redundant and could trigger unnecessary network calls
        
        // ✅ FIX: Only redirect to Flow Finder if profile transitioned from incomplete to complete
        // If profile was already complete (e.g., user just uploading a photo), stay on PowerUpAI
        const isProfileNowComplete = updatedProfile?.onboarding_state === 'first_itinerary_created' || 
                                     updatedProfile?.onboarding_state === 'profile_complete' ||
                                     (updatedProfile && updatedProfile.name && updatedProfile.dob && updatedProfile.birth_place && updatedProfile.gender && updatedProfile.phone);
        
        // ✅ FIX: Never redirect if profile was already complete before submission
        // This covers photo-only updates and any other updates to already-complete profiles
        const shouldRedirect = !wasProfileCompleteBefore && isProfileNowComplete;
        
        // ✅ FIX: Define isPhotoOnlyUpdate - tracks if only photo was updated (no other fields changed)
        const isPhotoOnlyUpdate = wasProfileCompleteBefore && 
                                  !!formData.photo && 
                                  formData.name === profile?.name &&
                                  formData.dob === profile?.dob &&
                                  formData.birth_place === profile?.birth_place &&
                                  formData.gender === profile?.gender &&
                                  formData.phone === profile?.phone &&
                                  formData.preferred_language === (profile?.preferred_language || 'en');
        
        if (window.uiLogger) {
          window.uiLogger.uiLog('[POWERUP] Redirect decision:', {
            wasProfileCompleteBefore,
            isProfileNowComplete,
            isPhotoOnlyUpdate,
            shouldRedirect,
            onboarding_state_before: profile?.onboarding_state,
            onboarding_state_after: updatedProfile?.onboarding_state,
            hasPhotoInForm: !!formData.photo,
            profileFieldsChanged: {
              name: formData.name !== profile?.name,
              dob: formData.dob !== profile?.dob,
              birth_place: formData.birth_place !== profile?.birth_place,
              gender: formData.gender !== profile?.gender,
              phone: formData.phone !== profile?.phone
            }
          });
        }
        
        if (shouldRedirect) {
          if (window.uiLogger) {
            window.uiLogger.uiLog('[POWERUP] Profile completed - opening Flow Finder in TV');
          }
          setTvPanel('flowFinder');
        } else {
          // Profile was already complete or only photo was updated - stay on PowerUpAI
          if (window.uiLogger) {
            window.uiLogger.uiLog('[POWERUP] Profile update complete - staying on PowerUpAI (was already complete or photo-only update)');
          }
          // Show success message or update UI as needed
        }
      } catch (e) {
        if (window.uiLogger) {
          window.uiLogger.uiError("Profile save error:", e.message || e);
        }
        setError(e.message || "Failed to save profile");
      } finally {
        setSubmitting(false);
      }
    };

    // Navigation helper - only for hard exit pages
    const navigate = (path) => {
      // Only allow navigation to hard exit pages
      const hardExitPages = ['/legal', '/flow-finder', '/flow-plan-form', '/flow-tune', '/micro-action'];
      if (hardExitPages.some(page => path.startsWith(page))) {
        // ✅ FIX: Do NOT add user_id to URL params (no URL identity in state-driven architecture)
        // Hard exit pages will derive identity from session token if needed
        // SF_ALLOW_HARDEXIT_REDIRECT
        window.location.href = path;
      } else {
        if (window.uiLogger) {
          window.uiLogger.uiWarn('[POWER-UP] Navigation to internal route blocked. Use appState.set() instead.');
        }
      }
    };

    const dismissPostPlanIntro = () => {
      try {
        localStorage.setItem(POWERUP_POST_PLAN_INTRO_SEEN_KEY, '1');
      } catch (_) {}
      setTvPanel('summary');
    };

    const handleHubSlotPress = (spec) => {
      if (!spec || !spec.key) return;
      setTvFfCompleteOverlay(null);
      if (tvOverlayTimerRef.current) {
        window.clearTimeout(tvOverlayTimerRef.current);
        tvOverlayTimerRef.current = null;
      }

      if (tvPanel === 'postPlanWelcome') {
        try {
          localStorage.setItem(POWERUP_POST_PLAN_INTRO_SEEN_KEY, '1');
        } catch (_) {}
      }

      if (spec.key === 'profile-summary') {
        setTvPanel('summary');
        /** Summary hotkey: jump personality carousel to slide 1 (travel-type). */
        if (showVisualCarousel && visualCarouselSlides.length > 0) {
          setVisualCarouselPage(0);
        }
        return;
      }
      if (spec.key === 'flow-finder') {
        if (needsProfile) {
          setError('Please complete your profile above before starting Flow Finder.');
          setTvPanel('summary');
          return;
        }
        if (flowFinderCompleted) {
          const lang =
            profile?.preferred_language ||
            formData.preferred_language ||
            (typeof window !== 'undefined' && window.SylvanFlowState?.getLanguage?.()) ||
            'en';
          Promise.resolve(
            typeof window.getSysMessage === 'function'
              ? window.getSysMessage('ui:powerup_flow_finder_already_complete', lang)
              : null
          ).then((msg) => {
            setTvFfCompleteOverlay(
              msg ||
                'Flow Finder is already complete — here is your profile summary.'
            );
            setTvPanel('summary');
            tvOverlayTimerRef.current = window.setTimeout(() => {
              setTvFfCompleteOverlay(null);
              tvOverlayTimerRef.current = null;
            }, POWERUP_FF_ALREADY_COMPLETE_OVERLAY_MS);
          });
          return;
        }
        setTvPanel('flowFinder');
        return;
      }
      if (spec.key === 'flow-plan') {
        void openFlowPlanTvIfAllowed();
        return;
      }
      if (spec.key === 'ask-sylvan') {
        setTvPanel('askSylvan');
      }
    };

    const usePlateEmbed =
      tvPanel === 'flowFinder' ||
      tvPanel === 'flowPlan' ||
      tvPanel === 'askSylvan';

    const layoutTourNode =
      SpotlightTourCmp && layoutTourOpen ? (
        <SpotlightTourCmp
          open={layoutTourOpen}
          steps={layoutTourSteps}
          labels={{
            skip: layoutTourLabels.skip,
            next: layoutTourLabels.next,
            done: layoutTourLabels.done,
            dontShowAgain: layoutTourLabels.dontShowAgain
          }}
          showDontShowAgain
          dontShowAgain={layoutTourDontShow}
          onDontShowAgainChange={setLayoutTourDontShow}
          onComplete={closeLayoutTour}
          onSkip={closeLayoutTour}
        />
      ) : null;

    // Gate on profile fetch only — sys-message/KV must not block (hang → infinite loading shell otherwise)
    if (loading) {
      return (
        <>
          {introActive ? <PowerUpIntroOverlay fading={introFading} /> : null}
          {layoutTourNode}
          <div className="mx-auto flex min-h-0 w-full max-w-[440px] flex-1 flex-col overflow-hidden bg-black [touch-action:manipulation]">
            <PowerUpMatchedHeader
              displayName={displayName}
              profilePhotoUrl={userAvatarUrl}
              headerTitle={uiStrings.headerTitle}
              headerSubtitle={uiStrings.headerSubtitle}
            />
            <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
              <div className="mx-auto min-h-full w-full pb-32 text-slate-100">
                <div className="px-3 sm:px-4 pt-3 pb-10">
                  <div className="py-12 text-center">
                    <p className="text-slate-400">Loading profile…</p>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <PowerUpBottomChrome navTwinNuAsk={uiStrings.navTwinNuAsk} navTwinExplore={uiStrings.navTwinExplore} />
        </>
      );
    }

    return (
      <>
        {introActive ? <PowerUpIntroOverlay fading={introFading} /> : null}
        {firstRunVisionOpen && FounderVision ? (
          <FounderVision
            allowBackdropDismiss={false}
            onDismiss={() => {
              try {
                if (profile?.user_key) {
                  localStorage.setItem(`${FIRST_RUN_VISION_SEEN_PREFIX}${profile.user_key}`, '1');
                }
              } catch (_) {}
              setFirstRunVisionOpen(false);
            }}
          />
        ) : null}
        <div className="mx-auto flex min-h-0 w-full max-w-[440px] flex-1 flex-col overflow-hidden bg-black [touch-action:manipulation]">
          <PowerUpMatchedHeader
            displayName={displayName}
            profilePhotoUrl={userAvatarUrl}
            headerTitle={uiStrings.headerTitle}
            headerSubtitle={uiStrings.headerSubtitle}
            menuButtonRef={tourMenuRef}
            tourActive={layoutTourOpen}
            onTourDismiss={closeLayoutTour}
          />
          <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
            <div className="mx-auto flex min-h-0 w-full flex-1 flex-col overflow-hidden text-slate-100">
              <div className={`flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-y-contain px-3 sm:px-4 pt-2 sm:pt-3 ${dockScrollPad}`}>
          {/* Body */}
            {/* Section 1: Welcome — name + greeting centered H+V in strip under header */}
            <div className="flex w-full min-w-0 shrink-0 flex-col items-center justify-center py-3 text-center">
              <div className="flex w-full max-w-full flex-row flex-wrap items-center justify-center content-center gap-x-2.5 gap-y-1 sm:gap-x-3">
                <h1 className="min-w-0 text-xl font-semibold text-white text-balance break-words sm:text-2xl">
                  {displayName}
                </h1>
                {uiStrings.welcomeToSylvanFlow ? (
                  <>
                    <span className="select-none shrink-0 text-slate-600 sm:text-base" aria-hidden>
                      ·
                    </span>
                    <p className="min-w-0 max-w-full text-sm leading-snug text-slate-400 sm:max-w-[min(100%,20rem)]">
                      {uiStrings.welcomeToSylvanFlow}
                    </p>
                  </>
                ) : null}
              </div>
            </div>

            <div className="border-t border-white/10 mt-0 mb-1.5 shrink-0" />

            {/* Section 2: TV — flex-1; “Sylvan sees” label lives inside rim for summary (item 8) */}
            <div className="flex min-h-0 flex-1 flex-col">

              {error && (
                <p className="text-sm text-red-500 mb-2 shrink-0">{error}</p>
              )}

              {!error && (
                <div className="flex min-h-0 flex-1 flex-col pt-2">
                  <div
                    ref={tourTvRef}
                    className={`${usePlateEmbed ? POWERUP_TV_LAYOUT.rimFill : POWERUP_TV_LAYOUT.rimSummaryGrow} ${topPlanRing}`}
                    style={{
                      background: POWERUP_TV_RIM_GRADIENT
                    }}
                  >
                    <div
                      className={
                        usePlateEmbed ? POWERUP_TV_LAYOUT.plateEmbed : POWERUP_TV_LAYOUT.plateSummary
                      }
                      style={{ boxShadow: POWERUP_TV_PLATE_BOX_SHADOW }}
                    >
                      {/* Neutral glass washes — inside plate (same layout vocabulary as hub tiles, no teal rim). */}
                      <div
                        className="pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[13px]"
                        aria-hidden
                      >
                        <div
                          className="absolute inset-x-0 bottom-0 h-[42%] opacity-[0.28]"
                          style={{
                            background: `linear-gradient(to top, ${POWERUP_TV_PLATE_GLOW}, transparent)`
                          }}
                        />
                        <div
                          className="absolute inset-x-0 bottom-0 h-px opacity-[0.82]"
                          style={{
                            background: `linear-gradient(90deg, transparent, ${POWERUP_TV_PLATE_GLOW}, transparent)`
                          }}
                        />
                        <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/40 to-transparent opacity-[0.85]" />
                      </div>
                      {!usePlateEmbed && tvPanel === 'summary' ? (
                        <div className="flex min-h-[2.25rem] shrink-0 items-center justify-center border-b border-white/[0.06] px-4 py-2 sm:min-h-[2.5rem] sm:px-5">
                          <div className="w-full text-center text-[10px] font-semibold leading-tight tracking-[0.08em] bg-gradient-to-r from-emerald-400 via-teal-300 to-violet-400 bg-clip-text text-transparent">
                            {uiStrings.heresWhatSylvanSees}
                          </div>
                        </div>
                      ) : null}
                      {tvPanel === 'postPlanWelcome' ? (
                        <div
                          dir="auto"
                          className={`relative flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-y-contain p-4 sm:p-5 ${POWERUP_TV_BOUNDED_MAX}`}
                        >
                          <div className="mb-4 flex items-start gap-3">
                            <div
                              className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-white/15 bg-gradient-to-br from-fuchsia-500/30 to-violet-600/25 text-lg font-semibold text-white shadow-[0_0_24px_-8px_rgba(168,85,247,0.45)]"
                              aria-hidden
                            >
                              ✓
                            </div>
                            <div className="min-w-0">
                              <h2 className="text-base font-semibold leading-snug text-white sm:text-lg">
                                {postPlanIntro.title || POWERUP_POST_PLAN_INTRO_FALLBACK.title}
                              </h2>
                              <p className="mt-1 text-xs leading-relaxed text-slate-300 sm:text-sm">
                                {postPlanIntro.lead || POWERUP_POST_PLAN_INTRO_FALLBACK.lead}
                              </p>
                            </div>
                          </div>
                          {postPlanVideoSrc ? (
                            <div className="mb-4 overflow-hidden rounded-xl border border-white/10 bg-black/40 shadow-inner">
                              <video
                                key={postPlanVideoSrc}
                                className={POWERUP_TV_LAYOUT.postPlanVideoMax}
                                controls
                                playsInline
                                preload="metadata"
                                src={postPlanVideoSrc}
                              />
                            </div>
                          ) : null}
                          <div className="space-y-3 text-sm leading-relaxed text-slate-200">
                            {postPlanDetailParas.map((para, i) => (
                              <p
                                key={i}
                                className={
                                  i === 0
                                    ? ''
                                    : 'text-xs leading-relaxed text-slate-400 sm:text-sm'
                                }
                              >
                                {para}
                              </p>
                            ))}
                          </div>
                          {postPlanHotkeyLines.length ? (
                            <div className="mt-5 space-y-2">
                              {postPlanHotkeyLines.map((line, i) => {
                                const accents = [
                                  'rgba(34,197,94,0.55)',
                                  'rgba(59,130,246,0.55)',
                                  'rgba(168,85,247,0.55)',
                                  'rgba(248,130,18,0.55)'
                                ];
                                return (
                                  <div
                                    key={i}
                                    className="rounded-lg border border-white/[0.06] bg-white/[0.03] px-3 py-2 text-xs leading-snug text-slate-300 sm:text-sm"
                                    style={{
                                      borderLeftWidth: 3,
                                      borderLeftColor: accents[i % accents.length]
                                    }}
                                  >
                                    {line}
                                  </div>
                                );
                              })}
                            </div>
                          ) : null}
                          <button
                            type="button"
                            className={btnPrimaryFull + ' mt-6'}
                            onClick={dismissPostPlanIntro}
                          >
                            {postPlanIntro.continueCta || POWERUP_POST_PLAN_INTRO_FALLBACK.continueCta}
                          </button>
                        </div>
                      ) : tvPanel === 'flowFinder' ? (
                        <iframe
                          title="Flow Finder"
                          src={flowFinderIframeSrc}
                          className="min-h-0 w-full flex-1 border-0 bg-[#070a0c]"
                        />
                      ) : tvPanel === 'flowPlan' ? (
                        <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
                          <div className={TV_EMBED.row}>
                            <button
                              type="button"
                              className={TV_EMBED.backBtn}
                              onClick={() => setTvPanel('summary')}
                            >
                              ← {tvStrings.flowPlanBackLabel || tvChromeFbInline.flowPlanBackLabel}
                            </button>
                            <span className={TV_EMBED.title}>
                              {tvStrings.flowPlanChromeTitle || tvChromeFbInline.flowPlanChromeTitle}
                            </span>
                            <span className={TV_EMBED.spacer} aria-hidden />
                          </div>
                          <iframe
                            title="Create Flow Plan"
                            src={flowPlanFormSrc}
                            className="min-h-0 w-full flex-1 border-0 bg-[#070a0c]"
                          />
                        </div>
                      ) : tvPanel === 'askSylvan' && typeof window.AskSylvanPage === 'function' ? (
                        <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
                          <div className={TV_EMBED.row}>
                            <button
                              type="button"
                              className={TV_EMBED.backBtn}
                              onClick={() => setTvPanel('summary')}
                            >
                              ← {tvStrings.flowPlanBackLabel || tvChromeFbInline.flowPlanBackLabel}
                            </button>
                            <span className={TV_EMBED.title}>
                              {tvStrings.askSylvanChromeTitle || tvChromeFbInline.askSylvanChromeTitle}
                            </span>
                            <span className={TV_EMBED.spacer} aria-hidden />
                          </div>
                          <div className="flex min-h-0 flex-1 flex-col overflow-hidden px-1 pt-1">
                            {createElement(window.AskSylvanPage, {
                              embeddedExploreTv: true,
                              embeddedPowerUpTv: true
                            })}
                          </div>
                        </div>
                      ) : tvPanel === 'askSylvan' ? (
                        <div className="flex flex-1 flex-col items-center justify-center px-4 text-center text-sm text-slate-400">
                          Ask Sylvan is not available.
                        </div>
                      ) : (
                        <div
                          dir="auto"
                          className={`relative flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-y-contain p-4 sm:p-5 ${POWERUP_TV_BOUNDED_MAX}`}
                        >
                          {profileSetupPhase === 'language' ? (
                            <div className="mx-auto w-full max-w-sm space-y-4 py-4 text-left">
                              <h2 className="text-base font-semibold text-white">{profileFormUi.langTitle}</h2>
                              <p className="text-xs leading-relaxed text-slate-400">{profileFormUi.langBody}</p>
                              <form onSubmit={handleLanguageSubmit} className="space-y-4">
                                <div>
                                  <label htmlFor="pu-profile-lang" className="mb-1 block text-xs font-medium text-slate-300">
                                    {profileFormUi.langLabel} <span className="text-red-400">*</span>
                                  </label>
                                  <select
                                    id="pu-profile-lang"
                                    required
                                    value={formData.preferred_language || 'en'}
                                    onChange={(ev) =>
                                      setFormData((prev) => ({ ...prev, preferred_language: ev.target.value }))
                                    }
                                    className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-sm text-white"
                                  >
                                    {POWERUP_LANG_OPTIONS.map((opt) => (
                                      <option key={opt.value} value={opt.value}>
                                        {opt.label}
                                      </option>
                                    ))}
                                  </select>
                                </div>
                                <button type="submit" disabled={langSubmitting} className={btnPrimaryFull}>
                                  {langSubmitting ? profileFormUi.langSaving : profileFormUi.langContinue}
                                </button>
                              </form>
                            </div>
                          ) : profileSetupPhase === 'details' ? (
                            <div className="mx-auto w-full max-w-sm space-y-3 py-2 text-left">
                              <h2 className="text-base font-semibold text-white">{profileFormUi.detailsTitle}</h2>
                              <p className="text-xs leading-relaxed text-slate-400">{profileFormUi.detailsBody}</p>
                              <p className="rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-slate-300">
                                {profileFormUi.languageLockedPrefix}{' '}
                                <span className="font-medium text-white">{lockedLanguageLabel}</span>
                              </p>
                              <form onSubmit={handleSubmit} className="space-y-3">
                                <div>
                                  <label htmlFor="pu-profile-name" className="mb-1 block text-xs font-medium text-slate-300">
                                    {profileFormUi.labelName} <span className="text-red-400">*</span>
                                  </label>
                                  <input
                                    id="pu-profile-name"
                                    type="text"
                                    required
                                    autoComplete="name"
                                    value={formData.name}
                                    onChange={(ev) => setFormData((prev) => ({ ...prev, name: ev.target.value }))}
                                    className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-sm text-white"
                                  />
                                </div>
                                <div>
                                  <label htmlFor="pu-profile-phone" className="mb-1 block text-xs font-medium text-slate-300">
                                    {profileFormUi.labelPhone} <span className="text-red-400">*</span>
                                  </label>
                                  <input
                                    id="pu-profile-phone"
                                    type="tel"
                                    required
                                    autoComplete="tel"
                                    placeholder={profileFormUi.phonePlaceholder}
                                    value={formData.phone}
                                    onChange={(ev) => setFormData((prev) => ({ ...prev, phone: ev.target.value }))}
                                    className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-sm text-white"
                                  />
                                </div>
                                <div>
                                  <label htmlFor="pu-profile-dob" className="mb-1 block text-xs font-medium text-slate-300">
                                    {profileFormUi.labelDob} <span className="text-red-400">*</span>
                                  </label>
                                  <input
                                    id="pu-profile-dob"
                                    type="date"
                                    required
                                    value={formData.dob}
                                    onChange={(ev) => setFormData((prev) => ({ ...prev, dob: ev.target.value }))}
                                    className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-sm text-white"
                                  />
                                </div>
                                <div>
                                  <label htmlFor="pu-profile-birth-place" className="mb-1 block text-xs font-medium text-slate-300">
                                    {profileFormUi.labelBirthPlace} <span className="text-red-400">*</span>
                                  </label>
                                  <select
                                    id="pu-profile-birth-place"
                                    required
                                    value={formData.birth_place}
                                    onChange={(ev) =>
                                      setFormData((prev) => ({ ...prev, birth_place: ev.target.value }))
                                    }
                                    className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-sm text-white"
                                  >
                                    <option value="">{profileFormUi.selectCountry}</option>
                                    {POWERUP_PROFILE_COUNTRIES.map((c) => (
                                      <option key={c} value={c}>
                                        {c}
                                      </option>
                                    ))}
                                  </select>
                                </div>
                                <div>
                                  <label htmlFor="pu-profile-gender" className="mb-1 block text-xs font-medium text-slate-300">
                                    {profileFormUi.labelGender} <span className="text-red-400">*</span>
                                  </label>
                                  <select
                                    id="pu-profile-gender"
                                    required
                                    value={formData.gender}
                                    onChange={(ev) => setFormData((prev) => ({ ...prev, gender: ev.target.value }))}
                                    className="w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2.5 text-sm text-white"
                                  >
                                    <option value="">{profileFormUi.selectOption}</option>
                                    <option value="Male">{profileFormUi.genderMale}</option>
                                    <option value="Female">{profileFormUi.genderFemale}</option>
                                    <option value="Other">{profileFormUi.genderOther}</option>
                                    <option value="Prefer not to say">{profileFormUi.genderPreferNot}</option>
                                  </select>
                                </div>
                                <div>
                                  <label htmlFor="pu-profile-photo" className="mb-1 block text-xs font-medium text-slate-300">
                                    {profileFormUi.labelPhoto} <span className="text-red-400">*</span>
                                  </label>
                                  <input
                                    id="pu-profile-photo"
                                    type="file"
                                    accept="image/*"
                                    onChange={(ev) =>
                                      setFormData((prev) => ({
                                        ...prev,
                                        photo: ev.target.files && ev.target.files[0] ? ev.target.files[0] : null
                                      }))
                                    }
                                    className="w-full text-xs text-slate-300 file:mr-2 file:rounded file:border-0 file:bg-teal-500/20 file:px-2 file:py-1 file:text-teal-200"
                                  />
                                  <p className="mt-1 text-[11px] text-slate-500">{profileFormUi.photoHint}</p>
                                </div>
                                <button type="submit" disabled={submitting} className={btnPrimaryFull}>
                                  {submitting ? profileFormUi.submitSaving : profileFormUi.submit}
                                </button>
                              </form>
                            </div>
                          ) : !flowFinderCompleted && !layoutTourOpen ? (
                            <div className="flex flex-col items-center justify-center px-3 py-14 text-center">
                              {(tvStrings.prereq ||
                                'Tap the blue Flow Finder tile below — your quiz opens in this screen; your summary appears here when you\'re done.')
                                .split(/\n\n+/)
                                .filter(Boolean)
                                .map((para, i) => (
                                  <p
                                    key={i}
                                    className={
                                      i === 0
                                        ? 'max-w-sm text-sm leading-relaxed text-slate-200'
                                        : 'max-w-sm mt-2 text-xs leading-relaxed text-slate-400'
                                    }
                                  >
                                    {para.trim()}
                                  </p>
                                ))}
                            </div>
                          ) : flowFinderCompleted && !showVisualCarousel && !hasPersonalityBlock ? (
                            <div className="flex flex-col items-center justify-center gap-4 px-3 py-14 text-center">
                              <div
                                className="h-10 w-10 shrink-0 animate-spin rounded-full border-2 border-[#2DE2C5]/25 border-t-[#2DE2C5] motion-reduce:animate-none"
                                aria-hidden
                              />
                              <p className="text-sm text-slate-300">
                                {tvStrings.loading || 'Preparing your profile summary…'}
                              </p>
                            </div>
                          ) : showVisualCarousel ? (
                            <div className="flex min-h-0 flex-1 flex-col gap-1">
                              <div className="relative flex min-h-0 flex-1 flex-col px-0">
                                <div
                                  ref={visualCarouselScrollRef}
                                  onScroll={onVisualCarouselScroll}
                                  onTouchStart={onVisualCarouselTouchStart}
                                  onTouchEnd={onVisualCarouselTouchEnd}
                                  className="flex min-h-0 w-full flex-1 snap-x snap-mandatory overflow-x-auto overflow-y-hidden overscroll-x-contain scroll-smooth touch-pan-x"
                                  role="region"
                                  aria-roledescription="carousel"
                                  aria-label="Profile summary. Swipe horizontally to change slide; on the first or last slide, swipe again to wrap around."
                                  style={{ WebkitOverflowScrolling: 'touch' }}
                                >
                                  {visualCarouselSlides.map((slideId, vcIdx) => (
                                    <div
                                      key={`powerup-vc-${vcIdx}-${slideId}`}
                                      className="relative box-border flex h-full min-h-0 w-full min-w-full max-w-full shrink-0 snap-center flex-col gap-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-2.5 pb-1 sm:px-3"
                                    >
                                      <div
                                        className={`pointer-events-none absolute inset-0 z-0 ${
                                          slideId === 'travel-type'
                                            ? 'opacity-[0.48]'
                                            : 'opacity-[0.34]'
                                        } ${powerupVcSlideAmbientClass(vcIdx)}`}
                                        aria-hidden
                                      />
                                      {renderPowerUpSlideImageBackdrop(
                                        powerupVcBackdropVariantForSlide(slideId),
                                        visualProfile?.archetype,
                                        slideId
                                      )}
                                      <div className="relative z-[2] flex min-h-0 w-full flex-1 flex-col gap-1">
                                    {slideId === 'travel-type' ? (
                                      <div className="flex min-h-0 flex-1 flex-col justify-center gap-3">
                                        <div className="relative overflow-hidden rounded-xl border border-white/15 bg-gradient-to-br from-emerald-950/50 via-[#060908]/78 to-violet-950/50 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.05)] ring-1 ring-white/[0.08] backdrop-blur-[0.5px]">
                                          <div className="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden>
                                            <img
                                              src={resolvePowerUpMomentAssetUrl(POWERUP_MOMENT_TRAVEL_TYPE_INNER_ARCHETYPE)}
                                              alt=""
                                              decoding="async"
                                              className="h-full w-full object-cover opacity-[0.22]"
                                              onError={powerupBackdropImgOnError}
                                            />
                                            <div
                                              className="absolute inset-0 bg-gradient-to-b from-[#030806]/78 via-[#060908]/85 to-[#070a0c]/90"
                                              aria-hidden
                                            />
                                          </div>
                                          <div className="relative z-[1] p-2.5">
                                            <p
                                              className={`text-[10px] font-semibold uppercase tracking-[0.14em] ${visualCarouselSlideTitleClass(slideId)}`}
                                            >
                                              {visualCarouselExtraFb.travelTypeTitle}
                                            </p>
                                            <h3 className="mt-1 bg-gradient-to-r from-emerald-300 via-cyan-300 to-violet-400 bg-clip-text text-lg font-semibold leading-snug text-transparent">
                                              {visualProfile.archetype}
                                            </h3>
                                            <p className="mt-1 text-sm leading-relaxed text-slate-100 drop-shadow-[0_1px_8px_rgba(0,0,0,0.75)]">
                                              {visualProfile.one_liner}
                                            </p>
                                          </div>
                                        </div>
                                        <div className="relative overflow-hidden rounded-xl border border-white/10 bg-white/[0.04] ring-1 ring-white/[0.06]">
                                          <div className="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden>
                                            <img
                                              src={resolvePowerUpMomentAssetUrl(POWERUP_MOMENT_TRAVEL_TYPE_INNER_MEANING)}
                                              alt=""
                                              decoding="async"
                                              className="h-full w-full object-cover opacity-[0.18]"
                                              onError={powerupBackdropImgOnError}
                                            />
                                            <div
                                              className="absolute inset-0 bg-gradient-to-b from-[#030806]/88 via-[#070a0c]/92 to-[#070a0c]/94"
                                              aria-hidden
                                            />
                                          </div>
                                          <div className="relative z-[1] p-2.5">
                                          <p
                                            className={`text-[9px] font-semibold uppercase tracking-wide ${visualCarouselSlideTitleClass('travel-type')} drop-shadow-[0_1px_6px_rgba(0,0,0,0.75)]`}
                                          >
                                            {visualCarouselExtraFb.travelTypeMeaningTitle}
                                          </p>
                                          <ul className="mt-2 list-inside list-disc space-y-2 text-[11px] leading-relaxed text-slate-100 [text-shadow:0_1px_6px_rgba(0,0,0,0.85)]">
                                            {travelTypeMeaningPb.map((line, mi) => (
                                              <li key={`tt-mean-${mi}`} className="break-words pl-0.5 marker:text-teal-300/90">
                                                {line}
                                              </li>
                                            ))}
                                          </ul>
                                          <p className="mt-3 border-t border-white/10 pt-3 text-[11px] leading-relaxed text-slate-300 [text-shadow:0_1px_5px_rgba(0,0,0,0.8)]">
                                            {visualCarouselExtraFb.travelTypePracticalLine}
                                          </p>
                                        </div>
                                      </div>
                                      </div>
                                    ) : null}
                                    {slideId === 'travel-dna' ? (
                                      <div className="flex min-h-0 w-full flex-1 flex-col justify-center">
                                        <div className="w-full space-y-2.5 pr-0.5">
                                        <p
                                          className={`text-[10px] font-semibold uppercase tracking-[0.14em] ${visualCarouselSlideTitleClass(slideId)}`}
                                        >
                                          {visualCarouselExtraFb.travelDnaSlideTitle}
                                        </p>
                                        {(() => {
                                          const g = travelDnaPanelsPb;
                                          const hasAny =
                                            (g.valued && g.valued.length) ||
                                            (g.draining && g.draining.length) ||
                                            (g.rewarding && g.rewarding.length);
                                          if (!hasAny) {
                                            return (
                                              <p className="text-[11px] leading-relaxed text-slate-500">
                                                {visualCarouselExtraFb.dnaEmpty}
                                              </p>
                                            );
                                          }
                                          const panel = (title, sentences, tone) =>
                                            sentences && sentences.length ? (
                                              <div className="rounded-xl border border-white/10 bg-white/[0.03] p-2.5 ring-1 ring-white/[0.06]">
                                                <p className={`text-[9px] font-semibold uppercase tracking-wide ${tone}`}>{title}</p>
                                                <div className="mt-2 space-y-2">
                                                  {sentences.map((para, pi) => (
                                                    <p key={`${title}-${pi}`} className="text-[11px] leading-relaxed text-slate-200/95">
                                                      {para}
                                                    </p>
                                                  ))}
                                                </div>
                                              </div>
                                            ) : null;
                                          return (
                                            <div className="space-y-2">
                                              {panel(
                                                visualCarouselExtraFb.dnaGroupValued,
                                                g.valued,
                                                'text-emerald-200/90'
                                              )}
                                              {panel(
                                                visualCarouselExtraFb.dnaGroupDraining,
                                                g.draining,
                                                'text-amber-200/90'
                                              )}
                                              {panel(
                                                visualCarouselExtraFb.dnaGroupRewarding,
                                                g.rewarding,
                                                'text-violet-200/90'
                                              )}
                                            </div>
                                          );
                                        })()}
                                        </div>
                                      </div>
                                    ) : null}
                                    {slideId === 'flow-shape' ? (
                                      <div className="flex min-h-0 w-full flex-1 flex-col justify-center">
                                        <div className="w-full space-y-2">
                                        <p
                                          className={`text-[10px] font-semibold uppercase tracking-[0.14em] ${visualCarouselSlideTitleClass(slideId)}`}
                                        >
                                          {visualCarouselUi.flowShapeTitle}
                                        </p>
                                        {(() => {
                                          const scoreKeys = [
                                            'pace',
                                            'novelty',
                                            'comfort',
                                            'culture_depth',
                                            'flexibility'
                                          ];
                                          const domKey = getDominantScoreKey(visualProfile.scores);
                                          const weakKey = getWeakestScoreKey(visualProfile.scores);
                                          const tripLine = buildTripSatisfactionInterpretation(
                                            visualProfile.scores,
                                            (k) => resolvedVisualScoreLabels[k] || k,
                                            visualCarouselExtraFb
                                          );
                                          return (
                                            <>
                                              <div className="flex flex-wrap gap-2">
                                                <span className="inline-flex rounded-full border border-violet-400/35 bg-violet-500/15 px-2.5 py-1 text-[9px] font-semibold uppercase tracking-wide text-violet-100">
                                                  {visualCarouselExtraFb.strongestSignalPrefix}:{' '}
                                                  {resolvedVisualScoreLabels[domKey] || domKey}
                                                </span>
                                                <span className="inline-flex rounded-full border border-amber-400/35 bg-amber-500/15 px-2.5 py-1 text-[9px] font-semibold uppercase tracking-wide text-amber-100">
                                                  {visualCarouselExtraFb.weakestSignalPrefix}:{' '}
                                                  {resolvedVisualScoreLabels[weakKey] || weakKey}
                                                </span>
                                              </div>
                                              <div className="grid min-h-0 grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)] items-start gap-2 sm:gap-3">
                                                <div className="min-w-0 space-y-1">
                                                  {scoreKeys.map((sk) => {
                                                    const val = visualProfile.scores && visualProfile.scores[sk];
                                                    const pct =
                                                      typeof val === 'number'
                                                        ? Math.max(0, Math.min(100, val))
                                                        : parseInt(String(val), 10) || 0;
                                                    return (
                                                      <div key={sk}>
                                                        <div className="mb-0.5 flex justify-between gap-1 text-[9px] text-slate-400">
                                                          <span className="min-w-0 truncate">
                                                            {resolvedVisualScoreLabels[sk] || sk}
                                                          </span>
                                                          <span className="shrink-0 tabular-nums text-slate-200">
                                                            {pct}
                                                          </span>
                                                        </div>
                                                        <div className="h-1.5 overflow-hidden rounded-full bg-white/10 ring-1 ring-white/[0.06]">
                                                          <div
                                                            className={`h-full rounded-full ${getVisualScoreBarClass(sk)}`}
                                                            style={{ width: `${pct}%` }}
                                                          />
                                                        </div>
                                                      </div>
                                                    );
                                                  })}
                                                </div>
                                                <div className="min-w-0 self-center">
                                                  <PowerUpVisualFlowRadar
                                                    scores={visualProfile.scores}
                                                    labelForKey={(k) => resolvedVisualScoreLabels[k] || k}
                                                  />
                                                </div>
                                              </div>
                                              <p className="text-[11px] leading-relaxed text-slate-300">{tripLine}</p>
                                            </>
                                          );
                                        })()}
                                        </div>
                                      </div>
                                    ) : null}
                                    {slideId === 'sylvan-selection-logic' ? (
                                      <div className="flex min-h-0 w-full flex-1 flex-col justify-center">
                                        <div className="w-full space-y-2.5 pr-0.5">
                                        <p
                                          className={`text-[10px] font-semibold uppercase tracking-[0.14em] ${visualCarouselSlideTitleClass(slideId)}`}
                                        >
                                          {visualCarouselExtraFb.sylvanSelectionTitle}
                                        </p>
                                        <div className="space-y-2">
                                          <div className="rounded-xl border border-teal-400/25 bg-teal-950/25 p-2.5 ring-1 ring-white/[0.06]">
                                            <p className="text-[9px] font-semibold uppercase tracking-wide text-teal-100/95">
                                            {visualCarouselExtraFb.svSectionRetain}
                                          </p>
                                            {sylvanOutcomePb.retain.length ? (
                                              <ul className="mt-2 list-inside list-disc space-y-1 text-[11px] leading-snug text-slate-200">
                                                {sylvanOutcomePb.retain.map((line, i) => (
                                                  <li key={`pri-${i}`} className="break-words pl-0.5">
                                                    {line}
                                                  </li>
                                                ))}
                                              </ul>
                                            ) : (
                                              <p className="mt-2 text-[11px] text-slate-500">{visualCarouselExtraFb.svEmptyPrioritize}</p>
                                            )}
                                          </div>
                                          <div className="rounded-xl border border-orange-400/30 bg-orange-950/20 p-2.5 ring-1 ring-white/[0.06]">
                                            <p className="text-[9px] font-semibold uppercase tracking-wide text-orange-100/95">
                                            {visualCarouselExtraFb.svSectionCautious}
                                          </p>
                                            {sylvanOutcomePb.cautious.length ? (
                                              <ul className="mt-2 list-inside list-disc space-y-1 text-[11px] leading-snug text-slate-200">
                                                {sylvanOutcomePb.cautious.map((line, i) => (
                                                  <li key={`dep-${i}`} className="break-words pl-0.5">
                                                    {line}
                                                  </li>
                                                ))}
                                              </ul>
                                            ) : (
                                              <p className="mt-2 text-[11px] text-slate-500">{visualCarouselExtraFb.svEmptyDeprioritize}</p>
                                            )}
                                          </div>
                                          <div className="rounded-xl border border-violet-400/30 bg-violet-950/25 p-2.5 ring-1 ring-white/[0.06]">
                                            <p className="text-[9px] font-semibold uppercase tracking-wide text-violet-100/95">
                                            {visualCarouselExtraFb.svSectionBalance}
                                          </p>
                                            {sylvanOutcomePb.balance.length ? (
                                              <ul className="mt-2 list-inside list-disc space-y-1 text-[11px] leading-snug text-slate-200">
                                                {sylvanOutcomePb.balance.map((line, i) => (
                                                  <li key={`must-${i}`} className="break-words pl-0.5">
                                                    {line}
                                                  </li>
                                                ))}
                                              </ul>
                                            ) : (
                                              <p className="mt-2 text-[11px] text-slate-500">{visualCarouselExtraFb.svEmptyMustRespect}</p>
                                            )}
                                          </div>
                                        </div>
                                        </div>
                                      </div>
                                    ) : null}
                                    {slideId === 'best-modes' ? (
                                      <div className="flex min-h-0 w-full flex-1 flex-col justify-start">
                                        <div className="w-full space-y-2">
                                        <p
                                          className={`text-[10px] font-semibold uppercase tracking-[0.14em] ${visualCarouselSlideTitleClass(slideId)}`}
                                        >
                                          {visualCarouselUi.bestModesTitle}
                                        </p>
                                        <div className="space-y-2.5">
                                          {(visualProfile.modes || []).slice(0, 3).map((mode, idx) => {
                                            const mv =
                                              typeof mode.value === 'number'
                                                ? mode.value
                                                : parseInt(String(mode.value), 10) || 0;
                                            const pct = Math.max(0, Math.min(100, mv));
                                            const modeStyle = getModeBarAndDotClasses(mode.label);
                                            const rawDesc =
                                              mode.desc && String(mode.desc).trim()
                                                ? String(mode.desc).trim()
                                                : '';
                                            const whenFit = safePublicProfileSentence(
                                              rawDesc,
                                              interpolateDashboardTemplate(
                                                visualCarouselExtraFb.modeWhenFitFallback,
                                                { mode: mode.label }
                                              )
                                            );
                                            const rhythmLine = visualCarouselExtraFb.modeRhythmBody;
                                            const experienceLine = visualCarouselExtraFb.modeExperienceBody;
                                            return (
                                              <div
                                                key={idx}
                                                className="rounded-xl border border-white/10 bg-white/[0.04] p-2.5 ring-1 ring-white/[0.06]"
                                              >
                                                <div className="flex justify-between gap-2 text-[12px]">
                                                  <span className="flex min-w-0 items-start gap-2 font-semibold text-slate-50">
                                                    <span
                                                      className={`mt-0.5 h-2.5 w-2.5 shrink-0 rounded-full ${modeStyle.dot}`}
                                                      aria-hidden
                                                    />
                                                    <span className="min-w-0 leading-snug">{mode.label}</span>
                                                  </span>
                                                  <span className="shrink-0 text-sm tabular-nums font-semibold text-teal-200/95">
                                                    {pct}
                                                  </span>
                                                </div>
                                                <p className="mt-1.5 pl-4 text-[9px] font-semibold uppercase tracking-wide text-slate-500">
                                                  {visualCarouselExtraFb.whenThisFits}
                                                </p>
                                                <p className="mt-0.5 line-clamp-4 pl-4 text-[11px] leading-snug text-slate-400">
                                                  {whenFit}
                                                </p>
                                                <p className="mt-2 pl-4 text-[9px] font-semibold uppercase tracking-wide text-slate-500">
                                                  {visualCarouselExtraFb.modeAdjustRhythmLead}
                                                </p>
                                                <p className="mt-0.5 line-clamp-3 pl-4 text-[11px] leading-snug text-slate-300/95">
                                                  {rhythmLine}
                                                </p>
                                                <p className="mt-2 pl-4 text-[9px] font-semibold uppercase tracking-wide text-slate-500">
                                                  {visualCarouselExtraFb.modeEmphasisLead}
                                                </p>
                                                <p className="mt-0.5 line-clamp-3 pl-4 text-[11px] leading-snug text-slate-300/95">
                                                  {experienceLine}
                                                </p>
                                                <div className="mt-2 h-2 overflow-hidden rounded-full bg-white/10 ring-1 ring-white/[0.06]">
                                                  <div
                                                    className={`h-full rounded-full ${modeStyle.bar}`}
                                                    style={{ width: `${pct}%` }}
                                                  />
                                                </div>
                                              </div>
                                            );
                                          })}
                                        </div>
                                        </div>
                                      </div>
                                    ) : null}
                                    {slideId === 'why-profile' ? (
                                      <div className="flex min-h-0 w-full flex-1 flex-col justify-center gap-1.5 pr-0.5">
                                        <p
                                          className={`text-[10px] font-semibold uppercase tracking-[0.14em] ${visualCarouselSlideTitleClass(slideId)}`}
                                        >
                                          {visualCarouselExtraFb.whySlideTitle}
                                        </p>
                                        <p className="text-[11px] leading-relaxed text-slate-400">
                                          {visualCarouselExtraFb.whyLead}
                                        </p>
                                        <div className="flex flex-wrap gap-1">
                                          {[
                                            visualCarouselExtraFb.whyChipFlowFinder,
                                            visualCarouselExtraFb.whyChipTags,
                                            visualCarouselExtraFb.whyChipRhythm
                                          ].map((lab, ci) => (
                                            <span
                                              key={`why-chip-${ci}`}
                                              className="rounded-full border border-white/12 bg-white/[0.04] px-2 py-0.5 text-[9px] text-slate-300"
                                            >
                                              {lab}
                                            </span>
                                          ))}
                                        </div>
                                        {readFullProfileHTML ? (
                                          <div
                                            className="min-h-0 flex-1 text-[11px] leading-relaxed text-slate-200/95 [&_p]:mb-2 [&_p:last-child]:mb-0 [&_strong]:text-slate-100"
                                            dangerouslySetInnerHTML={{ __html: readFullProfileHTML }}
                                          />
                                        ) : (
                                          <p className="text-[11px] leading-relaxed text-slate-500">
                                            {visualCarouselExtraFb.whyProfileNarrativeEmpty}
                                          </p>
                                        )}
                                      </div>
                                    ) : null}
                                      </div>
                                    </div>
                                  ))}
                                </div>
                              </div>
                              <div className="grid shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-x-1.5 gap-y-0.5">
                                <span className="min-w-0" aria-hidden />
                                <div className="flex flex-col items-center gap-0.5">
                                  <p className="text-center text-[9px] leading-none text-slate-400/95">
                                    {visualCarouselUi.swipeHint}
                                  </p>
                                  <div className="flex max-w-full flex-wrap justify-center gap-1.5">
                                    {visualCarouselSlides.map((slideId, i) => (
                                      <button
                                        key={`vc-dot-${slideId}-${i}`}
                                        type="button"
                                        aria-label={`Summary slide ${i + 1}`}
                                        aria-current={visualCarouselPage === i ? 'step' : undefined}
                                        onClick={() => setVisualCarouselPage(i)}
                                        className={
                                          visualCarouselPage === i
                                            ? 'h-1.5 w-5 shrink-0 rounded-full bg-gradient-to-r from-emerald-400 via-teal-400 to-violet-500 shadow-[0_0_12px_rgba(45,226,197,0.45)] transition'
                                            : 'h-1.5 w-1.5 shrink-0 rounded-full bg-white/25 transition hover:bg-white/40'
                                        }
                                      />
                                    ))}
                                  </div>
                                </div>
                                <div className="flex min-w-0 items-center justify-end">
                                  <span className="inline-flex max-w-[min(100%,11rem)] items-center whitespace-nowrap text-right text-[9px] leading-none text-slate-400/95">
                                    Powered by SylvanFlow<sup className="text-[6px] align-super">™</sup>
                                  </span>
                                </div>
                              </div>
                            </div>
                          ) : (
                            <div
                              className="text-sm sm:text-base leading-[1.7] sm:leading-[1.8] text-slate-200 break-words"
                              style={{
                                wordBreak: 'break-word',
                                overflowWrap: 'break-word',
                                hyphens: 'auto'
                              }}
                              dangerouslySetInnerHTML={{ __html: personalityTextHTML }}
                            />
                          )}
                        </div>
                      )}
                    </div>
                  </div>
                </div>
              )}
            </div>
            </div>
        </div>
        </div>
        </div>
        <PowerUpFlowFinderCompleteOverlay message={tvFfCompleteOverlay} />
        <PowerUpFlowPlanBlockedOverlay
          message={flowPlanBlockedMessage}
          onDismiss={() => setFlowPlanBlockedMessage(null)}
        />
        {layoutTourNode}
        <PowerUpBottomChrome
          navTwinNuAsk={uiStrings.navTwinNuAsk}
          navTwinExplore={uiStrings.navTwinExplore}
          tourTwinWrapRef={tourTwinRef}
          hubDock={
            <div ref={tourHubRef} className="relative mx-auto w-full max-w-[440px]">
              <PowerUpHubTileRow onHubSlotPress={handleHubSlotPress} hubSlotCopy={hubSlotCopy} />
              {needsProfile ? (
                <p className="mt-2 px-1 text-center text-xs leading-relaxed text-slate-400">
                  {profileSetupPhase === 'language'
                    ? 'Choose your language above, then continue to profile details.'
                    : tvStrings.hubGate ||
                      'Complete your profile above, then tap the blue Flow Finder tile for the quiz.'}
                </p>
              ) : null}
            </div>
          }
        />
      </>
    );
  }

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