<template>
  <view class="guide-map-shell">
    <view class="map-layer" :class="`map-${mapType}`">
      <template v-if="mapType === 'indoor'">
        <!-- #ifdef H5 -->
        <ThreeMap
          v-if="effectiveIndoorModelSource"
          ref="indoorRendererRef"
          class="indoor-three-map"
          :asset-base-url="indoorAssetBaseUrl"
          :model-source="effectiveIndoorModelSource"
          :initial-floor-id="activeFloorId"
          :initial-view="indoorView"
          :show-controls="false"
          :show-poi="shouldShowIndoorPois"
          :target-focus="targetFocusRequest"
          :touch-gesture-mode="touchGestureMode"
          :target-focus-distance-factor="targetFocusDistanceFactor"
          :route-preview="routePreview"
          :show-route="showRoute"
          :disable-auto-exit="disableAutoExit"
          :auto-switch-threshold-low="autoSwitchThresholdLow"
          :auto-switch-threshold-high="autoSwitchThresholdHigh"
          @floor-change="handleThreeFloorChange"
          @poi-click="handlePoiClick"
          @selection-clear="handleSelectionClear"
          @target-focus="handleTargetFocus"
          @auto-switch="handleAutoSwitch"
          @initial-model-progress="handleInitialModelProgress"
          @initial-model-ready="handleInitialModelReady"
          @initial-model-failed="handleInitialModelFailed"
        />
        <!-- #endif -->
        <!-- #ifndef H5 -->
        <view class="indoor-platform-fallback">
          <text class="indoor-platform-title">馆内三维导览</text>
          <text class="indoor-platform-desc">当前端暂不支持 WebGL 三维展示，请在 H5 端查看。</text>
        </view>
        <!-- #endif -->
      </template>
      <view v-else class="outdoor-map-bg">
        <TencentMap
          class="outdoor-map"
          :active-floor="activeFloor"
          :floors="props.floors"
          :polylines="outdoorNavPolylines"
          @map-tap="handleMapTap"
        />
      </view>
      <view v-if="dimmed" class="dim-layer"></view>
    </view>

    <view
      v-if="showSearch"
      class="guide-search-field"
      :class="`variant-${searchVariant}`"
      :style="searchFieldStyle"
      @tap="handleSearchTap"
    >
      <view v-if="searchVariant === 'home'" class="search-icon leading">
        <view class="search-icon-circle"></view>
        <view class="search-icon-handle"></view>
      </view>
      <text class="search-field-text">{{ searchText }}</text>
      <view v-if="showVoiceIcon" class="voice-icon">
        <view class="voice-head"></view>
        <view class="voice-stem"></view>
        <view class="voice-base"></view>
      </view>
      <view v-else-if="searchVariant !== 'home'" class="search-icon">
        <view class="search-icon-circle"></view>
        <view class="search-icon-handle"></view>
      </view>
    </view>

    <view
      v-if="showModeRow"
      class="guide-mode-row"
      :class="`layout-${modeLayout}`"
      :style="modeRowStyle"
    >
      <template v-if="modeLayout === 'full'">
        <view
          class="mode-option"
          :class="{ active: activeMode === '2d' }"
          @tap="handleModeChange('2d')"
        >
          <text class="mode-label">馆外2D</text>
        </view>
        <view
          class="mode-option"
          :class="{ active: activeMode === '3d' }"
          @tap="handleModeChange('3d')"
        >
          <text class="mode-label">馆内3D</text>
        </view>
      </template>
      <template v-else>
        <view class="compact-mode-switch">
          <view
            class="mode-option"
            :class="{ active: activeMode === '2d' }"
            @tap="handleModeChange('2d')"
          >
            <text class="mode-label">馆外2D</text>
          </view>
          <view
            class="mode-option"
            :class="{ active: activeMode === '3d' }"
            @tap="handleModeChange('3d')"
          >
            <text class="mode-label">馆内3D</text>
          </view>
        </view>
        <view class="mode-status-chip" :class="`tone-${modeStatusTone}`">
          <text class="mode-status-text">{{ modeStatus }}</text>
        </view>
      </template>
    </view>

    <slot name="overlay"></slot>

    <view
      v-if="showIndoorRightControls && showLayerModeToggle"
      class="layer-mode-toggle"
      :style="layerModeToggleStyle"
      @tap="handleLayerModeToggle"
    >
      <text class="layer-mode-label">{{ layerModeActionLabel }}</text>
    </view>

    <view
      v-if="showIndoorRightControls && showFloor"
      class="floor-switcher"
      :class="`side-${floorSide}`"
      :style="floorSwitcherStyle"
    >
      <view
        v-if="showFloorHeader"
        class="floor-header"
        :class="{ active: layerMode === 'multi' }"
        @tap.stop="handleFloorHeaderTap"
      >
        <text class="floor-header-icon">▱</text>
        <text class="floor-header-label">{{ layerModeActionLabel }}</text>
      </view>
      <view
        class="floor-list"
      >
        <view
          v-for="floor in floorItems"
          :key="floor.id"
          class="floor-item"
          :class="{
            active: activeFloorId === floor.id,
            pending: loadingFloorId === floor.id,
            failed: failedFloorId === floor.id,
            disabled: isFloorSwitchDisabled(floor.id)
          }"
          @tap.stop="handleFloorChange(floor)"
        >
          <text class="floor-label">{{ floor.label }}</text>
        </view>
      </view>
    </view>

    <view v-if="showZoomControls" class="zoom-controls" :style="zoomControlsStyle">
      <view class="zoom-btn" @tap="handleZoomClick('in')">
        <text class="zoom-text">+</text>
      </view>
      <view class="zoom-divider"></view>
      <view class="zoom-btn" @tap="handleZoomClick('out')">
        <text class="zoom-text">−</text>
      </view>
    </view>

    <view v-if="showMoreControl" class="more-control" :style="moreControlStyle" @tap="handleMoreTap">
      <text class="more-control-text">更多</text>
    </view>

    <view v-if="tools.length" class="tool-stack" :style="toolStackStyle">
      <view
        v-for="tool in tools"
        :key="tool"
        class="tool-btn"
        :aria-label="tool"
        :title="tool"
        @tap="handleToolClick(tool)"
      >
        <svg
          v-if="toolIconType(tool) === 'reset'"
          class="tool-icon-svg"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path d="M5 7v5h5" />
          <path d="M6.2 13.4A6.2 6.2 0 1 0 7.4 6.7L5 9.1" />
        </svg>
        <svg
          v-else-if="toolIconType(tool) === 'top'"
          class="tool-icon-svg"
          viewBox="0 0 24 24"
          fill="none"
        >
          <rect x="6" y="6" width="12" height="12" rx="2" />
          <path d="M9 9h6v6H9z" />
        </svg>
        <svg
          v-else-if="toolIconType(tool) === 'oblique'"
          class="tool-icon-svg"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path d="M5 9.5 12 5l7 4.5-7 4.5-7-4.5Z" />
          <path d="M5 9.5v5L12 19l7-4.5v-5" />
          <path d="M12 14v5" />
        </svg>
        <svg
          v-else-if="toolIconType(tool) === 'clear'"
          class="tool-icon-svg"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path d="M7 7l10 10" />
          <path d="M17 7 7 17" />
        </svg>
        <text v-else class="tool-label">{{ tool }}</text>
      </view>
    </view>

    <slot></slot>
  </view>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import TencentMap from '@/components/map/TencentMap.vue'
