<template>
  <GuidePageFrame
    :active-tab="currentTab"
    variant="overlay"
    :show-top-tabs="false"
    @tab-change="handleTabChange"
  >
    <GuideMapShell
      v-if="currentTab === 'guide'"
      ref="guideMapShellRef"
      :search-text="guideSearchText"
      :active-mode="guideShellMode"
      :mode-layout="'status'"
      :mode-status="guideStatusLabel"
      mode-status-tone="glass"
      search-top="20px"
      search-variant="home"
      :show-search="false"
      :show-mode-row="false"
      mode-top="80px"
      :map-type="guideMapType"
      :outdoor-variant="guideOutdoorVariant"
      :indoor-asset-base-url="indoorNavAssetBaseUrl"
      :indoor-model-source="indoorModelSource"
      :floors="guideFloors"
      :normalize-floor-id="normalizeGuideFloorId"
      indoor-initial-view="overview"
      :indoor-view="indoorView"
      :layer-mode="indoorLayerMode"
      :active-floor="activeGuideFloor"
      :visible-poi-ids="searchVisiblePoiIds"
      :target-focus-request="searchTargetFocusRequest"
      layer-mode-toggle-top="154px"
      floor-bottom="193px"
      floor-max-height="206px"
      floor-side="left"
      show-floor-header
      :show-layer-mode-toggle="false"
      :show-floor="is3DMode && !showRoutePlanner"
      :show-zoom-controls="is3DMode"
      zoom-controls-top="calc(100vh - 292px)"
      :route-preview="activeRoutePreview"
      :show-route="Boolean(activeRoutePreview)"
      :outdoor-markers="homeArrivalFeatureEnabled ? arrivalOutdoorMarkers : []"
      :active-outdoor-marker-id="homeArrivalFeatureEnabled ? activeArrivalMarkerId : ''"
      :outdoor-focus-offset-y="homeArrivalFeatureEnabled ? arrivalOutdoorFocusOffsetY : 0"
      :disable-auto-exit="disableIndoorAutoExit"
      @mode-change="handleModeChange"
      @floor-request="handleFloorRequest"
      @floor-change="handleFloorChange"
      @floor-switch-failed="handleFloorSwitchFailed"
      @indoor-view-change="handleIndoorViewChange"
      @poi-click="handleGuidePoiClick"
      @selection-clear="handleGuideSelectionClear"
      @auto-switch="handleAutoSwitch"
      @initial-model-progress="handleInitialModelProgress"
      @initial-model-ready="handleInitialModelReady"
      @initial-model-failed="handleInitialModelFailed"
      @tool-click="handleIndoorToolClick"
      @outdoor-marker-click="handleArrivalMarkerClick"
    >
      <template #overlay>
        <view v-if="homeArrivalFeatureEnabled && guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
          <text class="entrance-tip-text">主入口 约 4 分钟</text>
        </view>
        <view v-if="is3DMode && showIndoorGestureHint" class="indoor-gesture-hint">
          <text class="indoor-gesture-text">{{ indoorGestureHintText }}</text>
        </view>
      </template>

      <view
        v-if="selectedGuidePoi && !showRoutePlanner && !isSimulatingRoute"
        class="guide-poi-card"
        :class="{ collapsed: isPoiCardCollapsed }"
      >
        <view class="poi-card-handle" @tap.stop="togglePoiCardCollapsed"></view>
        <view v-if="isPoiCardCollapsed" class="poi-card-collapsed" @tap="expandPoiCard">
          <view class="poi-card-copy">
            <text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
            <text class="poi-card-subtitle">{{ selectedGuidePoiCollapsedSubtitle }}</text>
          </view>
          <view class="poi-card-mini-action">
            <text class="poi-card-mini-text">展开</text>
          </view>
        </view>
        <template v-else>
          <view class="poi-card-header">
            <view class="poi-card-copy">
              <text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
              <text class="poi-card-subtitle">{{ selectedGuidePoiSubtitle }}</text>
            </view>
            <view class="poi-card-header-actions">
              <view class="poi-card-soft-action" @tap="collapsePoiCard">
                <text class="poi-card-soft-text">收起</text>
              </view>
              <view class="poi-card-close" @tap="handleGuidePoiCardClose">
                <text class="poi-card-close-text">×</text>
              </view>
            </view>
          </view>
          <view
            v-if="selectedGuidePoiExplainHall"
            class="poi-card-actions"
          >
            <view class="poi-action primary" @tap="handleViewSelectedHall">
              <text class="poi-action-text">查看展厅</text>
            </view>
          </view>
        </template>
      </view>

      <view
        v-if="showGuideFloatingActions"
        class="guide-quick-actions"
      >
        <view
          v-if="homeArrivalFeatureEnabled"
          class="guide-quick-action"
          :class="{ active: guideQuickActiveAction === 'arrival' }"
          @tap="handleMoreOutdoorNav"
        >
          <view class="guide-quick-icon arrival">
            <view class="guide-quick-pin"></view>
          </view>
          <text class="guide-quick-action-text">来馆</text>
        </view>
        <view
          class="guide-quick-action"
          :class="{ active: guideQuickActiveAction === 'indoor' }"
          @tap="handleMoreRouteGuide"
        >
          <view class="guide-quick-icon indoor">
            <view class="guide-quick-icon-floor top"></view>
            <view class="guide-quick-icon-floor bottom"></view>
          </view>
          <text class="guide-quick-action-text">导览</text>
        </view>
        <view
          class="guide-quick-action"
          :class="{ active: guideQuickActiveAction === 'explain' }"
          @tap="handleExplainQuickTap"
        >
          <view class="guide-quick-icon explain">
            <view class="guide-quick-explain-wave top"></view>
            <view class="guide-quick-explain-wave middle"></view>
            <view class="guide-quick-explain-wave bottom"></view>
          </view>
          <text class="guide-quick-action-text">讲解</text>
        </view>
      </view>

      <view
        v-if="showGuideHomeDock"
        v-show="homeSearchExpanded"
        class="guide-home-search-scrim"
        @mousedown.stop.prevent="handleHomeSearchOutsideTap"
        @click.stop="handleHomeSearchOutsideTap"
        @touchstart.stop.prevent="handleHomeSearchOutsideTap"
        @tap.stop="handleHomeSearchOutsideTap"
      ></view>

      <view
        v-show="showGuideHomeDock"
        class="guide-home-dock"
        :class="{ expanded: homeSearchExpanded }"
      >
        <PoiSearchPanel
          ref="homeSearchPanelRef"
          variant="home"
          :current-floor-id="activeGuideFloor"
          :current-floor-label="getGuideFloorLabel(activeGuideFloor)"
          @expanded-change="handleHomeSearchExpandedChange"
          @category-mode-change="handleHomeCategoryModeChange"
          @results-change="handleHomeSearchResultsChange"
          @result-tap="handleHomeSearchResultTap"
          @cancel="handleHomeSearchCancel"
        />
      </view>

      <RoutePlannerPanel
        v-if="is3DMode"
        :visible="showRoutePlanner"
        :start-point="routeStartPoint"
        :end-point="routeEndPoint"
        :point-options="routePointOptions"
        :has-route-preview="Boolean(activeRoutePreview)"
        :loading="routePlanning"
        :error="routePanelError"
        :summary="routePlanSummary"
        :picker-loading="routeTargetsLoading"
        :picker-error="routeTargetsError"
        :route-ready="routeReady"
        @update:start-point="handleRouteStartPointChange"
        @update:end-point="handleRouteEndPointChange"
        @picker-open="handleRoutePickerOpen"
        @search="handleRoutePickerSearch"
        @swap="handleRouteSwap"
        @clear="handleRouteClear"
        @view-route="handleRouteView"
        @simulate-guide="handleRouteSimulate"
        @back="handleRouteBack"
      />

      <view v-if="isSimulatingRoute && activeRoutePreview" class="route-sim-top-pill">
        <text class="route-sim-top-text">
          {{ activeRoutePreview.start.name }} → {{ activeRoutePreview.end.name }}
        </text>
      </view>

      <view v-if="isSimulatingRoute && activeRoutePreview" class="route-sim-step-bar">
        <text class="route-sim-step-text">{{ routeSimulationStepText }}</text>
        <view class="route-sim-expand" @tap="showRoutePlanner = true">
          <text class="route-sim-expand-text">展开预览</text>
        </view>
      </view>

      <view v-if="homeArrivalFeatureEnabled && guideOutdoorState === 'entrance' && !is3DMode" class="entrance-bottom-card">
        <text class="entrance-card-title">主入口</text>
        <text class="entrance-card-desc">
          可参考主入口位置，也可手动进入馆内3D并选择楼层/区域。
        </text>
        <view class="entrance-actions">
          <view class="entrance-btn secondary" @tap="handleSwitchEntrance">
            <text class="entrance-btn-text">切换入口</text>
          </view>
          <view class="entrance-btn primary" @tap="handleEnter3DMode">
            <text class="entrance-btn-text">进入馆内3D</text>
          </view>
        </view>
      </view>
    </GuideMapShell>

    <ArrivalPanel
      ref="arrivalPanelRef"
      v-if="homeArrivalFeatureEnabled && currentTab === 'guide'"
      :visible="showArrivalPanel"
      :active-type="activeArrivalType"
      :targets="activeArrivalTargets"
      :selected-target-id="selectedArrivalTargetId"
      :selected-target="selectedArrivalTarget"
      @update:active-type="handleArrivalTypeChange"
      @select-target="handleArrivalTargetSelect"
      @collapsed-change="handleArrivalPanelCollapsedChange"
      @layout-change="handleArrivalPanelLayoutChange"
    />

    <view v-else-if="currentTab === 'explain'" class="explain-page">
      <ExplainHallSelect
        :halls="explainHallItems"
        stage="hall"
        :loading="explainLoading"
        :error="explainError"
        @hall-click="handleExplainHallClick"
        @retry="loadExplainHalls"
        @back="handleExplainBack"
      />
    </view>

    <view
      v-if="launchOverlayVisible"
      class="app-launch-overlay"
      :class="{ leaving: launchOverlayLeaving }"
    >
      <view class="app-launch-stage">
        <view class="app-launch-content">
          <image
            class="app-launch-logo"
            src="/static/guide/museum-brand-icons.webp"
            mode="widthFix"
          />
          <text class="app-launch-name-en">SHENZHEN NATURAL HISTORY MUSEUM</text>
          <text class="app-launch-name-cn">深圳自然博物馆</text>
          <progress
            class="app-launch-progress"
            :percent="launchProgress"
            active
            active-mode="forwards"
            :stroke-width="3"
            active-color="#e0e100"
            background-color="#eceee7"
            :show-info="false"
          />
        </view>
      </view>
    </view>
  </GuidePageFrame>