// #ifdef H5
import ThreeMap from '@/components/map/ThreeMap.vue'
// #endif
import type {
  GuideModelSource,
  GuideRenderPoi
} from '@/domain/guideModel'
import type {
  GuideRouteResult
} from '@/domain/museum'
import {
  isIndoorNavigableFloor
} from '@/domain/guideFloor'

interface GuideFloorOption {
  id: string
  label: string
}

interface FloorSwitchEvent {
  floorId: string
  floorLabel: string
}

interface OutdoorNavPolyline {
  points: Array<{ latitude: number; longitude: number }>
  color: string
  width: number
}

type CameraViewMode = 'reset' | 'top' | 'oblique'

interface TargetPoiFocusRequest {
  requestId: number | string
  poiId: string
  name?: string
  floorId: string
  floorLabel?: string
  primaryCategoryZh?: string
  positionGltf?: [number, number, number]
  sourceObjectName?: string
}

interface TargetPoiFocusResult {
  requestId: number | string
  poiId: string
  floorId: string
  status: 'focused' | 'missing' | 'error'
  message?: string
}

type IndoorViewMode = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'
type TouchGestureMode = 'orbit' | 'pan'

interface InitialModelProgressEvent {
  progress: number
  message: string
  view: IndoorViewMode
  floorId?: string
  elapsedMs?: number
}

const props = withDefaults(defineProps<{
  searchText?: string
  activeMode?: '2d' | '3d'
  activeFloor?: string
  indoorView?: IndoorViewMode
  indoorInitialView?: IndoorViewMode
  layerMode?: LayerDisplayMode
  searchTop?: string
  floorTop?: string
  floorBottom?: string
  floorMaxHeight?: string
  layerModeToggleTop?: string
  toolsTop?: string
  tools?: string[]
  cameraView?: CameraViewMode
  searchVariant?: 'default' | 'home'
  showVoiceIcon?: boolean
  showModeRow?: boolean
  showFloorHeader?: boolean
  showZoomControls?: boolean
  showLocationControl?: boolean
  showMoreControl?: boolean
  toolsBottom?: string
  floorSide?: 'left' | 'right'
  zoomControlsTop?: string
  moreControlBottom?: string
  showSearch?: boolean
  showFloor?: boolean
  showLayerModeToggle?: boolean
  modeTop?: string
  modeLayout?: 'full' | 'status'
  modeStatus?: string
  modeStatusTone?: 'solid' | 'glass'
  mapType?: 'indoor' | 'outdoor'
  outdoorVariant?: 'home' | 'entrance'
  indoorAssetBaseUrl?: string
  indoorModelSource?: GuideModelSource
  floors?: GuideFloorOption[]
  normalizeFloorId?: (labelOrId: string) => string
  dimmed?: boolean
  targetFocusRequest?: TargetPoiFocusRequest | null
  touchGestureMode?: TouchGestureMode
  targetFocusDistanceFactor?: number
  routePreview?: GuideRouteResult | null
  showRoute?: boolean
  disableAutoExit?: boolean
  autoSwitchThresholdLow?: number
  autoSwitchThresholdHigh?: number
  outdoorNavPolylines?: OutdoorNavPolyline[]
}>(), {
  searchText: '请输入地点进行搜索',
  activeMode: '3d',
  activeFloor: '1F',
  indoorView: 'floor',
  indoorInitialView: 'floor',
  layerMode: 'single',
  searchTop: '16px',
  floorTop: '164px',
  floorBottom: '',
  floorMaxHeight: '',
  layerModeToggleTop: '154px',
  toolsTop: '406px',
  toolsBottom: '',
  tools: () => [] as string[],
  cameraView: 'reset',
  searchVariant: 'default',
  showVoiceIcon: false,
  showModeRow: true,
  showFloorHeader: false,
  showZoomControls: false,
  showLocationControl: false,
  showMoreControl: false,
  floorSide: 'right',
  zoomControlsTop: '520px',
  moreControlBottom: '34px',
  showSearch: true,
  showFloor: true,
  showLayerModeToggle: false,
  modeTop: '60px',
  modeLayout: 'full',
  modeStatus: '',
  modeStatusTone: 'solid',
  mapType: 'indoor',
  outdoorVariant: 'home',
  indoorAssetBaseUrl: '',
  indoorModelSource: undefined,
  floors: () => [] as GuideFloorOption[],
  normalizeFloorId: (labelOrId: string) => labelOrId,
  dimmed: false,
  targetFocusRequest: null,
  touchGestureMode: 'orbit',
  targetFocusDistanceFactor: 0.36,
  routePreview: null,
  showRoute: false,
  disableAutoExit: false,
  autoSwitchThresholdLow: 1.0,
  autoSwitchThresholdHigh: 1.3,
  outdoorNavPolylines: () => [] as OutdoorNavPolyline[]
})