</template>

<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue'
import ArrivalPanel from '@/components/navigation/ArrivalPanel.vue'
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
import type {
  RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
import ExplainHallSelect, {
  type ExplainHallSelectItem
} from '@/components/explain/ExplainHallSelect.vue'
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
import {
  getLastGuideLocationPreviewUrl,
  isGuideTopTab,
  type GuideTopTab
} from '@/utils/guideTopTabs'
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
import {
  guideUseCase
} from '@/usecases/guideUseCase'
import {
  guideRouteUseCase
} from '@/usecases/guideRouteUseCase'
import {
  explainUseCase
} from '@/usecases/explainUseCase'
import type {
  GuideRenderPoi
} from '@/domain/guideModel'
import type {
  PoiCategoryResultState,
  PoiSearchContext
} from '@/domain/poiSearch'
import {
  type TargetPoiFocusRequestViewModel
} from '@/view-models/guideViewModels'
import {
  useGuideModelState,
  type GuideModelResetReason,
  type GuideModelResetRequest
} from '@/composables/useGuideModelState'
import type { ResetViewBaselineResult } from '@/components/map/ThreeMap.vue'
import {
  ARRIVAL_TARGETS_BY_TYPE,
  type ArrivalSearchTarget,
  type ArrivalTargetType
} from '@/data/arrivalSearchTargets'
import type {
  GuideRouteResult,
  GuideRouteTarget,
  MuseumFloor,
  MuseumHall,
  MuseumPoi
} from '@/domain/museum'
import { getFloorSortLevel } from '@/domain/guideFloor'

type GuideIndoorView = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'

const guideModelState = useGuideModelState()

const isIndexHashRoute = () => {
  if (typeof window === 'undefined') return false

  const hash = window.location.hash || ''
  return (
    !hash
    || hash === '#'
    || hash === '#/'
    || hash.startsWith('#/?')
    || hash.startsWith('#/pages/index/index')
  )
}

const topTabFromHash = () => {
  if (typeof window === 'undefined') return null
  if (!isIndexHashRoute()) return null

  const queryString = window.location.hash.split('?')[1] || ''
  const tab = new URLSearchParams(queryString).get('tab')
  return isGuideTopTab(tab) ? tab : null
}

const launchOverlayMinDisplayMs = 700
const launchOverlayGuideTimeoutMs = 12000
const launchOverlayNonGuideTimeoutMs = 900

const shouldShowLaunchOverlay = () => {
  if (typeof window === 'undefined') return false
  return isIndexHashRoute()
}

const initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide'
// 到馆服务暂不对外开放；保留实现，后续仅需恢复此开关即可。
const homeArrivalFeatureEnabled = false

// 3D 模式状态
const is3DMode = ref(true)
const guideOutdoorState = ref<'home' | 'entrance'>('home')
const indoorView = ref<GuideIndoorView>('overview')
const activeGuideFloor = ref('')
const requestedFloorId = ref('')
const requestedFloorLabel = ref('')
const loadingFloorId = ref('')
const renderedFloorId = ref('')
const failedFloorId = ref('')
const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
const selectedGuidePoiExplainHall = ref<MuseumHall | null>(null)
let selectedGuidePoiExplainHallRequestId = 0

// 状态
const currentTab = ref<GuideTopTab>(initialTopTab())
const launchOverlayVisible = ref(shouldShowLaunchOverlay())
const launchOverlayLeaving = ref(false)
const launchProgress = ref(8)
const launchInitialModelSettled = ref(false)
const showRoutePlanner = ref(false)
const routeStartPoint = ref<RoutePointOption | null>(null)
const routeEndPoint = ref<RoutePointOption | null>(null)
const routePointOptions = ref<RoutePointOption[]>([])
const routeTargetsLoading = ref(false)
const routeTargetsError = ref('')
const routePlanning = ref(false)
const routePlanError = ref('')
const routePreviewUnavailableMessage = ref('')
const routeReady = ref(false)
const activeRoutePreview = ref<GuideRouteResult | null>(null)
const isSimulatingRoute = ref(false)
const isPoiCardCollapsed = ref(false)
const returningToOverview = ref(false)

// GuideMapShell 组件引用，用于显式调用路线清除方法
const guideMapShellRef = ref<{
  clearRoute?: () => void
  switchFloor?: (floorId: string) => Promise<void> | void
  showOverview?: () => Promise<void> | void
  resetToViewBaseline?: (options: {
    view: 'overview' | 'floor'
    floorId?: string
    reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
  }) => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
  resetToInitialState?: () => Promise<boolean> | boolean
} | null>(null)
const homeSearchPanelRef = ref<{
  collapseHomePanel?: () => void
  resetSearchState?: () => Promise<void> | void
  returnToCollapsedAfterDetail?: () => Promise<void>
  restoreResultListScroll?: () => Promise<void>
} | null>(null)
const homeSearchExpanded = ref(false)
const homeCategoryModeActive = ref(false)
const searchVisiblePoiIds = ref<string[] | null>(null)
const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
type PoiSearchOverlayHistoryState = {
  museumGuideOverlay?: 'poi-search'
  [key: string]: unknown
}

let poiSearchHistoryPushed = false
let removingPoiSearchHistory = false

let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
let launchOverlayHideDelayTimer: ReturnType<typeof setTimeout> | null = null
let launchOverlayStartedAt = 0
let launchOverlayRunId = 0

const clearLaunchTimers = () => {
  if (launchOverlayFallbackTimer) {
    clearTimeout(launchOverlayFallbackTimer)
    launchOverlayFallbackTimer = null
  }

  if (launchOverlayFadeTimer) {
    clearTimeout(launchOverlayFadeTimer)
    launchOverlayFadeTimer = null
  }

  if (launchOverlayHideDelayTimer) {
    clearTimeout(launchOverlayHideDelayTimer)
    launchOverlayHideDelayTimer = null
  }
}

const completeHideLaunchOverlay = () => {
  if (!launchOverlayVisible.value) return

  const runId = launchOverlayRunId
  launchProgress.value = 100
  launchOverlayLeaving.value = true
  clearLaunchTimers()

  launchOverlayFadeTimer = setTimeout(() => {
    if (runId !== launchOverlayRunId) return
    launchOverlayVisible.value = false
    launchOverlayLeaving.value = false
    launchOverlayFadeTimer = null
  }, 360)
}

const hideLaunchOverlay = () => {
  if (!launchOverlayVisible.value || launchOverlayLeaving.value) return

  const elapsedMs = launchOverlayStartedAt ? Date.now() - launchOverlayStartedAt : launchOverlayMinDisplayMs
  const waitMs = Math.max(0, launchOverlayMinDisplayMs - elapsedMs)

  if (waitMs > 0) {
    if (launchOverlayHideDelayTimer) clearTimeout(launchOverlayHideDelayTimer)
    const runId = launchOverlayRunId
    launchOverlayHideDelayTimer = setTimeout(() => {
      if (runId !== launchOverlayRunId) return
      launchOverlayHideDelayTimer = null
      completeHideLaunchOverlay()
    }, waitMs)
    return
  }

  completeHideLaunchOverlay()
}

const shouldWaitForLaunchModel = () => (
  currentTab.value === 'guide'
  && is3DMode.value
)

// The launch overlay is independent from the selected map business mode. Releasing it
// must leave ThreeMap mounted so its loading/error/retry lifecycle can continue.
const releaseLaunchOverlayWithoutInitialModel = (reason: string, event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => {
  if (!launchOverlayVisible.value || launchOverlayLeaving.value) return

  console.warn('首页前置加载页未等待初始三维模型，已关闭启动遮罩:', {
    reason,
    event
  })

  launchInitialModelSettled.value = true
  launchProgress.value = Math.max(launchProgress.value, 96)
  hideLaunchOverlay()
}

const startLaunchOverlay = () => {
  if (!launchOverlayVisible.value) return

  clearLaunchTimers()
  const runId = ++launchOverlayRunId
  launchInitialModelSettled.value = false
  launchOverlayStartedAt = Date.now()
  launchProgress.value = shouldWaitForLaunchModel() ? 8 : 68

  const timeoutMs = shouldWaitForLaunchModel()
    ? launchOverlayGuideTimeoutMs
    : launchOverlayNonGuideTimeoutMs

  launchOverlayFallbackTimer = setTimeout(() => {
    if (runId !== launchOverlayRunId) return
    if (shouldWaitForLaunchModel() && !launchInitialModelSettled.value) {
      releaseLaunchOverlayWithoutInitialModel('timeout')
      return
    }
    hideLaunchOverlay()
  }, timeoutMs)
}

const handleInitialModelProgress = (event: { progress: number; message: string; view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => {
  if (!launchOverlayVisible.value || launchInitialModelSettled.value || !shouldWaitForLaunchModel()) return

  launchProgress.value = Math.min(99, Math.max(8, event.progress))
}

const settleLaunchOverlayByModel = (status: 'ready' | 'failed', event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => {
  launchInitialModelSettled.value = true

  if (status === 'failed') {
    releaseLaunchOverlayWithoutInitialModel('failed', event)
    return
  } else {
    launchProgress.value = 100
    console.log('首页前置加载页收到初始模型加载完成:', event)
  }

  hideLaunchOverlay()
}

const showArrivalPanel = ref(false)
const showArrivalPanelCollapsed = ref(false)
const arrivalPanelRef = ref<InstanceType<typeof ArrivalPanel> | null>(null)
const activeArrivalType = ref<ArrivalTargetType>('bus')
const selectedArrivalTargetId = ref('')
const arrivalPanelHeight = ref(0)

const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
const indoorModelSource = guideUseCase.getModelSource()
const guideFloors = ref<MuseumFloor[]>([])
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
const getPreferredInitialGuideFloorId = (floors: MuseumFloor[]) => (
  floors.find((floor) => (
    getFloorSortLevel(floor) === 1
    || normalizeGuideFloorId(floor.id) === 'L1'
    || normalizeGuideFloorId(floor.label) === 'L1'
  ))?.id
  || floors[0]?.id
  || ''
)
const getGuideFloorById = (floorId: string) => (
  guideFloors.value.find((floor) => floor.id === floorId)
)
const getGuideFloorLabel = (floorId: string) => (
  getGuideFloorById(floorId)?.label || floorId
)
const indoorLayerMode = computed<LayerDisplayMode>(() => (
  indoorView.value === 'multi' ? 'multi' : 'single'
))
const showIndoorGestureHint = ref(true)
const indoorGestureHintText = ref('单指平移 · 双指缩放/平移 · 点选标记查看位置')
let indoorGestureHintTimer: ReturnType<typeof setTimeout> | null = null

const showIndoorHint = (text = '单指平移 · 双指缩放/平移 · 点选标记查看位置', durationMs = 5200) => {
  indoorGestureHintText.value = text
  showIndoorGestureHint.value = is3DMode.value

  if (indoorGestureHintTimer) {
    clearTimeout(indoorGestureHintTimer)
  }

  indoorGestureHintTimer = setTimeout(() => {
    showIndoorGestureHint.value = false
    indoorGestureHintTimer = null
  }, durationMs)
}

const routePlanSummary = computed(() => {
  if (activeRoutePreview.value) {
    const floorCount = new Set(activeRoutePreview.value.floorSegments.map((segment) => segment.floorId)).size
    const floorText = floorCount > 1 ? `跨 ${floorCount} 层` : activeRoutePreview.value.start.floorLabel
    return `${floorText} · 约 ${Math.round(activeRoutePreview.value.distanceMeters)} 米`
  }

  if (routeStartPoint.value && routeEndPoint.value) {
    return `${routeStartPoint.value.floorLabel} 到 ${routeEndPoint.value.floorLabel}`
  }

  return ''
})

const routePanelError = computed(() => routePlanError.value)

const disableIndoorAutoExit = computed(() => (
  Boolean(selectedGuidePoi.value)
  || Boolean(activeRoutePreview.value)
  || showRoutePlanner.value
  || isSimulatingRoute.value
))

const showGuideHomeDock = computed(() => (
  currentTab.value === 'guide'
  && is3DMode.value
  && !showRoutePlanner.value
  && !showArrivalPanel.value
  && !selectedGuidePoi.value
  && !isSimulatingRoute.value
  && !(guideOutdoorState.value === 'entrance' && !is3DMode.value)
))

const showGuideFloatingActions = computed(() => (
  currentTab.value === 'guide'
  && !showRoutePlanner.value
  && (!showArrivalPanel.value || showArrivalPanelCollapsed.value)
  && !selectedGuidePoi.value
  && !isSimulatingRoute.value
  && (!showGuideHomeDock.value || !homeSearchExpanded.value)
))

const activeArrivalTargets = computed(() => ARRIVAL_TARGETS_BY_TYPE[activeArrivalType.value])

const selectedArrivalTarget = computed(() => (
  activeArrivalTargets.value.find((target) => target.id === selectedArrivalTargetId.value)
  || activeArrivalTargets.value[0]
  || null
))

const arrivalOutdoorMarkers = computed(() => {
  if (!showArrivalPanel.value || is3DMode.value) return []

  return activeArrivalTargets.value.map((target) => ({
    id: target.id,
    title: target.title,
    subtitle: target.subtitle,
    latitude: target.latitude,
    longitude: target.longitude
  }))
})

const activeArrivalMarkerId = computed(() => (
  showArrivalPanel.value && !is3DMode.value ? selectedArrivalTarget.value?.id || '' : ''
))

const arrivalOutdoorFocusOffsetY = computed(() => {
  if (!showArrivalPanel.value || showArrivalPanelCollapsed.value || is3DMode.value) return 0
  return Math.min(260, Math.max(0, Math.round(arrivalPanelHeight.value / 2 + 24)))
})

const guideQuickActiveAction = computed<'indoor' | 'arrival' | 'explain'>(() => {
  if (currentTab.value === 'explain') return 'explain'
  if (showArrivalPanel.value) return 'arrival'
  return is3DMode.value ? 'indoor' : 'arrival'
})

const toRoutePointOption = (target: GuideRouteTarget): RoutePointOption => ({
  poiId: target.poiId,
  name: target.name,
  floorId: target.floorId,
  floorLabel: target.floorLabel,
  categoryLabel: target.categoryLabel
})

const selectedGuidePoiSubtitle = computed(() => {
  const poi = selectedGuidePoi.value
  if (!poi) return ''
  const floor = guideFloors.value.find((item) => item.id === poi.floorId)
  const floorLabel = floor?.label || poi.floorId
  return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}`
})

const isGuidePoiHallLike = (poi: GuideRenderPoi | null) => Boolean(
  poi
  && (
    poi.kind === 'hall'
    || poi.primaryCategory === 'touring_poi'
    || poi.primaryCategory === 'exhibition_hall'
    || poi.primaryCategory === 'exhibition_hall_entrance'
  )
)

const selectedGuidePoiIsHall = computed(() => isGuidePoiHallLike(selectedGuidePoi.value))

const selectedGuidePoiCollapsedSubtitle = computed(() => (
  selectedGuidePoiExplainHall.value ? '点按展开查看展厅' : selectedGuidePoiSubtitle.value
))

const routeSimulationStepText = computed(() => {
  if (!activeRoutePreview.value) return '位置预览 · 查看位置关系'
  const hasConnector = activeRoutePreview.value.connectorPoints.length > 0
  return hasConnector ? '位置预览 · 跨楼层位置关系' : '位置预览 · 同层位置关系'
})

const explainHallItems = ref<ExplainHallSelectItem[]>([])
const explainLoading = ref(false)
const explainError = ref('')
let explainLoadPromise: Promise<void> | null = null

// Tab 切换处理
const syncTopTabToUrl = (tabId: GuideTopTab) => {
  if (typeof window === 'undefined') return
  if (!isIndexHashRoute()) return

  const currentHash = window.location.hash || ''
  const queryStart = currentHash.indexOf('?')
  const params = new URLSearchParams(queryStart >= 0 ? currentHash.slice(queryStart + 1) : '')
  params.set('tab', tabId)
  const url = `#/pages/index/index?${params.toString()}`
  if (window.location.hash !== url) {
    window.history.replaceState(window.history.state, '', url)
  }
}

const isPoiSearchOverlayHistoryState = (state: unknown): state is PoiSearchOverlayHistoryState => (
  Boolean(
    state
    && typeof state === 'object'
    && (state as PoiSearchOverlayHistoryState).museumGuideOverlay === 'poi-search'
  )
)

const canManagePoiSearchOverlayHistory = () => (
  typeof window !== 'undefined'
  && isIndexHashRoute()
  && isEmbeddedInWechatMiniProgram()
)

const removePoiSearchOverlayFromCurrentHistoryState = () => {
  if (!canManagePoiSearchOverlayHistory()) return
  if (!isPoiSearchOverlayHistoryState(window.history.state)) return

  const nextState = { ...window.history.state }
  delete nextState.museumGuideOverlay
  window.history.replaceState(nextState, '', window.location.href)
}

const pushPoiSearchOverlayHistory = () => {
  if (!canManagePoiSearchOverlayHistory() || !homeSearchExpanded.value) return
  if (poiSearchHistoryPushed) return

  if (isPoiSearchOverlayHistoryState(window.history.state)) {
    poiSearchHistoryPushed = true
    return
  }

  window.history.pushState({
    ...(window.history.state || {}),
    museumGuideOverlay: 'poi-search'
  } satisfies PoiSearchOverlayHistoryState, '', window.location.href)
  poiSearchHistoryPushed = true
}

const removePoiSearchOverlayHistory = () => {
  if (!canManagePoiSearchOverlayHistory() || !poiSearchHistoryPushed) return

  if (!isPoiSearchOverlayHistoryState(window.history.state)) {
    poiSearchHistoryPushed = false
    return
  }

  poiSearchHistoryPushed = false
  removingPoiSearchHistory = true
  window.history.back()
}

const collapsePoiSearchAfterHistoryBack = async () => {
  homeSearchPanelRef.value?.collapseHomePanel?.()
  await homeSearchPanelRef.value?.resetSearchState?.()
  clearHomeSearchMapState()
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  homeSearchExpanded.value = false
}

const handlePoiSearchHistoryPopState = (event: PopStateEvent) => {
  if (!canManagePoiSearchOverlayHistory()) return

  if (removingPoiSearchHistory) {
    removingPoiSearchHistory = false
    poiSearchHistoryPushed = false
    if (homeSearchExpanded.value) pushPoiSearchOverlayHistory()
    return
  }

  if (!homeSearchExpanded.value) return
  if (isPoiSearchOverlayHistoryState(event.state)) {
    poiSearchHistoryPushed = true
    return
  }

  poiSearchHistoryPushed = false
  void collapsePoiSearchAfterHistoryBack()
}

const loadTopTabData = (tabId: GuideTopTab) => {
  if (tabId === 'explain') {
    void loadExplainHalls()
  } else {
    void loadGuideFloors()
  }
}

const handleTabChange = (tabId: GuideTopTab) => {
  if (tabId === 'guide' && currentTab.value === 'explain') {
    const lastLocationPreviewUrl = getLastGuideLocationPreviewUrl()
    if (lastLocationPreviewUrl) {
      uni.reLaunch({
        url: lastLocationPreviewUrl
      })
      return
    }
  }

  currentTab.value = tabId
  syncTopTabToUrl(tabId)
  loadTopTabData(tabId)

  console.log('切换标签:', tabId)
  
  // 关闭可能打开的面板
  showRoutePlanner.value = false
  isSimulatingRoute.value = false
  isPoiCardCollapsed.value = false
  closeArrivalPanel()
}

const syncTopTabFromHash = () => {
  if (!isIndexHashRoute()) return

  const tab = topTabFromHash()
  if (tab) {
    const tabChanged = currentTab.value !== tab
    currentTab.value = tab
    loadTopTabData(tab)
    if (tabChanged) {
      showRoutePlanner.value = false
      isSimulatingRoute.value = false
      isPoiCardCollapsed.value = false
      closeArrivalPanel()
    }
    syncTopTabToUrl(tab)
    return
  }

  syncTopTabToUrl(currentTab.value)
  loadTopTabData(currentTab.value)
}

onMounted(() => {
  startLaunchOverlay()
  showIndoorHint()

  if (typeof window === 'undefined') return

  syncTopTabFromHash()
  removePoiSearchOverlayFromCurrentHistoryState()
  window.addEventListener('hashchange', syncTopTabFromHash)
  window.addEventListener('popstate', handlePoiSearchHistoryPopState)
})

onUnmounted(() => {
  launchOverlayRunId += 1
  clearLaunchTimers()

  if (indoorGestureHintTimer) {
    clearTimeout(indoorGestureHintTimer)
    indoorGestureHintTimer = null
  }

  if (typeof window === 'undefined') return

  window.removeEventListener('hashchange', syncTopTabFromHash)
  window.removeEventListener('popstate', handlePoiSearchHistoryPopState)
})

onLoad((options: any = {}) => {
  const routeTab = Array.isArray(options.tab) ? options.tab[0] : options.tab
  const tab = topTabFromHash() || routeTab
  if (isGuideTopTab(tab)) {
    currentTab.value = tab
  } else {
    syncTopTabToUrl(currentTab.value)
  }

  loadTopTabData(currentTab.value)
})

const loadGuideFloors = async () => {
  try {
    const floors = await guideUseCase.getFloors()
    guideFloors.value = floors
    const normalizedActiveFloorId = activeGuideFloor.value
      ? normalizeGuideFloorId(activeGuideFloor.value)
      : ''
    activeGuideFloor.value = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
      || getPreferredInitialGuideFloorId(floors)
      || activeGuideFloor.value
    renderedFloorId.value = activeGuideFloor.value
  } catch (error) {
    console.error('加载导览楼层失败:', error)
    guideFloors.value = []
  }
}

const loadExplainHalls = async () => {
  if (explainHallItems.value.length) return
  if (explainLoadPromise) return explainLoadPromise

  explainLoading.value = true
  explainError.value = ''

  explainLoadPromise = (async () => {
    try {
      const halls = await explainUseCase.loadExplainHalls()
      const summaries = await explainUseCase.loadExplainHallSummaries(halls.map((hall) => hall.id))
      explainHallItems.value = buildExplainHallItems(halls, summaries)
    } catch (error) {
      console.error('加载讲解内容失败:', error)
      explainError.value = '讲解内容加载失败，请稍后重试'
      explainHallItems.value = []
    } finally {
      explainLoading.value = false
      explainLoadPromise = null
    }
  })()

  return explainLoadPromise
}

const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()

const buildExplainHallItems = (
  halls: Awaited<ReturnType<typeof explainUseCase.loadExplainHalls>>,
  summaries: Awaited<ReturnType<typeof explainUseCase.loadExplainHallSummaries>> = {}
): ExplainHallSelectItem[] => {
  return halls.map((hall) => {
    const keywordParts = [
      hall.name,
      hall.floorLabel,
      hall.description,
      hall.area
    ].filter(Boolean) as string[]

    return {
      id: hall.id,
      name: hall.name,
      floorLabel: hall.floorLabel,
      image: hall.image,
      explainCount: hall.exhibitCount || 0,
      guideStopCount: summaries[hall.id]?.guideStopCount,
      searchText: normalizeExplainKeyword(keywordParts.join(' '))
    }
  })
}

const handleFloorRequest = ({ floorId, floorLabel }: { floorId: string; floorLabel: string }) => {
  requestedFloorId.value = floorId
  requestedFloorLabel.value = floorLabel
  loadingFloorId.value = floorId
  failedFloorId.value = ''
}

const handleFloorChange = (floorId: string) => {
  activeGuideFloor.value = floorId
  renderedFloorId.value = floorId
  if (loadingFloorId.value === floorId) {
    loadingFloorId.value = ''
  }
  if (failedFloorId.value === floorId) {
    failedFloorId.value = ''
  }
  indoorView.value = 'floor'
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  showIndoorHint(`已切换到 ${getGuideFloorLabel(floorId)}，单指平移、双指可缩放`, 3200)
  console.log('楼层渲染完成:', floorId)
}

const handleFloorSwitchFailed = ({ floorId, floorLabel }: { floorId: string; floorLabel: string }) => {
  if (loadingFloorId.value === floorId) {
    loadingFloorId.value = ''
  }
  failedFloorId.value = floorId
  showIndoorHint(`${floorLabel || getGuideFloorLabel(floorId)} 加载失败，请稍后重试`, 3600)
  console.warn('楼层渲染失败:', floorId)
}

const handleIndoorViewChange = (view: GuideIndoorView) => {
  indoorView.value = view
  if (view !== 'floor') {
    selectedGuidePoi.value = null
    isPoiCardCollapsed.value = false
  }

  if (view === 'multi') {
    showIndoorHint('已切换多层视图，可点选楼层查看', 3200)
  } else if (view === 'floor') {
    showIndoorHint('已切换单层视图，可点选标记查看位置', 3200)
  }
}

const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }) => {
  console.log('自动切换视图:', event)
  indoorView.value = event.to
  if (event.to !== 'floor') {
    selectedGuidePoi.value = null
    isPoiCardCollapsed.value = false
  }
  showIndoorHint(event.to === 'floor' ? '已进入单层视图，可点选标记查看位置' : '已返回建筑外观', 3600)
}

const handleInitialModelReady = (event: { view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => {
  const readyFloorId = event.floorId || activeGuideFloor.value
  if (event.view === 'overview' && readyFloorId) {
    activeGuideFloor.value = readyFloorId
    renderedFloorId.value = readyFloorId
    guideModelState.initializeModel({ view: event.view, floorId: readyFloorId })
  }
  settleLaunchOverlayByModel('ready', event)
  const context = guideModelState.consumePendingViewBaselineReset()
  if (context) {
    void resetGuideModelToFloorBaseline({
      rendererReason: context.rendererReason,
      floorId: context.floorId,
      request: context
    })
  }
}

const handleInitialModelFailed = (event: { view: GuideIndoorView; floorId?: string; message: string; elapsedMs?: number }) => {
  settleLaunchOverlayByModel('failed', event)
}

const handleIndoorToolClick = (tool: string) => {
  if (!is3DMode.value) return

  if (tool === '复位') {
    showIndoorHint('已复位视角，单指平移、双指可缩放', 2800)
  } else if (tool === '俯视') {
    showIndoorHint('已切换俯视视角，可点选标记查看位置', 2800)
  } else if (tool === '斜视') {
    showIndoorHint('已切换斜视视角，可查看空间层次', 2800)
  }
}

const handleGuidePoiClick = (poi: GuideRenderPoi) => {
  selectedGuidePoi.value = poi
  isPoiCardCollapsed.value = !selectedGuidePoiIsHall.value
  indoorView.value = 'floor'
  isSimulatingRoute.value = false
  activeGuideFloor.value = poi.floorId
  renderedFloorId.value = poi.floorId
  showIndoorHint(`${poi.name} · ${getGuideFloorLabel(poi.floorId)}`, 3000)
}

const handleGuideSelectionClear = () => {
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
}

const handleGuidePoiCardClose = () => {
  const floorId = selectedGuidePoi.value?.floorId
  if (!floorId) {
    handleGuideSelectionClear()
    return
  }

  void resetGuideModelToFloorBaseline({
    rendererReason: 'floor-reset',
    floorId
  })
}

const collapsePoiCard = () => {
  isPoiCardCollapsed.value = true
}

const expandPoiCard = () => {
  isPoiCardCollapsed.value = false
}

const togglePoiCardCollapsed = () => {
  isPoiCardCollapsed.value = !isPoiCardCollapsed.value
}

const normalizeHallMatchText = (value?: string) => (value || '')
  .trim()
  .replace(/[\s（）()【】[\]_-]/g, '')
  .replace(/^展厅\d+/, '')
  .toLowerCase()

const resolvePoiExplainHall = async (poi: GuideRenderPoi): Promise<MuseumHall | null> => {
  const halls = await explainUseCase.listHalls()
  const matchedByPoiId = halls.find((hall) => (
    hall.poiId === poi.id
    || hall.location?.poiId === poi.id
  ))
  if (matchedByPoiId) return matchedByPoiId

  const hallId = poi.hallId
  if (hallId) {
    const matchedById = halls.find((hall) => hall.id === hallId)
    if (matchedById) return matchedById
  }

  const matchedByEntrance = halls.find((hall) => (
    (poi.entrances || []).some((entrance) => (
      hall.poiId === entrance.sourcePlaceId
      || hall.poiId === entrance.id
    ))
  ))
  if (matchedByEntrance) return matchedByEntrance

  const poiHallName = normalizeHallMatchText(poi.hallName || poi.name)
  return halls.find((hall) => {
    const hallName = normalizeHallMatchText(hall.name)
    return hallName && poiHallName && hallName === poiHallName
  }) || null
}

watch(selectedGuidePoi, (poi) => {
  const requestId = ++selectedGuidePoiExplainHallRequestId
  selectedGuidePoiExplainHall.value = null
  if (!poi || !isGuidePoiHallLike(poi)) return

  void resolvePoiExplainHall(poi)
    .then((hall) => {
      if (
        requestId !== selectedGuidePoiExplainHallRequestId
        || selectedGuidePoi.value?.id !== poi.id
      ) return

      selectedGuidePoiExplainHall.value = hall
    })
    .catch((error) => {
      if (requestId !== selectedGuidePoiExplainHallRequestId) return
      console.warn('点位讲解展厅关联解析失败:', error)
    })
}, { flush: 'sync' })

const navigateToPoiDetail = ({
  url,
  floorId,
  failureMessage
}: {
  url: string
  floorId: string
  failureMessage: string
}) => {
  const returnContext = guideModelState.beginPoiDetailReturn(floorId)
  uni.navigateTo({
    url,
    success: () => {
      guideModelState.confirmPoiDetailReturn(returnContext.requestId)
    },
    fail: () => {
      guideModelState.cancelPoiDetailReturn(returnContext.requestId)
      uni.showToast({
        title: failureMessage,
        icon: 'none'
      })
    }
  })
}

const navigateFromSelectedPoiToDetail = (url: string) => {
  const poi = selectedGuidePoi.value
  if (!poi) return

  // Detail pages do not own the mounted renderer. The homepage consumes this once on return.
  navigateToPoiDetail({
    url,
    floorId: poi.floorId,
    failureMessage: '讲解详情打开失败，请重试'
  })
}

const navigateToHallExplainObjects = (hallId: string, hallName = '讲解') => {
  navigateFromSelectedPoiToDetail(
    explainGuideStopListUrl(hallId, hallName)
  )
}

const handleViewSelectedHall = () => {
  const hall = selectedGuidePoiExplainHall.value
  if (!hall) return
  navigateToHallExplainObjects(hall.id, hall.name)
}

const loadRouteTargets = async (keyword = '') => {
  routeTargetsLoading.value = true
  routeTargetsError.value = ''

  try {
    const targets = keyword
      ? await guideRouteUseCase.searchTargets(keyword)
      : await guideRouteUseCase.listTargets()
    const options = targets.map(toRoutePointOption)
    routePointOptions.value = options
  } catch (error) {
    console.error('加载路线目标失败:', error)
    routeTargetsError.value = error instanceof Error ? error.message : '路线目标加载失败'
    routePointOptions.value = []
  } finally {
    routeTargetsLoading.value = false
  }
}

const refreshRouteReadinessState = async () => {
  try {
    const readiness = await guideRouteUseCase.getRouteReadiness()
    routeReady.value = readiness.ready
    routePreviewUnavailableMessage.value = readiness.ready
      ? ''
      : readiness.message || '当前可查看位置预览和位置关系，暂不提供正式室内导航'
  } catch (error) {
    console.warn('读取路线能力状态失败:', error)
    routeReady.value = false
    routePreviewUnavailableMessage.value = '当前可查看位置预览和位置关系，暂不提供正式室内导航'
  }
}

const clearRoutePreviewState = () => {
  guideMapShellRef.value?.clearRoute?.()
  activeRoutePreview.value = null
  routePlanError.value = ''
  isSimulatingRoute.value = false
}

const handleRouteClear = () => {
  routeStartPoint.value = null
  routeEndPoint.value = null
  showRoutePlanner.value = false
  clearRoutePreviewState()
}

const handleRouteBack = () => {
  if (activeRoutePreview.value) {
    activeRoutePreview.value = null
    routePlanError.value = ''
    isSimulatingRoute.value = false
    showRoutePlanner.value = true
    return
  }

  showRoutePlanner.value = false
  isSimulatingRoute.value = false
}

const handleRouteStartPointChange = (point: RoutePointOption | null) => {
  routeStartPoint.value = point
  clearRoutePreviewState()
}

const handleRouteEndPointChange = (point: RoutePointOption | null) => {
  routeEndPoint.value = point
  clearRoutePreviewState()
}

const handleRouteSwap = () => {
  const nextStart = routeEndPoint.value
  routeEndPoint.value = routeStartPoint.value
  routeStartPoint.value = nextStart
  clearRoutePreviewState()
}

const handleRoutePickerOpen = () => {
  if (!routePointOptions.value.length) {
    void loadRouteTargets()
  }
}

const handleRoutePickerSearch = ({ keyword }: { mode: 'start' | 'end'; keyword: string }) => {
  void loadRouteTargets(keyword)
}

const focusRouteFloor = (route: GuideRouteResult) => {
  const routeFloorCount = new Set(route.floorSegments.map((segment) => segment.floorId)).size
  activeGuideFloor.value = route.start.floorId
  renderedFloorId.value = route.start.floorId
  indoorView.value = routeFloorCount > 1 ? 'multi' : 'floor'
}

const handleRouteView = async ({ startPoint, endPoint }: { startPoint: RoutePointOption; endPoint: RoutePointOption }) => {
  routePlanning.value = true
  routePlanError.value = ''
  isSimulatingRoute.value = false

  try {
    await refreshRouteReadinessState()
    if (routePreviewUnavailableMessage.value) {
      activeRoutePreview.value = null
      routePlanError.value = routePreviewUnavailableMessage.value
      return
    }

    const result = await guideRouteUseCase.planRoute({
      startPoiId: startPoint.poiId,
      endPoiId: endPoint.poiId
    })

    if (!result.route) {
      activeRoutePreview.value = null
      routePlanError.value = result.error || '馆内导览位置关系生成失败'
      return
    }

    activeRoutePreview.value = result.route
    focusRouteFloor(result.route)
    showRoutePlanner.value = true
  } catch (error) {
    console.error('馆内导览位置关系生成失败:', error)
    activeRoutePreview.value = null
    routePlanError.value = error instanceof Error ? error.message : '馆内导览位置关系生成失败'
  } finally {
    routePlanning.value = false
  }
}

const handleRouteSimulate = () => {
  if (!activeRoutePreview.value) return
  showRoutePlanner.value = false
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  isSimulatingRoute.value = true
}

// 进入 3D 馆内模式
const handleEnter3DMode = () => {
  console.log('进入 3D 馆内模式')
  closeArrivalPanel()
  indoorView.value = 'overview'
  is3DMode.value = true
  guideOutdoorState.value = 'home'
  showIndoorHint()
  if (!routePointOptions.value.length) {
    void loadRouteTargets()
  }
}

const handleModeChange = (mode: '2d' | '3d') => {
  is3DMode.value = mode === '3d'

  if (mode === '2d') {
    closeHomeSearchDock()
    indoorView.value = 'overview'
    guideOutdoorState.value = 'home'
    selectedGuidePoi.value = null
    isPoiCardCollapsed.value = false
    showRoutePlanner.value = false
    isSimulatingRoute.value = false
    clearRoutePreviewState()
  } else {
    closeArrivalPanel()
    indoorView.value = 'overview'
    showIndoorHint()
    if (!routePointOptions.value.length) {
      void loadRouteTargets()
    }
  }
}

const guideStatusLabel = computed(() => {
  if (is3DMode.value) {
    if (indoorView.value === 'overview') return '建筑外观'
    if (indoorView.value === 'multi') return '馆内多层'
    if (loadingFloorId.value) {
      return `切换${requestedFloorLabel.value || getGuideFloorLabel(requestedFloorId.value || loadingFloorId.value)}`
    }
    if (failedFloorId.value) {
      return `${getGuideFloorLabel(failedFloorId.value)}失败`
    }
    return getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'
  }

  if (guideOutdoorState.value === 'entrance') {
    return '推荐入口'
  }

  return '馆外参考'
})

const handleSwitchEntrance = () => {
  guideOutdoorState.value = 'home'
}

const handleMoreRouteGuide = async () => {
  if (returningToOverview.value) return

  if (is3DMode.value && indoorView.value !== 'overview') {
    returningToOverview.value = true
    showRoutePlanner.value = false
    selectedGuidePoi.value = null
    isPoiCardCollapsed.value = false
    isSimulatingRoute.value = false
    requestedFloorId.value = ''
    requestedFloorLabel.value = ''
    loadingFloorId.value = ''
    failedFloorId.value = ''
    clearRoutePreviewState()

    try {
      await guideMapShellRef.value?.showOverview?.()
      indoorView.value = 'overview'
      guideOutdoorState.value = 'home'
      closeHomeSearchDock()
      showIndoorHint('已返回建筑外观，可放大再次进入馆内', 3200)
    } catch (error) {
      console.error('返回建筑外观失败:', error)
      showIndoorHint('建筑外观加载失败，请稍后重试', 3200)
    } finally {
      returningToOverview.value = false
    }
    return
  }

  closeArrivalPanel()
  showRoutePlanner.value = false
  is3DMode.value = true
  guideOutdoorState.value = 'home'
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  isSimulatingRoute.value = false
  const floorId = activeGuideFloor.value
  const floorLabel = getGuideFloorLabel(floorId) || '默认楼层'
  if (!floorId) {
    showIndoorHint('暂未获取到可进入的楼层，请稍后重试', 3200)
    return
  }

  requestedFloorId.value = floorId
  requestedFloorLabel.value = floorLabel
  loadingFloorId.value = floorId
  failedFloorId.value = ''
  // 渲染器提交 floor-change 前仍保持建筑外观，避免楼层控件显示假状态。
  showIndoorHint(`正在进入 ${floorLabel}…`, 3200)
  await guideMapShellRef.value?.switchFloor?.(floorId)
}

const selectFirstArrivalTarget = () => {
  selectedArrivalTargetId.value = activeArrivalTargets.value[0]?.id || ''
}

const resetArrivalPanelState = () => {
  activeArrivalType.value = 'bus'
  selectFirstArrivalTarget()
}

const closeArrivalPanel = () => {
  showArrivalPanel.value = false
  showArrivalPanelCollapsed.value = false
  selectedArrivalTargetId.value = ''
  arrivalPanelHeight.value = 0
}

const handleArrivalPanelCollapsedChange = (collapsed: boolean) => {
  showArrivalPanelCollapsed.value = collapsed
}

const handleArrivalPanelLayoutChange = (layout: { height: number; collapsed: boolean }) => {
  arrivalPanelHeight.value = layout.height
  showArrivalPanelCollapsed.value = layout.collapsed
}

const handleArrivalTypeChange = (type: ArrivalTargetType) => {
  activeArrivalType.value = type
  selectFirstArrivalTarget()
}

const handleArrivalTargetSelect = (target: ArrivalSearchTarget) => {
  selectedArrivalTargetId.value = target.id
}

const handleArrivalMarkerClick = (markerId: string) => {
  if (!homeArrivalFeatureEnabled) return
  const target = activeArrivalTargets.value.find((item) => item.id === markerId)
  if (!target) return
  selectedArrivalTargetId.value = target.id
}

const closeHomeSearchDock = () => {
  homeSearchPanelRef.value?.collapseHomePanel?.()
  homeSearchExpanded.value = false
}

const resetGuideModelToFloorBaseline = async ({
  rendererReason,
  floorId,
  request
}: {
  rendererReason: GuideModelResetReason
  floorId: string
  request?: GuideModelResetRequest
}) => {
  const initial = guideModelState.initialState.value
  const resetRequest = request || guideModelState.beginViewBaselineReset({ floorId, rendererReason })
  searchTargetFocusRequest.value = null
  searchVisiblePoiIds.value = null
  homeCategoryModeActive.value = false
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  showRoutePlanner.value = false
  activeRoutePreview.value = null
  routeStartPoint.value = null
  routeEndPoint.value = null
  routePlanError.value = ''
  isSimulatingRoute.value = false
  requestedFloorId.value = ''
  requestedFloorLabel.value = ''
  loadingFloorId.value = ''
  failedFloorId.value = ''
  is3DMode.value = true
  guideOutdoorState.value = 'home'
  indoorView.value = 'floor'
  activeGuideFloor.value = floorId || initial.defaultActiveFloorId || activeGuideFloor.value
  closeArrivalPanel()
  if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return

  const resetResult = await guideMapShellRef.value?.resetToViewBaseline?.({
    view: 'floor',
    floorId: activeGuideFloor.value,
    reason: rendererReason
  })
  if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return
  if (resetResult === 'applied') {
    renderedFloorId.value = activeGuideFloor.value
    guideModelState.completeViewBaselineReset(resetRequest)
    console.info('馆内三维模型已恢复楼层视觉基线:', { rendererReason })
  } else if (resetResult === 'not-ready' || resetResult === undefined) {
    guideModelState.queueViewBaselineReset(resetRequest)
  }
}

onShow(async () => {
  await nextTick()
  const detailReturnContext = guideModelState.consumePoiDetailReturn()
  if (detailReturnContext) {
    await resetGuideModelToFloorBaseline({
      rendererReason: 'poi-detail-close',
      floorId: detailReturnContext.floorId
    })
    await homeSearchPanelRef.value?.restoreResultListScroll?.()
    return
  }

  await homeSearchPanelRef.value?.restoreResultListScroll?.()
})

const handleMoreOutdoorNav = () => {
  if (!homeArrivalFeatureEnabled) return
  if (showArrivalPanel.value && showArrivalPanelCollapsed.value) {
    arrivalPanelRef.value?.expandPanel()
    showArrivalPanelCollapsed.value = false
    return
  }

  resetArrivalPanelState()
  showArrivalPanel.value = true
  showArrivalPanelCollapsed.value = false
  showRoutePlanner.value = false
  is3DMode.value = false
  guideOutdoorState.value = 'home'
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  isSimulatingRoute.value = false
  closeHomeSearchDock()
}

const handleExplainQuickTap = () => {
  handleTabChange('explain')
}

const handleHomeSearchExpandedChange = (expanded: boolean) => {
  homeSearchExpanded.value = expanded
  if (expanded) {
    pushPoiSearchOverlayHistory()
    return
  }

  removePoiSearchOverlayHistory()
}

const clearHomeSearchMapState = () => {
  homeCategoryModeActive.value = false
  searchVisiblePoiIds.value = null
  searchTargetFocusRequest.value = null
}

const handleHomeCategoryModeChange = (active: boolean) => {
  homeCategoryModeActive.value = active
  if (!active) {
    searchVisiblePoiIds.value = null
    return
  }

  closeArrivalPanel()
  showRoutePlanner.value = false
  isSimulatingRoute.value = false
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  is3DMode.value = true
  indoorView.value = 'floor'
}

const handleHomeSearchResultsChange = (state: PoiCategoryResultState) => {
  if (!state.active) {
    searchVisiblePoiIds.value = null
    return
  }

  searchVisiblePoiIds.value = [...state.visiblePoiIds]
  if (state.floorId) {
    activeGuideFloor.value = state.floorId
  }
}

const createSearchDetailUrl = (poi: MuseumPoi, context: PoiSearchContext) => {
  const resultCount = homeCategoryModeActive.value
    ? context.floorResultCount
    : context.resultCount
  const params = new URLSearchParams({
    source: 'search',
    searchOrigin: context.origin,
    searchKeyword: context.keyword,
    searchCategoryId: context.categoryId,
    searchFloorId: context.floorId,
    searchFloorLabel: context.floorLabel,
    resultCount: String(Math.max(1, resultCount)),
    floorResultCount: String(context.floorResultCount),
    visiblePoiIds: context.visiblePoiIds.join(','),
    listScrollTop: String(context.listScrollTop),
    id: poi.id,
    target: poi.name,
    floorId: poi.floorId,
    floorLabel: poi.floorLabel
  })

  return `/pages/facility/detail?${params.toString()}`
}

const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => {
  // The homepage keeps its renderer mounted; detail close is consumed by onShow once.
  searchTargetFocusRequest.value = null
  navigateToPoiDetail({
    url: createSearchDetailUrl(poi, context),
    floorId: poi.floorId,
    failureMessage: '点位详情打开失败，请重试'
  })
}

const handleHomeSearchCancel = () => {
  clearHomeSearchMapState()
  selectedGuidePoi.value = null
  isPoiCardCollapsed.value = false
  homeSearchExpanded.value = false
}

const handleHomeSearchOutsideTap = () => {
  closeHomeSearchDock()
}

const guideShellMode = computed(() => (is3DMode.value ? '3d' : '2d'))
const guideMapType = computed(() => (is3DMode.value ? 'indoor' : 'outdoor'))
const guideOutdoorVariant = computed(() => (
  guideOutdoorState.value === 'entrance' ? 'entrance' : 'home'
))
const guideSearchText = computed(() => {
  return '请输入地点进行搜索'
})

// 讲解页面处理
const handleExplainHallClick = async (hallId: string) => {
  const hall = explainHallItems.value.find((item) => item.id === hallId)
  uni.navigateTo({
    url: explainGuideStopListUrl(hallId, hall?.name || '讲解')
  })
}

const handleExplainBack = () => {
  currentTab.value = 'guide'
  syncTopTabToUrl('guide')
  loadTopTabData('guide')
  showRoutePlanner.value = false
  isSimulatingRoute.value = false
  isPoiCardCollapsed.value = false
}
</script>

<style scoped lang="scss">
.app-launch-overlay {
  position: fixed;
  inset: 0;
  z-index: 5000;
  display: flex;
  justify-content: center;
  overflow: hidden;
  background: #ffffff;
  opacity: 1;
  transition: opacity 0.36s ease, transform 0.36s ease;
}

.app-launch-overlay.leaving {
  opacity: 0;
  transform: scale(1.015);
  pointer-events: none;
}

.app-launch-stage {
  width: 100%;
  max-width: 430px;
  height: 100vh;
  height: 100dvh;
  min-height: 480px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: #ffffff;
}

.app-launch-content {
  width: 100%;
  padding: 0 28px;
  display: flex;
  align-items: center;
  flex-direction: column;
  box-sizing: border-box;
  transform: translateY(-3vh);
}

.app-launch-logo {
  width: min(84vw, 350px);
  height: auto;
}

.app-launch-name-en {
  margin-top: 20px;
  white-space: nowrap;
  font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  font-size: 13px;
  line-height: 18px;
  font-weight: 500;
  letter-spacing: 0;
  color: #262421;
}

.app-launch-name-cn {
  margin-top: 8px;
  white-space: nowrap;
  font-family: 'Songti SC', STSong, SimSun, serif;
  font-size: 36px;
  line-height: 48px;
  font-weight: 500;
  letter-spacing: 0;
  color: #000000;
}

.app-launch-progress {
  width: 112px;
  margin-top: 32px;
}

@media (max-width: 360px) {
  .app-launch-content {
    padding: 0 24px;
  }

  .app-launch-logo {
    width: calc(100vw - 52px);
  }

  .app-launch-name-cn {
    font-size: 32px;
    line-height: 43px;
  }
}

@media (max-height: 680px) {
  .app-launch-content {
    transform: translateY(-1vh);
  }

  .app-launch-progress {
    margin-top: 26px;
  }
}

.explain-page {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #f6f7f4;
  z-index: 10;
}

.guide-poi-card {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(env(safe-area-inset-bottom) + 24px);
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid #e5e6de;
  border-radius: 8px;
  box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12);
  z-index: 1003;
}

.guide-poi-card {
  padding: 10px 14px 14px;
}

.guide-poi-card.collapsed {
  padding: 8px 10px 10px;
}

.poi-card-handle {
  width: 38px;
  height: 4px;
  margin: 0 auto 10px;
  background: #d8dbd2;
  border-radius: 999px;
}

.poi-card-collapsed {
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.poi-card-mini-action {
  flex: 0 0 auto;
  height: 30px;
  padding: 0 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #151713;
  border-radius: 8px;
}

.poi-card-mini-text {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: var(--museum-accent);
}

.poi-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.poi-card-copy {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.poi-card-title {
  font-size: 17px;
  line-height: 24px;
  font-weight: 700;
  color: #151713;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.poi-card-subtitle {
  font-size: 12px;
  line-height: 17px;
  color: #696962;
}

.poi-card-header-actions {
  flex: 0 0 auto;
  display: flex;
  gap: 6px;
}

.poi-card-soft-action {
  height: 28px;
  padding: 0 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border: 1px solid #e4e5df;
  border-radius: 8px;
  background: #f5f7f2;
}

.poi-card-soft-text {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: #545861;
}

.poi-card-close {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: #f5f7f2;
}

.poi-card-close-text {
  font-size: 18px;
  line-height: 20px;
  color: #545861;
}

.poi-card-actions {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.poi-action {
  min-width: 0;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 6px;
  box-sizing: border-box;
  background: #f6f7f4;
  border: 1px solid #e5e6de;
  border-radius: 8px;
}

.poi-action.primary {
  background: #151713;
  border-color: #151713;
}

.poi-action-text {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: #151713;
  text-align: center;
}

.poi-action.primary .poi-action-text {
  color: var(--museum-accent);
}

.guide-home-dock {
  position: absolute;
  --museum-h5-page-width: min(100vw, 430px);
  left: 50%;
  right: auto;
  width: calc(var(--museum-h5-page-width) - 20px);
  bottom: calc(env(safe-area-inset-bottom) + 10px);
  padding: 7px;
  box-sizing: border-box;
  overflow: hidden;
  background:
    linear-gradient(180deg, rgba(224, 225, 0, 0.14) 0, rgba(224, 225, 0, 0) 46px),
    #443c2d;
  border: 1px solid #a66a42;
  border-radius: 8px;
  box-shadow: 0 12px 28px rgba(58, 50, 35, 0.26);
  transform: translateX(-50%);
  z-index: 1003;
}

.guide-home-dock::before {
  content: '';
  position: absolute;
  left: 7px;
  right: 7px;
  top: 0;
  height: 3px;
  background: var(--museum-accent);
}

.guide-home-dock.expanded {
  position: fixed;
  left: 50%;
  right: auto;
  top: 0;
  bottom: auto;
  width: var(--museum-h5-page-width);
  height: 100vh;
  height: 100dvh;
  height: var(--home-search-viewport-height, 100dvh);
  max-height: none;
  display: flex;
  flex-direction: column;
  padding: calc(env(safe-area-inset-top) + 12px) 14px calc(env(safe-area-inset-bottom) + 14px);
  background: #f7f8f2;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  transform: translateX(-50%);
  overflow: hidden;
  overscroll-behavior: contain;
  touch-action: pan-x pan-y;
  z-index: 1100;
}

.guide-home-dock.expanded::before {
  display: none;
}

/* #ifdef H5 */
.guide-home-dock.expanded {
  min-height: 100vh;
  min-height: 100dvh;
  max-width: var(--museum-h5-page-width);
}
/* #endif */

.guide-home-search-scrim {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background: transparent;
  z-index: 1002;
}

.guide-quick-actions {
  position: absolute;
  top: clamp(88px, 13vh, 112px);
  right: 14px;
  width: 52px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  z-index: 1002;
}

.guide-quick-action {
  width: 52px;
  min-height: 58px;
  padding: 7px 4px 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  box-sizing: border-box;
  background: rgba(245, 245, 237, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: 8px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
  backdrop-filter: blur(10px);
  transition:
    background 0.18s ease,
    border-color 0.18s ease,
    box-shadow 0.18s ease,
    transform 0.16s ease;
}

.guide-quick-action.active {
  background: var(--museum-accent);
  border-color: var(--museum-accent);
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(224, 225, 0, 0.35);
  animation: guide-quick-active-in 0.22s ease-out both;
}

.guide-quick-action:active {
  transform: translateY(1px) scale(0.98);
}

.guide-quick-icon {
  width: 22px;
  height: 22px;
  flex: 0 0 22px;
  position: relative;
  color: #151713;
}

.guide-quick-action.active .guide-quick-icon {
  color: #0a0b08;
}

.guide-quick-icon-floor {
  position: absolute;
  left: 3px;
  width: 16px;
  height: 9px;
  box-sizing: border-box;
  border: 1.7px solid currentColor;
  border-radius: 2px;
  transform: skewY(-18deg);
}

.guide-quick-icon-floor.top {
  top: 2px;
}

.guide-quick-icon-floor.bottom {
  top: 10px;
}

.guide-quick-pin {
  position: absolute;
  left: 5px;
  top: 2px;
  width: 12px;
  height: 16px;
  box-sizing: border-box;
  border: 1.8px solid currentColor;
  border-radius: 7px 7px 7px 0;
  transform: rotate(-45deg);
}

.guide-quick-pin::after {
  content: '';
  position: absolute;
  left: 3px;
  top: 3px;
  width: 4px;
  height: 4px;
  background: currentColor;
  border-radius: 50%;
}

.guide-quick-explain-wave {
  position: absolute;
  left: 2px;
  right: 2px;
  height: 4px;
  border: 1.8px solid currentColor;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  border-radius: 999px 999px 0 0;
}

.guide-quick-explain-wave.top {
  top: 3px;
}

.guide-quick-explain-wave.middle {
  top: 9px;
  left: 5px;
}

.guide-quick-explain-wave.bottom {
  top: 15px;
  right: 6px;
}

.guide-quick-action-text {
  width: 100%;
  font-size: 12px;
  line-height: 16px;
  font-weight: 700;
  color: #151713;
  text-align: center;
  white-space: nowrap;
}

.guide-quick-action.active .guide-quick-action-text {
  color: #0a0b08;
}

@keyframes guide-quick-active-in {
  from {
    transform: scale(0.96);
  }

  to {
    transform: scale(1);
  }
}

.route-sim-top-pill {
  position: absolute;
  top: 126px;
  left: 50%;
  max-width: calc(100% - 40px);
  height: 34px;
  padding: 0 14px;
  display: flex;
  align-items: center;
  box-sizing: border-box;
  background: rgba(21, 23, 19, 0.86);
  border-radius: 8px;
  transform: translateX(-50%);
  z-index: 1003;
}

.route-sim-top-text {
  min-width: 0;
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: #ffffff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.route-sim-step-bar {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(env(safe-area-inset-bottom) + 24px);
  min-height: 54px;
  padding: 10px 10px 10px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid #e5e6de;
  border-radius: 8px;
  box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12);
  z-index: 1003;
}

.route-sim-step-text {
  min-width: 0;
  flex: 1;
  font-size: 14px;
  line-height: 20px;
  font-weight: 700;
  color: #151713;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.route-sim-expand {
  height: 34px;
  padding: 0 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: #000000;
  border-radius: 8px;
}

.route-sim-expand-text {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: var(--museum-accent);
}

.entrance-tip {
  position: absolute;
  top: 126px;
  left: 30px;
  width: 168px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid #e5e6de;
  border-radius: 12px;
  z-index: 45;
}

.entrance-tip-text {
  font-size: 14px;
  line-height: 19px;
  font-weight: 500;
  color: #1f2329;
}

.indoor-gesture-hint {
  position: absolute;
  top: clamp(136px, 19vh, 190px);
  left: 50%;
  max-width: calc(100% - 112px);
  min-height: 38px;
  padding: 8px 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: rgba(9, 15, 10, 0.9);
  border: 1px solid rgba(255, 235, 59, 0.36);
  border-radius: 999px;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 58;
}

.indoor-gesture-text {
  min-width: 0;
  font-size: 13px;
  line-height: 18px;
  font-weight: 700;
  color: #f4ff2f;
  text-align: center;
  white-space: normal;
}

.home-shortcuts-card {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(env(safe-area-inset-bottom) + 128px);
  height: 84px;
  padding: 16px 10px 8px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid #ffffff;
  border-radius: 16px;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
  z-index: 45;
}

.home-shortcut-item {
  width: 62px;
  height: 60px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 4px;
}

.home-shortcut-icon {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: #ffffff;
  border: 1px solid #d9dcd2;
  border-radius: 10px;
}

.home-shortcut-abbr {
  font-size: 15px;
  line-height: 20px;
  font-weight: 700;
  color: #1f2329;
}

.home-shortcut-label {
  font-size: 12px;
  line-height: 16px;
  color: #1f2329;
  text-align: center;
}

.home-shortcut-item.active {
  .home-shortcut-icon {
    background: #000000;
    border-color: #000000;
  }

  .home-shortcut-abbr {
    color: var(--museum-accent);
  }
}

.search-bar-wrapper {
  position: absolute;
  top: 44px;
  left: 0;
  right: 0;
  z-index: 1003;
}

.guide-mode-row {
  position: absolute;
  top: 104px;
  left: 16px;
  right: 16px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  z-index: 1003;
}

.mode-switch {
  width: 220px;
  height: 34px;
  display: flex;
  align-items: center;
  padding: 4px;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.72);
  border: 1px solid rgba(230, 230, 230, 0.5);
  border-radius: 17px;
}

.mode-option {
  width: 106px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 13px;
}

.mode-option.active {
  background: #000000;
}

.mode-label {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: #1f2329;
}

.mode-option.active .mode-label {
  color: var(--museum-accent);
}

.mode-status-chip {
  width: 111px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.42);
  border-radius: 14px;
}

.mode-status-text {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: #696962;
}

.area-selector-wrapper {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(env(safe-area-inset-bottom) + 128px);
  z-index: 1003;
}

.guide-task-card {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(env(safe-area-inset-bottom) + 24px);
  height: 94px;
  padding: 16px 16px 10px;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  background: #ffffff;
  border: 1px solid #ffffff;
  border-radius: 16px;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
  z-index: 1003;
}

.guide-task-card.has-subtitle {
  height: auto;
  min-height: 128px;
}

.task-title {
  flex-shrink: 0;
  font-size: 17px;
  line-height: 24px;
  font-weight: 700;
  color: #000000;
}

.task-subtitle {
  margin-top: 4px;
  min-height: 20px;
  font-size: 12px;
  line-height: 18px;
  color: #696962;
}

.task-primary-btn {
  flex-shrink: 0;
  margin-top: 4px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000000;
  border-radius: 8px;
}

.task-primary-text {
  font-size: 14px;
  line-height: 19px;
  font-weight: 500;
  color: var(--museum-accent);
}

.entrance-bottom-card {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(env(safe-area-inset-bottom) + 24px);
  height: 158px;
  padding: 18px 16px 14px;
  box-sizing: border-box;
  background: #ffffff;
  border: 1px solid #ffffff;
  border-radius: 16px;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
  z-index: 45;
}

.entrance-card-title {
  display: block;
  font-size: 17px;
  line-height: 24px;
  font-weight: 700;
  color: #000000;
}

.entrance-card-desc {
  display: block;
  width: 305px;
  margin-top: 10px;
  font-size: 13px;
  line-height: 18px;
  color: #6b7178;
}

.entrance-actions {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 14px;
  display: flex;
  gap: 12px;
}

.entrance-btn {
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border-radius: 8px;
}

.entrance-btn.secondary {
  width: 126px;
  background: #ffffff;
  border: 1px solid #000000;
}

.entrance-btn.primary {
  width: 181px;
  background: #000000;
  border: 1px solid #000000;
}

.entrance-btn-text {
  font-size: 14px;
  line-height: 19px;
  font-weight: 500;
  color: #000000;
}

.entrance-btn.primary .entrance-btn-text {
  color: var(--museum-accent);
}

</style>