const emit = defineEmits<{
  searchTap: []
  modeChange: [mode: '2d' | '3d']
  floorRequest: [event: FloorSwitchEvent]
  floorChange: [floor: string]
  floorSwitchFailed: [event: FloorSwitchEvent]
  toolClick: [tool: string]
  moreClick: []
  indoorViewChange: [view: IndoorViewMode]
  layerModeChange: [mode: LayerDisplayMode]
  poiClick: [poi: GuideRenderPoi]
  selectionClear: []
  targetFocus: [result: TargetPoiFocusResult]
  autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }]
  initialModelProgress: [event: InitialModelProgressEvent]
  initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }]
  initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }]
  mapTap: [location: { latitude: number; longitude: number }]
}>()

// 监听视角切换，循环切换：reset -> top -> oblique -> reset
watch(() => props.cameraView, (view) => {
  if (props.mapType !== 'indoor') return
  indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)

  if (view === 'reset') {
    indoorRendererRef.value?.resetCamera?.()
  } else if (view === 'top') {
    indoorRendererRef.value?.setCameraPreset?.('top')
  } else if (view === 'oblique') {
    indoorRendererRef.value?.setCameraPreset?.('oblique')
  }
})

const indoorRendererRef = ref<{
  switchFloor?: (floorId: string) => Promise<void> | void
  showOverview?: () => Promise<void> | void
  showMultiFloor?: () => Promise<void> | void
  resetCamera?: () => void
  setCameraPreset?: (preset: 'top' | 'oblique') => void
  zoomCamera?: (direction: 'in' | 'out', options?: { source?: 'button' | 'gesture' }) => void
  clearSelection?: (shouldEmit?: boolean) => void
  clearRoute?: () => Promise<void> | void
  disableAutoSwitchTemporarily?: (durationMs: number) => void
} | null>(null)

const indoorFloors = computed(() => props.floors.filter((floor) => isIndoorNavigableFloor(floor)))
const floorItems = computed(() => indoorFloors.value)
// #ifdef H5
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
// #endif
const showIndoorRightControls = computed(() => (
  props.mapType === 'indoor' && props.indoorView !== 'overview'
))

const activeFloorId = computed(() => {
  const matchedFloor = indoorFloors.value.find((floor) => (
    floor.id === props.activeFloor || floor.label === props.activeFloor
  ))
  if (matchedFloor) return matchedFloor.id

  const normalizedFloorId = props.normalizeFloorId(props.activeFloor)
  const normalizedFloor = indoorFloors.value.find((floor) => (
    floor.id === normalizedFloorId || floor.label === normalizedFloorId
  ))
  return normalizedFloor?.id || normalizedFloorId
})
const requestedFloorId = ref('')
const requestedFloorLabel = ref('')
const loadingFloorId = ref('')
const renderedFloorId = ref(activeFloorId.value)
const activeFloorSwitchRequestSeq = ref(0)
const renderedFloorSwitchRequestSeq = ref(0)
const failedFloorId = ref('')
let floorSwitchRequestSeq = 0

const searchFieldStyle = computed(() => ({
  top: props.searchTop
}))

const floorSwitcherStyle = computed(() => ({
  ...(props.floorBottom ? { bottom: props.floorBottom } : { top: props.floorTop })
}))

const layerModeToggleStyle = computed(() => ({
  top: props.layerModeToggleTop
}))

const modeRowStyle = computed(() => ({
  top: props.modeTop
}))

const toolStackStyle = computed(() => ({
  ...(props.toolsTop ? { top: props.toolsTop } : {}),
  ...(props.toolsBottom ? { bottom: props.toolsBottom } : {}),
  left: '18px'
}))

const zoomControlsStyle = computed(() => ({
  top: props.zoomControlsTop
}))

const moreControlStyle = computed(() => ({
  bottom: props.moreControlBottom
}))

const shouldShowIndoorPois = computed(() => (
  props.mapType === 'indoor'
) || Boolean(props.targetFocusRequest))
const nextLayerMode = computed<LayerDisplayMode>(() => (
  props.layerMode === 'multi' ? 'single' : 'multi'
))
const layerModeActionLabel = computed(() => (
  nextLayerMode.value === 'multi' ? '多层' : '单层'
))

const handleSearchTap = () => {
  emit('searchTap')
}

const handleModeChange = (mode: '2d' | '3d') => {
  emit('modeChange', mode)
}

const isStaleFloorSwitchError = (error: unknown) => (
  error instanceof Error && error.message === 'STALE_MODEL_LOAD'
)

const findFloorItemById = (floorId: string) => (
  floorItems.value.find((floor) => floor.id === floorId)
)

const isFloorSwitchDisabled = (floorId: string) => (
  Boolean(loadingFloorId.value) && loadingFloorId.value !== floorId
)

const clearFloorLoadingIfCurrent = (floorId: string) => {
  if (loadingFloorId.value === floorId) {
    loadingFloorId.value = ''
  }
}

const markFloorSwitchFailedIfUnrendered = (floorId: string, requestSeq: number) => {
  if (
    loadingFloorId.value !== floorId
    || activeFloorSwitchRequestSeq.value !== requestSeq
    || renderedFloorSwitchRequestSeq.value === requestSeq
  ) return

  failedFloorId.value = floorId
  clearFloorLoadingIfCurrent(floorId)
  emit('floorSwitchFailed', {
    floorId,
    floorLabel: requestedFloorId.value === floorId
      ? requestedFloorLabel.value || findFloorItemById(floorId)?.label || floorId
      : findFloorItemById(floorId)?.label || floorId
  })
}

const handleFloorChange = (floor: { id: string; label: string }) => {
  const floorId = floor.id
  if (!floorId || loadingFloorId.value) return
  if (floorId === renderedFloorId.value && activeFloorId.value === floorId && props.layerMode !== 'multi') {
    emit('floorChange', floorId)
    emit('indoorViewChange', 'floor')
    emit('layerModeChange', 'single')
    return
  }

  requestedFloorId.value = floorId
  requestedFloorLabel.value = floor.label
  loadingFloorId.value = floorId
  const requestSeq = ++floorSwitchRequestSeq
  activeFloorSwitchRequestSeq.value = requestSeq
  failedFloorId.value = ''
  indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
  emit('floorRequest', {
    floorId,
    floorLabel: floor.label
  })
  emit('indoorViewChange', 'floor')
  emit('layerModeChange', 'single')

  Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
    .then(() => {
      markFloorSwitchFailedIfUnrendered(floorId, requestSeq)
    })
    .catch((error) => {
      if (isStaleFloorSwitchError(error)) return
      failedFloorId.value = floorId
      clearFloorLoadingIfCurrent(floorId)
      emit('floorSwitchFailed', {
        floorId,
        floorLabel: floor.label
      })
      console.error('楼层切换失败:', error)
    })
}

const handleLayerModeChange = (mode: LayerDisplayMode) => {
  // 手动切换展示层数时临时禁用缩放自动切换 10 秒
  indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)

  if (mode === 'multi') {
    loadingFloorId.value = ''
    requestedFloorId.value = ''
    requestedFloorLabel.value = ''
    void indoorRendererRef.value?.showMultiFloor?.()
    emit('indoorViewChange', 'multi')
  } else {
    const floorId = activeFloorId.value
    const floorLabel = findFloorItemById(floorId)?.label || floorId
    if (!floorId) return
    if (floorId === renderedFloorId.value && props.layerMode !== 'multi') {
      emit('indoorViewChange', 'floor')
      emit('layerModeChange', mode)
      return
    }

    requestedFloorId.value = floorId
    requestedFloorLabel.value = floorLabel
    loadingFloorId.value = floorId
    const requestSeq = ++floorSwitchRequestSeq
    activeFloorSwitchRequestSeq.value = requestSeq
    failedFloorId.value = ''
    emit('floorRequest', {
      floorId,
      floorLabel
    })
    Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
      .then(() => {
        markFloorSwitchFailedIfUnrendered(floorId, requestSeq)
      })
      .catch((error) => {
        if (isStaleFloorSwitchError(error)) return
        failedFloorId.value = floorId
        clearFloorLoadingIfCurrent(floorId)
        emit('floorSwitchFailed', {
          floorId,
          floorLabel
        })
        console.error('楼层切换失败:', error)
      })
    emit('indoorViewChange', 'floor')
  }

  emit('layerModeChange', mode)
}

const handleLayerModeToggle = () => {
  handleLayerModeChange(nextLayerMode.value)
}

const handleFloorHeaderTap = () => {
  handleLayerModeChange(props.layerMode === 'multi' ? 'single' : 'multi')
}

const handleToolClick = (tool: string) => {
  if (props.mapType === 'indoor') {
    indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)

    if (tool === '复位') {
      indoorRendererRef.value?.resetCamera?.()
    } else if (tool === '俯视') {
      indoorRendererRef.value?.setCameraPreset?.('top')
    } else if (tool === '斜视') {
      indoorRendererRef.value?.setCameraPreset?.('oblique')
    } else if (tool === '清除') {
      indoorRendererRef.value?.clearSelection?.()
    }
  }

  emit('toolClick', tool)
}

const toolIconType = (tool: string) => {
  if (tool === '复位') return 'reset'
  if (tool === '俯视') return 'top'
  if (tool === '斜视') return 'oblique'
  if (tool === '清除') return 'clear'
  return 'text'
}

const handleZoomClick = (direction: 'in' | 'out') => {
  indoorRendererRef.value?.zoomCamera?.(direction, { source: 'button' })
  emit('toolClick', direction === 'in' ? '放大' : '缩小')
}

const handleMoreTap = () => {
  emit('moreClick')
}

const handleThreeFloorChange = (floorId: string) => {
  renderedFloorId.value = floorId
  if (loadingFloorId.value === floorId) {
    renderedFloorSwitchRequestSeq.value = activeFloorSwitchRequestSeq.value
  }
  if (failedFloorId.value === floorId) {
    failedFloorId.value = ''
  }
  clearFloorLoadingIfCurrent(floorId)
  emit('floorChange', floorId)
  emit('indoorViewChange', 'floor')
  emit('layerModeChange', 'single')
}

const handlePoiClick = (poi: GuideRenderPoi) => {
  emit('poiClick', poi)
}

const handleSelectionClear = () => {
  emit('selectionClear')
}

const handleTargetFocus = (result: TargetPoiFocusResult) => {
  emit('targetFocus', result)
}

const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }) => {
  emit('autoSwitch', event)
}

const handleInitialModelProgress = (event: InitialModelProgressEvent) => {
  emit('initialModelProgress', event)
}

const handleInitialModelReady = (event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }) => {
  emit('initialModelReady', event)
}

const handleInitialModelFailed = (event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }) => {
  emit('initialModelFailed', event)
}

const handleMapTap = (location: { latitude?: number; longitude?: number }) => {
  const { latitude, longitude } = location
  if (latitude !== undefined && longitude !== undefined) {
    emit('mapTap', { latitude, longitude })
  }
}

// 暴露 clearRoute 方法供父组件显式调用路线清除
defineExpose({
  clearRoute: () => {
    indoorRendererRef.value?.clearRoute?.()
  }
})
</script>

<style scoped lang="scss">
.guide-map-shell {
  position: relative;
  z-index: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #ffffff;
  color: #000000;
}

.map-layer {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
  background: var(--museum-bg-map);
  border: 0;
  box-sizing: border-box;
  z-index: 0;
}

.indoor-three-map,
.outdoor-map {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.dim-layer {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.28);
  z-index: 2;
}

.outdoor-map-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: #f6f8f5;
}

.indoor-platform-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 24px;
  box-sizing: border-box;
  background: #f1f3ee;
  text-align: center;
}

.indoor-platform-title {
  font-size: 18px;
  line-height: 24px;
  font-weight: 700;
  color: #1f2329;
}

.indoor-platform-desc {
  max-width: 260px;
  font-size: 13px;
  line-height: 20px;
  color: #626a73;
}

.guide-search-field {
  position: absolute;
  left: 16px;
  right: 16px;
  height: 36px;
  display: flex;
  align-items: center;
  padding: 0 10px;
  box-sizing: border-box;
  background: var(--museum-bg-cream);
  border: 1px solid #ffffff;
  border-radius: 6px;
  z-index: 40;
}

.guide-search-field.variant-home {
  left: 20px;
  right: 20px;
  height: 46px;
  padding: 0 14px;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid #d7dad3;
  border-radius: 8px;
  box-shadow: 0 8px 18px rgba(36, 49, 42, 0.08);
}

.search-field-text {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  line-height: 19px;
  color: #8b8b84;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.variant-home .search-field-text {
  padding: 0 10px;
  font-size: 15px;
  line-height: 22px;
  color: #8b9097;
}

.search-icon {
  position: relative;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.search-icon.leading {
  width: 22px;
  height: 22px;
}

.search-icon.leading .search-icon-circle {
  top: 2px;
  left: 1px;
  width: 14px;
  height: 14px;
  border-width: 2px;
}

.search-icon.leading .search-icon-handle {
  left: 14px;
  top: 15px;
  width: 10px;
  height: 2px;
}

.voice-icon {
  position: relative;
  width: 22px;
  height: 24px;
  flex: 0 0 22px;
}

.voice-head {
  position: absolute;
  left: 6px;
  top: 1px;
  width: 10px;
  height: 15px;
  box-sizing: border-box;
  border: 2px solid #151713;
  border-radius: 7px;
}

.voice-stem {
  position: absolute;
  left: 10px;
  top: 15px;
  width: 2px;
  height: 6px;
  background: #151713;
  border-radius: 2px;
}

.voice-base {
  position: absolute;
  left: 5px;
  bottom: 0;
  width: 12px;
  height: 2px;
  background: #151713;
  border-radius: 2px;
}

.search-icon-circle {
  position: absolute;
  top: 2px;
  left: 1px;
  width: 9px;
  height: 9px;
  border: 1.4px solid #1f2329;
  border-radius: 50%;
  box-sizing: border-box;
}

.search-icon-handle {
  position: absolute;
  left: 10px;
  top: 10px;
  width: 8px;
  height: 1.4px;
  background: #1f2329;
  border-radius: 2px;
  transform: rotate(45deg);
  transform-origin: left center;
}

.guide-mode-row {
  position: absolute;
  left: 16px;
  right: 16px;
  height: 34px;
  z-index: 40;
}

.guide-mode-row.layout-full {
  display: flex;
  align-items: center;
  padding: 3px;
  box-sizing: border-box;
  background: #ffffff;
  border: 1px solid #e6e6dd;
  border-radius: 17px;
}

.guide-mode-row.layout-status {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.compact-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 {
  flex: 1;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 13px;
}

.layout-status .mode-option {
  width: 106px;
  flex: 0 0 106px;
}

.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;
  box-sizing: border-box;
  background: #ffffff;
  border: 1px solid #e6e6e6;
  border-radius: 14px;
}

.mode-status-chip.tone-glass {
  background: rgba(255, 255, 255, 0.42);
  border-color: transparent;
}

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

.floor-switcher {
  position: absolute;
  width: 44px;
  height: auto;
  padding: 1px;
  box-sizing: border-box;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.84);
  border: 1px solid #dde5df;
  border-radius: 8px;
  box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
  z-index: 35;
}

.floor-list {
  display: flex;
  flex-direction: column;
  overflow: visible;
}

.floor-switcher.side-right {
  right: 16px;
}

.floor-switcher.side-left {
  left: 18px;
}

.floor-header {
  position: relative;
  min-height: 38px;
  padding: 5px 4px 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  box-sizing: border-box;
  background: #ffffff;
  border-radius: 8px 8px 0 0;
}

.floor-header.active {
  background: #000000;
}

.floor-header::after {
  content: '';
  position: absolute;
  left: 7px;
  right: 7px;
  bottom: 0;
  height: 1px;
  background: #ecede7;
}

.floor-header-icon {
  width: 16px;
  height: 16px;
  position: relative;
  display: block;
  color: #151713;
  font-size: 0;
  line-height: 0;
}

.floor-header-icon::before,
.floor-header-icon::after {
  content: '';
  position: absolute;
  box-sizing: border-box;
  border: 1.8px solid currentColor;
  border-radius: 2px;
}

.floor-header-icon::before {
  left: 2px;
  top: 4px;
  width: 11px;
  height: 9px;
}

.floor-header-icon::after {
  left: 5px;
  top: 1px;
  width: 11px;
  height: 9px;
  background: #ffffff;
}

.floor-header.active .floor-header-icon {
  color: var(--museum-accent);
}

.floor-header-label {
  font-size: 10px;
  line-height: 12px;
  color: #545861;
}

.floor-header.active .floor-header-label {
  color: var(--museum-accent);
}

.layer-mode-toggle {
  position: absolute;
  right: 16px;
  width: 46px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: #ffffff;
  border: 1px solid #dde5df;
  border-radius: 8px;
  box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
  z-index: 36;
}

.layer-mode-toggle:active {
  background: #f5f7f2;
}

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

.floor-item {
  position: relative;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #ffffff;
}

.floor-item:first-child {
  border-radius: 7px 7px 0 0;
}

.floor-item:last-child {
  border-radius: 0 0 7px 7px;
}

.floor-item:not(:last-child)::after {
  content: '';
  position: absolute;
  left: 7px;
  right: 7px;
  bottom: 0;
  height: 1px;
  background: #ecede7;
}

.floor-item.active {
  background: #000000;
}

.floor-item.pending {
  pointer-events: none;
  opacity: 0.72;
}

.floor-item.pending::before {
  content: '';
  position: absolute;
  inset: 5px;
  border: 1px solid rgba(224, 225, 0, 0.9);
  border-radius: 6px;
}

.floor-item.disabled {
  pointer-events: none;
  opacity: 0.46;
}

.floor-item.failed:not(.pending) .floor-label {
  color: #b84a4a;
}

.floor-label {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: #545861;
}

.floor-item.active .floor-label {
  color: var(--museum-accent);
}

.tool-stack {
  position: absolute;
  left: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 35;
}

.tool-btn {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid #e4e5df;
  border-radius: 8px;
  box-sizing: border-box;
}

.tool-btn:active {
  background: #f6f7f4;
  transform: translateY(1px);
}

.tool-icon-svg {
  width: 22px;
  height: 22px;
  stroke: #151713;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

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

.zoom-controls {
  position: absolute;
  right: 18px;
  width: 48px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid #dde5df;
  border-radius: 8px;
  box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
  z-index: 35;
}

.zoom-btn {
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.zoom-divider {
  height: 1px;
  margin: 0 9px;
  background: #ecede7;
}

.zoom-text {
  font-size: 28px;
  line-height: 32px;
  font-weight: 300;
  color: #151713;
}

.location-control,
.more-control {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid #dde5df;
  border-radius: 8px;
  box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
  z-index: 35;
}

.location-control {
  left: 18px;
  width: 48px;
  height: 48px;
}

.location-ring {
  width: 20px;
  height: 20px;
  box-sizing: border-box;
  border: 2px solid #151713;
  border-radius: 50%;
}

.location-ring::before,
.location-ring::after {
  content: '';
  position: absolute;
  background: #151713;
  border-radius: 2px;
}

.location-ring::before {
  left: 23px;
  top: 10px;
  width: 2px;
  height: 28px;
}

.location-ring::after {
  left: 10px;
  top: 23px;
  width: 28px;
  height: 2px;
}

.location-dot {
  position: absolute;
  width: 6px;
  height: 6px;
  background: #151713;
  border-radius: 50%;
}

.more-control {
  right: 18px;
  min-width: 54px;
  height: 36px;
  padding: 0 12px;
}

.more-control-text {
  font-size: 13px;
  line-height: 18px;
  font-weight: 500;
  color: #151713;
}

@media (min-width: 768px) {
  .guide-map-shell {
    max-width: 430px;
    margin: 0 auto;
    box-shadow: 0 12px 36px rgba(36, 49, 42, 0.16);
  }
}
</style>
