<template>
  <view
    class="poi-search-panel"
    :class="[`variant-${variant}`, {
      expanded: showSearchContent,
      'is-collapsed': isHomeCollapsed,
      'category-results-open': isHomeCategoryMode
    }]"
    @touchstart="handlePanelTouchStart"
    @touchend="handlePanelTouchEnd"
    @touchcancel="resetPanelTouch"
    @mousedown="handlePanelMouseStart"
    @mousemove="handlePanelMouseMove"
    @mouseup="handlePanelMouseEnd"
    @mouseleave="resetPanelMouse"
  >
    <view v-if="showHomeFullscreenNav" class="home-fullscreen-nav">
      <view class="home-back-button" data-testid="poi-search-cancel" @tap.stop="handleCancel">
        <text class="home-back-icon">‹</text>
      </view>
      <text class="home-fullscreen-title">点位搜索</text>
      <view class="home-nav-spacer"></view>
    </view>

    <view class="search-box" @tap="handleSearchBoxTap" @click="handleSearchBoxTap">
      <view class="search-icon"></view>
      <input
        class="search-input"
        type="text"
        confirm-type="search"
        :focus="searchInputFocused"
        placeholder="请输入地点进行搜索"
        placeholder-class="search-placeholder"
        :value="searchDraftKeyword"
        @mousedown="handleSearchBoxTap"
        @click="handleSearchBoxTap"
        @tap="handleSearchBoxTap"
        @focus="handleSearchFocus"
        @input="handleSearchInput"
        @confirm="handleSearchConfirm"
      />
      <view v-if="searchDraftKeyword" class="clear-button" @tap.stop="handleSearchClear">
        <text class="clear-text">×</text>
      </view>
    </view>

    <view
      v-if="variant === 'home' && !showSearchContent && !isHomeCategoryMode"
      class="home-category-strip"
      aria-label="快捷查找"
    >
      <view
        v-for="item in homeCategories"
        :key="item.id"
        class="home-category-chip"
        :class="{ active: activeCategoryId === item.id, disabled: isCategoryDisabled(item) }"
        :data-testid="`poi-category-${item.id}`"
        :aria-disabled="isCategoryDisabled(item)"
        @tap="handleFacilityShortcut(item)"
      >
        <view class="home-category-icon">
          <svg class="poi-category-icon" viewBox="0 0 32 32" aria-hidden="true">
            <use :href="getPoiCategoryIconHref(item.id)" />
          </svg>
        </view>
        <text class="home-category-label">{{ item.label }}</text>
      </view>
    </view>

    <view
      v-if="isHomeCategoryMode"
      class="home-category-results"
      data-testid="poi-home-category-results"
    >
      <view class="home-category-results-header">
        <view class="home-category-results-copy">
          <text class="home-category-results-title">{{ activeCategory?.label || searchKeyword }}</text>
          <text class="home-category-results-meta">
            当前楼层 {{ activeFloor || '待确认' }} · {{ floorResults.length }} 个点位
          </text>
        </view>
        <view
          class="home-category-results-close"
          data-testid="poi-category-cancel"
          aria-label="取消分类查询"
          @tap.stop="handleCancel"
        >
          <text class="home-category-results-close-text">×</text>
        </view>
      </view>

      <text v-if="dataWarning" class="data-warning">{{ dataWarning }}</text>

      <view
        ref="resultListRef"
        class="result-list home-result-list"
        data-testid="poi-result-list"
        @scroll="handleResultListScroll"
      >
        <view
          v-for="item in floorResults"
          :key="item.id"
          class="result-row"
          :class="{ disabled: !isPoiLocatable(item) }"
          :data-testid="`poi-result-${item.id}`"
          :aria-disabled="!isPoiLocatable(item)"
          @tap="handleResultTap(item)"
        >
          <view class="pin-icon"></view>
          <view class="result-copy">
            <text class="result-name">{{ poiDisplayName(item) }}</text>
            <text class="result-meta">{{ poiResultMeta(item) }}</text>
          </view>
          <text v-if="isPoiLocatable(item)" class="result-arrow">›</text>
        </view>

        <view v-if="!floorResults.length" class="empty-state" data-testid="poi-empty">
          <text class="empty-title">{{ emptyTitle }}</text>
          <text class="empty-desc">{{ emptyDesc }}</text>
          <view
            v-if="loadError && !isLoading"
            class="retry-button"
            data-testid="poi-retry"
            @tap.stop="retryCurrentSearch"
          >
            <text class="retry-button-text">重新加载</text>
          </view>
        </view>
      </view>
    </view>

    <view v-if="showSearchContent" class="poi-search-content">
      <view v-if="variant === 'home'" class="home-collapse-handle" @tap.stop="handleCancel">
        <view class="home-collapse-bar"></view>
      </view>

      <view class="category-scroll" aria-label="快捷查找">
        <view class="category-grid" :class="{ 'home-shortcut-grid': variant === 'home' }">
          <view
            v-for="item in searchCategoryColumns"
            :key="item.id"
            class="category-item"
            :class="{ active: activeCategoryId === item.id, disabled: isCategoryDisabled(item) }"
            :data-testid="`poi-category-${item.id}`"
            :aria-disabled="isCategoryDisabled(item)"
            @tap="handleFacilityShortcut(item)"
          >
            <view class="category-icon-shell">
              <svg class="poi-category-icon" viewBox="0 0 32 32" aria-hidden="true">
                <use :href="getPoiCategoryIconHref(item.id)" />
              </svg>
            </view>
            <text class="category-label">{{ item.label }}</text>
          </view>
        </view>
      </view>

      <view class="result-card">
        <view class="result-card-header">
          <text class="museum-title">深圳自然博物馆</text>
          <text class="result-count">当前楼层 {{ floorResults.length }} 个点位</text>
        </view>

        <text v-if="dataWarning" class="data-warning result-card-warning">{{ dataWarning }}</text>

        <view class="result-body">
          <view class="floor-tabs">
            <view
              v-for="floor in displayFloors"
              :key="floor.label"
              class="floor-tab"
              :class="{ active: activeFloor === floor.label }"
              @tap="handleFloorChange(floor.label)"
            >
              <text class="floor-tab-text">{{ floor.label }}</text>
            </view>
          </view>

          <view
            ref="resultListRef"
            class="result-list"
            data-testid="poi-result-list"
            @scroll="handleResultListScroll"
          >
            <view
              v-for="item in floorResults"
              :key="item.id"
              class="result-row"
              :class="{ disabled: !isPoiLocatable(item) }"
              :data-testid="`poi-result-${item.id}`"
              :aria-disabled="!isPoiLocatable(item)"
              @tap="handleResultTap(item)"
            >
              <view class="pin-icon"></view>
              <view class="result-copy">
                <text class="result-name">{{ poiDisplayName(item) }}</text>
                <text class="result-meta">{{ poiResultMeta(item) }}</text>
              </view>
              <text v-if="isPoiLocatable(item)" class="result-arrow">›</text>
            </view>

            <view v-if="!floorResults.length" class="empty-state" data-testid="poi-empty">
              <text class="empty-title">{{ emptyTitle }}</text>
              <text class="empty-desc">{{ emptyDesc }}</text>
              <view
                v-if="loadError && !isLoading"
                class="retry-button"
                data-testid="poi-retry"
                @tap.stop="retryCurrentSearch"
              >
                <text class="retry-button-text">重新加载</text>
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import type {
  MuseumFloor,
  MuseumPoi
} from '@/domain/museum'
import {
  compareFloorsTopToBottom,
  isIndoorNavigableFloor
} from '@/domain/guideFloor'
import {
  guideUseCase
} from '@/usecases/guideUseCase'
import {
  createVisitorPoiPresentations
} from '@/view-models/visitorPoiPresentation'
import {
  HOME_POI_CATEGORIES,
  POI_CATEGORIES,
  getPoiCategoryIconHref,
  getPoiCategoryById,
  resolvePoiCategory,
  type PoiCategoryDefinition,
  type PoiCategoryId
} from '@/domain/poiCategories'
import type {
  GuidePoiSearchViewState,
  PoiCategoryResultState,
  PoiSearchContext
} from '@/domain/poiSearch'
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'

const props = withDefaults(defineProps<{
  initialKeyword?: string
  variant?: 'home' | 'page'
  autofocus?: boolean
  currentFloorId?: string
  currentFloorLabel?: string
  locationPreviewSelection?: boolean
}>(), {
  initialKeyword: '',
  variant: 'page',
  autofocus: false,
  currentFloorId: '',
  currentFloorLabel: '',
  locationPreviewSelection: false
})

const emit = defineEmits<{
  resultTap: [poi: MuseumPoi, context: PoiSearchContext]
  'expanded-change': [expanded: boolean]
  'category-mode-change': [active: boolean]
  'results-change': [state: PoiCategoryResultState]
  cancel: []
}>()

const homeCategories = HOME_POI_CATEGORIES
const searchCategories = POI_CATEGORIES
const searchCategoryColumns = computed(() => (
  props.variant === 'home' ? homeCategories : searchCategories
))

const searchKeyword = ref('')
const searchDraftKeyword = ref('')
const searchInputFocused = ref(false)
const homeExpanded = ref(false)
const homeCategoryMode = ref(false)
const activeCategoryId = ref<PoiCategoryId | ''>('')
const touchStartX = ref(0)
const touchStartY = ref(0)
const isPanelTouching = ref(false)
const isPanelMouseDown = ref(false)
const homeContentTapBlockedUntil = ref(0)
const floors = ref<MuseumFloor[]>([])
const activeFloor = ref('')
const pois = ref<MuseumPoi[]>([])
const searchViewState = ref<GuidePoiSearchViewState | null>(null)
const isLoading = ref(false)
const searchError = ref('')
const floorLoadError = ref('')
const excludedPoiCount = ref(0)
const duplicatePoiCount = ref(0)
const resultListScrollTop = ref(0)
const resultListRef = ref<HTMLElement | { $el?: HTMLElement } | null>(null)
let searchRequestSeq = 0

const collapseDragThreshold = 48
const homeExpandTapGuardMs = 360
const showSearchContent = computed(() => props.variant === 'page' || homeExpanded.value)
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
const showHomeFullscreenNav = computed(() => (
  props.variant === 'home'
  && showSearchContent.value
  && !shouldUseHostNavigation.value
))
const isHomeCategoryMode = computed(() => (
  props.variant === 'home' && homeCategoryMode.value && !homeExpanded.value
))
const isHomeCollapsed = computed(() => props.variant === 'home' && !showSearchContent.value)
const activeCategory = computed(() => getPoiCategoryById(activeCategoryId.value))
const loadError = computed(() => searchError.value || floorLoadError.value)
const categoryStatesById = computed(() => new Map(
  (searchViewState.value?.categories || []).map((item) => [item.definition.id, item] as const)
))

const displayFloors = computed(() => [...floors.value]
  .filter(isIndoorNavigableFloor)
  .sort(compareFloorsTopToBottom))

const floorResults = computed(() => (
  activeFloor.value
    ? pois.value.filter((poi) => poi.floorLabel === activeFloor.value)
    : []
))

const emptyTitle = computed(() => {
  if (isLoading.value) return '正在读取点位'
  if (loadError.value) return loadError.value
  return '当前楼层暂无匹配点位'
})

const emptyDesc = computed(() => {
  if (loadError.value) return '请检查网络后重试，当前搜索条件已保留'
  if (props.variant === 'home') return '当前楼层没有此类点位，可更换分类继续查看'
  return activeCategory.value
    ? `可切换楼层查看其他${activeCategory.value.label}点位`
    : '可切换楼层或更换关键词'
})

const isPoiLocatable = (poi: MuseumPoi) => Boolean(poi.positionGltf)

// 数据质量统计保留给诊断日志，普通游客只看到可恢复的加载状态。
const dataWarning = computed(() => {
  const diagnostic = {
    excluded: excludedPoiCount.value,
    duplicates: duplicatePoiCount.value,
    floorLoadError: floorLoadError.value
  }
  if (import.meta.env.DEV && Object.values(diagnostic).some(Boolean)) {
    console.debug('[guide-poi-diagnostics]', diagnostic)
  }
  return ''
})

const isCategoryDisabled = (category: PoiCategoryDefinition) => {
  const state = categoryStatesById.value.get(category.id)
  return isLoading.value
    || !state
    || state.disabled
}

const activeFloorOption = computed(() => (
  displayFloors.value.find((floor) => floor.label === activeFloor.value) || null
))

const createSearchContext = (): PoiSearchContext => {
  const visiblePoiIds = searchViewState.value?.visiblePoiIds || []
  const resultCount = pois.value.length

  return {
    origin: props.variant,
    keyword: searchKeyword.value,
    categoryId: activeCategoryId.value,
    floorId: searchViewState.value?.floorId || activeFloorOption.value?.id || props.currentFloorId || '',
    floorLabel: searchViewState.value?.floorLabel || activeFloor.value || props.currentFloorLabel || '',
    resultCount,
    floorResultCount: floorResults.value.length,
    visiblePoiIds,
    listScrollTop: resultListScrollTop.value
  }
}

const emitResultsState = () => {
  const context = createSearchContext()
  const active = props.variant === 'home'
    && (searchViewState.value?.mode === 'category' || searchViewState.value?.mode === 'keyword')
  emit('results-change', {
    ...context,
    visiblePoiIds: active ? context.visiblePoiIds : [],
    active
  })
}

const emitPendingHomeSearchResults = (floor?: Pick<MuseumFloor, 'id' | 'label'>) => {
  if (props.variant !== 'home') return

  const context = createSearchContext()
  emit('results-change', {
    ...context,
    floorId: floor?.id || context.floorId,
    floorLabel: floor?.label || context.floorLabel,
    visiblePoiIds: [],
    active: true,
    pending: true
  })
}

const setHomeCategoryMode = (active: boolean) => {
  const nextValue = props.variant === 'home' && active
  if (homeCategoryMode.value === nextValue) return
  homeCategoryMode.value = nextValue
  emit('category-mode-change', nextValue)
}

const syncActiveFloor = () => {
  if (!displayFloors.value.length) {
    activeFloor.value = ''
    return
  }

  const requestedFloor = displayFloors.value.find((floor) => (
    floor.id === props.currentFloorId
    || floor.label === props.currentFloorLabel
  ))
  if (props.variant === 'home' && requestedFloor) {
    activeFloor.value = requestedFloor.label
    return
  }

  if (displayFloors.value.some((floor) => floor.label === activeFloor.value)) return
  activeFloor.value = displayFloors.value.find((floor) => floor.label === '1F')?.label
    || displayFloors.value[0].label
}

const loadFloors = async () => {
  floorLoadError.value = ''
  try {
    floors.value = await guideUseCase.getFloors()
    syncActiveFloor()
  } catch (error) {
    console.error('加载点位搜索楼层失败:', error)
    if (!floors.value.length) activeFloor.value = ''
    floorLoadError.value = '楼层信息加载失败'
  }
}

const activeFloorId = () => (
  activeFloorOption.value?.id
  || searchViewState.value?.floorId
  || props.currentFloorId
  || ''
)

const commitPoiSearchViewState = async (
  state: GuidePoiSearchViewState,
  requestSeq: number
) => {
  if (requestSeq !== searchRequestSeq) return

  searchViewState.value = state
  pois.value = state.results
  activeFloor.value = state.floorLabel
  duplicatePoiCount.value = 0
  excludedPoiCount.value = 0
  await nextTick()
  if (requestSeq !== searchRequestSeq) return
  emitResultsState()
}

const runPoiSearchRequest = async (
  loader: () => Promise<GuidePoiSearchViewState>
) => {
  const requestSeq = ++searchRequestSeq
  isLoading.value = true
  searchError.value = ''

  try {
    const state = await loader()
    await commitPoiSearchViewState(state, requestSeq)
    return state
  } catch (error) {
    await handleLoadFailure(requestSeq, error, '加载点位搜索结果失败:')
    return null
  } finally {
    if (requestSeq === searchRequestSeq) isLoading.value = false
  }
}

const handleLoadFailure = async (requestSeq: number, error: unknown, message: string) => {
  if (requestSeq !== searchRequestSeq) return
  console.error(message, error)
  pois.value = []
  if (searchViewState.value) {
    searchViewState.value = {
      ...searchViewState.value,
      results: [],
      visiblePoiIds: []
    }
  }
  searchError.value = '点位加载失败'
  await nextTick()
  if (requestSeq !== searchRequestSeq) return
  if (props.variant === 'home' && (homeCategoryMode.value || Boolean(searchKeyword.value))) {
    emitPendingHomeSearchResults()
    return
  }
  emitResultsState()
}

const loadInitialSpacePoints = async () => {
  const requestSeq = ++searchRequestSeq
  isLoading.value = true
  searchError.value = ''

  try {
    const state = await guideUseCase.createInitialPoiSearchState(activeFloorId())
    if (requestSeq !== searchRequestSeq) return
    await commitPoiSearchViewState(state, requestSeq)
  } catch (error) {
    await handleLoadFailure(requestSeq, error, '加载点位基础数据失败:')
  } finally {
    if (requestSeq === searchRequestSeq) isLoading.value = false
  }
}

const loadPois = async (keyword = '') => {
  if (!keyword.trim()) {
    await loadInitialSpacePoints()
    return
  }

  const requestSeq = ++searchRequestSeq
  isLoading.value = true
  searchError.value = ''
  if (props.variant === 'home') {
    // Keep a pending query from briefly showing stale list or marker IDs.
    pois.value = []
    emitPendingHomeSearchResults()
  }
  try {
    const state = await guideUseCase.searchPoiKeyword(
      searchViewState.value,
      keyword,
      activeFloorId()
    )
    if (requestSeq !== searchRequestSeq) return
    await commitPoiSearchViewState(state, requestSeq)
  } catch (error) {
    await handleLoadFailure(requestSeq, error, '加载点位搜索结果失败:')
  } finally {
    if (requestSeq === searchRequestSeq) isLoading.value = false
  }
}

const refreshCurrentPoiSearchFloor = async (floorLabel: string) => {
  const floor = displayFloors.value.find((item) => item.label === floorLabel)
  if (!floor) return

  activeFloor.value = floor.label
  resultListScrollTop.value = 0
  const hasExplicitQuery = searchViewState.value?.mode === 'category'
    || searchViewState.value?.mode === 'keyword'
  if (props.variant === 'home' && hasExplicitQuery) {
    emitPendingHomeSearchResults(floor)
  }

  await runPoiSearchRequest(() => guideUseCase.changePoiSearchFloor(
    searchViewState.value,
    floor.id
  ))
}

const focusSearchInput = async () => {
  searchInputFocused.value = false
  await nextTick()
  searchInputFocused.value = true
}

const expandHomePanel = () => {
  if (props.variant !== 'home') return
  if (!homeExpanded.value) {
    homeContentTapBlockedUntil.value = Date.now() + homeExpandTapGuardMs
  }
  setHomeCategoryMode(false)
  homeExpanded.value = true
}

const shouldBlockHomeContentTap = () => (
  props.variant === 'home'
  && homeExpanded.value
  && Date.now() < homeContentTapBlockedUntil.value
)

// #ifdef H5
const homeSearchLockClass = 'guide-home-search-fullscreen-lock'
let removeViewportResizeListener: (() => void) | null = null

const updateHomeSearchViewportHeight = () => {
  if (typeof document === 'undefined') return
  const viewportHeight = window.visualViewport?.height || window.innerHeight
  document.documentElement.style.setProperty('--home-search-viewport-height', `${viewportHeight}px`)
}

const setH5HomeSearchLock = (locked: boolean) => {
  if (typeof document === 'undefined') return
  document.documentElement.classList.toggle(homeSearchLockClass, locked)
  document.body?.classList.toggle(homeSearchLockClass, locked)

  if (!locked) {
    document.documentElement.style.removeProperty('--home-search-viewport-height')
    removeViewportResizeListener?.()
    removeViewportResizeListener = null
    return
  }

  updateHomeSearchViewportHeight()
  if (!removeViewportResizeListener) {
    const target = window.visualViewport || window
    target.addEventListener('resize', updateHomeSearchViewportHeight)
    removeViewportResizeListener = () => target.removeEventListener('resize', updateHomeSearchViewportHeight)
  }
}
// #endif

const collapseHomePanel = () => {
  if (props.variant !== 'home') return
  homeExpanded.value = false
  searchInputFocused.value = false
  uni.hideKeyboard()
}

const resetSearchState = async () => {
  searchKeyword.value = ''
  searchDraftKeyword.value = ''
  activeCategoryId.value = ''
  resultListScrollTop.value = 0
  searchError.value = ''
  setHomeCategoryMode(false)
  homeExpanded.value = false
  searchInputFocused.value = false
  if (searchViewState.value?.mode !== 'default') {
    pois.value = []
    emitPendingHomeSearchResults()
  }
  await runPoiSearchRequest(() => guideUseCase.clearPoiSearch(
    searchViewState.value,
    activeFloorId()
  ))
}

// 从详情页返回时只清理搜索 UI 与分类 marker，不参与地图目标焦点的复位。
const returnToCollapsedAfterDetail = async () => {
  await resetSearchState()
  uni.hideKeyboard()
}

const handleCancel = async () => {
  await resetSearchState()
  uni.hideKeyboard()
  emit('cancel')
}

const resetPanelTouch = () => {
  isPanelTouching.value = false
  touchStartX.value = 0
  touchStartY.value = 0
}

const resetPanelMouse = () => {
  isPanelMouseDown.value = false
  touchStartX.value = 0
  touchStartY.value = 0
}

const getGesturePoint = (event: any) => {
  const touchPoint = event.changedTouches?.[0] || event.touches?.[0]
  if (touchPoint) return touchPoint
  if (typeof event.clientX === 'number' && typeof event.clientY === 'number') return event
  return null
}

const handlePanelTouchStart = (event: any) => {
  if (props.variant !== 'home' || !homeExpanded.value) return
  const point = getGesturePoint(event)
  if (!point) return
  touchStartX.value = point.clientX
  touchStartY.value = point.clientY
  isPanelTouching.value = true
}

const handlePanelTouchEnd = (event: any) => {
  if (!isPanelTouching.value || props.variant !== 'home' || !homeExpanded.value) {
    resetPanelTouch()
    return
  }
  const point = getGesturePoint(event)
  if (point) {
    const deltaX = point.clientX - touchStartX.value
    const deltaY = point.clientY - touchStartY.value
    if (deltaY > collapseDragThreshold && deltaY > Math.abs(deltaX) * 1.2) collapseHomePanel()
  }
  resetPanelTouch()
}

const handlePanelMouseStart = (event: any) => {
  if (props.variant !== 'home' || !homeExpanded.value) return
  const point = getGesturePoint(event)
  if (!point) return
  touchStartX.value = point.clientX
  touchStartY.value = point.clientY
  isPanelMouseDown.value = true
}

const handlePanelMouseMove = (event: any) => {
  if (!isPanelMouseDown.value || props.variant !== 'home' || !homeExpanded.value) return
  const point = getGesturePoint(event)
  if (!point) return
  const deltaX = point.clientX - touchStartX.value
  const deltaY = point.clientY - touchStartY.value
  if (deltaY > collapseDragThreshold && deltaY > Math.abs(deltaX) * 1.2) {
    collapseHomePanel()
    resetPanelMouse()
  }
}

const handlePanelMouseEnd = (event: any) => {
  if (!isPanelMouseDown.value || props.variant !== 'home' || !homeExpanded.value) {
    resetPanelMouse()
    return
  }
  const point = getGesturePoint(event)
  if (point) {
    const deltaX = point.clientX - touchStartX.value
    const deltaY = point.clientY - touchStartY.value
    if (deltaY > collapseDragThreshold && deltaY > Math.abs(deltaX) * 1.2) collapseHomePanel()
  }
  resetPanelMouse()
}

const enterFullSearch = () => {
  if (props.variant !== 'home' || homeExpanded.value) return
  const cameFromCategory = homeCategoryMode.value
  expandHomePanel()
  if (cameFromCategory) {
    activeCategoryId.value = ''
    searchKeyword.value = ''
    searchDraftKeyword.value = ''
    pois.value = []
    emitPendingHomeSearchResults()
    void loadInitialSpacePoints()
  }
}

const handleSearchFocus = () => enterFullSearch()

const handleSearchBoxTap = () => {
  if (props.variant !== 'home' || homeExpanded.value) return
  enterFullSearch()
  void focusSearchInput()
}

const handleSearchInput = (event: any) => {
  searchDraftKeyword.value = event.detail.value
  enterFullSearch()
}

const handleSearchClear = async () => {
  searchDraftKeyword.value = ''
  searchKeyword.value = ''
  activeCategoryId.value = ''
  setHomeCategoryMode(false)
  if (searchViewState.value?.mode !== 'default') {
    pois.value = []
    emitPendingHomeSearchResults()
  }
  await runPoiSearchRequest(() => guideUseCase.clearPoiSearch(
    searchViewState.value,
    activeFloorId()
  ))
  await focusSearchInput()
}

type SearchShortcutOptions = {
  homeCategoryResults?: boolean
}

const searchShortcut = async (
  categoryInput: PoiCategoryDefinition | PoiCategoryId,
  options: SearchShortcutOptions = {}
) => {
  const category = typeof categoryInput === 'string'
    ? getPoiCategoryById(categoryInput)
    : categoryInput
  if (!category || isCategoryDisabled(category)) return

  const requestSeq = ++searchRequestSeq
  const useHomeResultList = props.variant === 'home'
    && (options.homeCategoryResults ?? !homeExpanded.value)
  if (useHomeResultList) {
    homeExpanded.value = false
    searchInputFocused.value = false
    uni.hideKeyboard()
  }
  searchKeyword.value = category.label
  searchDraftKeyword.value = category.label
  activeCategoryId.value = category.id
  resultListScrollTop.value = 0
  searchError.value = ''
  setHomeCategoryMode(useHomeResultList)
  isLoading.value = true
  if (useHomeResultList) {
    // The category result replaces the current home list, so clear both
    // surfaces until its canonical IDs have resolved.
    pois.value = []
    emitPendingHomeSearchResults()
  }

  try {
    const state = await guideUseCase.selectPoiSearchCategory(
      searchViewState.value,
      category.id,
      activeFloorId()
    )
    if (requestSeq !== searchRequestSeq) return
    await commitPoiSearchViewState(state, requestSeq)
  } catch (error) {
    await handleLoadFailure(requestSeq, error, '加载点位分类搜索结果失败:')
  } finally {
    if (requestSeq === searchRequestSeq) isLoading.value = false
  }
}

const handleFacilityShortcut = (category: PoiCategoryDefinition) => {
  void searchShortcut(category, {
    // 快捷入口始终进入首页底部分类结果；关键词确认仍保留全屏搜索结果。
    homeCategoryResults: props.variant === 'home'
  })
}

const handleSearchConfirm = async (event?: any) => {
  const value = typeof event?.detail?.value === 'string'
    ? event.detail.value
    : searchDraftKeyword.value
  searchKeyword.value = value.trim()
  searchDraftKeyword.value = searchKeyword.value
  setHomeCategoryMode(false)
  if (props.variant === 'home' && !homeExpanded.value) expandHomePanel()

  activeCategoryId.value = ''
  resultListScrollTop.value = 0
  await loadPois(searchKeyword.value)
}

const handleFloorChange = (floor: string) => {
  if (activeFloor.value === floor) return
  void refreshCurrentPoiSearchFloor(floor)
}

const handleResultListScroll = (event: any) => {
  const scrollTop = Number(event?.detail?.scrollTop ?? event?.currentTarget?.scrollTop ?? 0)
  resultListScrollTop.value = Number.isFinite(scrollTop) ? Math.max(0, scrollTop) : 0
}

const getResultListElement = (): HTMLElement | null => {
  const target = resultListRef.value
  if (!target) return null
  if ('$el' in target) return target.$el || null
  return target as HTMLElement
}

const captureResultListScroll = () => {
  const scrollTop = Number(getResultListElement()?.scrollTop)
  if (Number.isFinite(scrollTop)) resultListScrollTop.value = Math.max(0, scrollTop)
}

const restoreResultListScroll = async () => {
  const scrollTop = resultListScrollTop.value
  if (scrollTop <= 0) return

  await nextTick()
  const resultListElement = getResultListElement()
  if (resultListElement) resultListElement.scrollTop = scrollTop
}

const poiPresentations = computed(() => createVisitorPoiPresentations(pois.value))
const poiDisplayName = (poi: MuseumPoi) => (
  poiPresentations.value.get(poi.id)?.displayName || '未命名点位'
)

const poiResultMeta = (poi: MuseumPoi) => {
  const categoryLabel = resolvePoiCategory(poi)?.label || poi.primaryCategory?.label || '其他'
  return isPoiLocatable(poi) ? categoryLabel : `${categoryLabel} · 暂无地图坐标`
}

const encodeQueryValue = (value: string | number) => encodeURIComponent(String(value))

const createPoiLocationQuery = (poi: MuseumPoi, context: PoiSearchContext) => {
  const values: Record<string, string | number> = {
    id: poi.id,
    target: poiDisplayName(poi),
    floorId: poi.floorId,
    floorLabel: poi.floorLabel,
    source: 'search',
    searchOrigin: context.origin,
    searchKeyword: context.keyword,
    searchCategoryId: context.categoryId,
    searchFloorId: context.floorId,
    searchFloorLabel: context.floorLabel,
    resultCount: context.resultCount,
    floorResultCount: context.floorResultCount,
    visiblePoiIds: context.visiblePoiIds.join(','),
    listScrollTop: context.listScrollTop
  }

  return Object.entries(values)
    .map(([key, value]) => `${key}=${encodeQueryValue(value)}`)
    .join('&')
}

const handleResultTap = (poi: MuseumPoi) => {
  if (shouldBlockHomeContentTap()) return
  if (!isPoiLocatable(poi)) {
    uni.showToast({ title: '该点位缺少地图坐标，暂无法定位', icon: 'none' })
    return
  }

  // H5 普通 view 的滚动事件并非始终派发，进入详情前直接读取真实位置。
  captureResultListScroll()
  const context = createSearchContext()
  if (props.variant === 'home' || props.locationPreviewSelection) {
    emit('resultTap', poi, context)
    return
  }

  uni.navigateTo({
    url: `/pages/facility/detail?${createPoiLocationQuery(poi, context)}`
  })
}

const retryCurrentSearch = async () => {
  if (floorLoadError.value) await loadFloors()
  if (activeCategory.value) {
    await searchShortcut(activeCategory.value)
    return
  }
  if (searchKeyword.value) {
    await loadPois(searchKeyword.value)
    return
  }
  await loadInitialSpacePoints()
}

const applyInitialKeyword = async (keyword: string) => {
  const normalizedKeyword = keyword.trim()
  searchKeyword.value = normalizedKeyword
  searchDraftKeyword.value = normalizedKeyword
  if (props.variant === 'page') homeExpanded.value = true
  if (props.variant === 'home' && normalizedKeyword) expandHomePanel()
  if (props.variant === 'home' && !normalizedKeyword) homeExpanded.value = false

  activeCategoryId.value = ''
  if (normalizedKeyword) {
    await loadPois(normalizedKeyword)
    return
  }
  await loadInitialSpacePoints()
}

watch(() => props.initialKeyword, (keyword) => {
  void applyInitialKeyword(keyword || '')
})

watch([() => props.currentFloorId, () => props.currentFloorLabel], () => {
  if (props.variant !== 'home') return
  const requestedFloor = displayFloors.value.find((floor) => (
    floor.id === props.currentFloorId
    || floor.label === props.currentFloorLabel
  ))
  if (!requestedFloor || requestedFloor.id === searchViewState.value?.floorId) return
  void refreshCurrentPoiSearchFloor(requestedFloor.label)
})

watch(showSearchContent, (expanded) => {
  emit('expanded-change', expanded)
  // #ifdef H5
  setH5HomeSearchLock(props.variant === 'home' && expanded)
  // #endif
}, { immediate: true })

onMounted(async () => {
  await loadFloors()
  await applyInitialKeyword(props.initialKeyword)
  if (props.autofocus) void focusSearchInput()
})

onUnmounted(() => {
  searchRequestSeq += 1
  // #ifdef H5
  setH5HomeSearchLock(false)
  // #endif
})

defineExpose({
  expandHomePanel,
  collapseHomePanel,
  resetSearchState,
  returnToCollapsedAfterDetail,
  focusSearchInput,
  searchShortcut,
  retryCurrentSearch,
  restoreResultListScroll
})
</script>

<style scoped lang="scss">
/* #ifdef H5 */
:global(html.guide-home-search-fullscreen-lock),
:global(body.guide-home-search-fullscreen-lock) {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}
/* #endif */

.poi-search-panel {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  color: #262421;
  font-family: '鸿蒙黑体', 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  overflow-x: hidden;
}

.variant-home.expanded {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  overscroll-behavior: contain;
}

.variant-page {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.home-fullscreen-nav {
  height: 38px;
  flex: 0 0 38px;
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr) 38px;
  align-items: center;
  margin-bottom: 10px;
}

.home-back-button,
.home-nav-spacer {
  width: 38px;
  height: 38px;
}

.home-back-button {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: #ffffff;
  border: 1px solid #e1e4da;
}

.home-back-icon {
  display: flex;
  width: 18px;
  height: 18px;
  align-items: center;
  justify-content: center;
  font-size: 0;
  line-height: 0;
  font-weight: 600;
  color: #151713;
}

.home-back-icon::before {
  content: '';
  width: 10px;
  height: 10px;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateX(2px) rotate(45deg);
}

.home-fullscreen-title {
  min-width: 0;
  font-size: 17px;
  line-height: 22px;
  font-weight: 800;
  color: #151713;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.search-box {
  height: 48px;
  flex: 0 0 48px;
  display: flex;
  align-items: center;
  padding: 0 14px;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid #d8dbd0;
  border-radius: 8px;
  box-shadow: 0 8px 18px rgba(36, 49, 42, 0.08);
}

.variant-home .search-box {
  background: #f5f5ed;
  border-color: rgba(224, 225, 0, 0.72);
  box-shadow: none;
}

.variant-home.is-collapsed .search-box {
  height: 38px;
  flex-basis: 38px;
  padding: 0 12px;
  border-radius: 7px;
}

.home-category-strip {
  margin-top: 10px;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
}

.variant-home.is-collapsed .home-category-strip {
  margin-top: 7px;
  gap: 6px;
}

.home-category-chip {
  min-width: calc((100% - 24px) / 5);
  flex: 0 0 calc((100% - 24px) / 5);
  min-height: 68px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 6px 2px 5px;
  box-sizing: border-box;
  background: #f5f5ed;
  border: 1px solid #dfe2d7;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(21, 23, 19, 0.1);
  transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.14s ease;
}

.variant-home.is-collapsed .home-category-chip {
  min-height: 68px;
}

.home-category-strip::-webkit-scrollbar {
  display: none;
}

.home-category-chip:active {
  transform: translateY(1px);
  background: #eef06d;
  border-color: #c8ca00;
}

.home-category-chip.active {
  background: #f2f48d;
  border-color: #bfc100;
}

.home-category-icon {
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
}

.poi-category-icon {
  display: block;
  width: 100%;
  height: 100%;
  color: #151713;
  pointer-events: none;
}

.home-category-chip.active .poi-category-icon,
.category-item.active .poi-category-icon {
  color: #151713;
}

.home-category-label {
  min-width: 0;
  max-width: 100%;
  font-size: 11px;
  line-height: 15px;
  font-weight: 700;
  color: #151713;
  text-align: center;
  white-space: nowrap;
}

.home-category-results {
  max-height: min(42vh, 350px);
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: #fffef7;
  border: 1px solid #e8e7d4;
  border-radius: 8px;
}

.home-category-results-header {
  min-height: 54px;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 10px 9px 12px;
  box-sizing: border-box;
  border-bottom: 1px solid #efeedf;
}

.home-category-results-copy {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.home-category-results-title {
  font-size: 14px;
  line-height: 19px;
  font-weight: 800;
  color: #151713;
}

.home-category-results-meta {
  font-size: 11px;
  line-height: 15px;
  color: #6e746c;
}

.home-category-results-close {
  width: 32px;
  height: 32px;
  flex: 0 0 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: #f0f1e9;
}

.home-category-results-close-text {
  font-size: 20px;
  line-height: 20px;
  color: #4f554d;
}

.home-result-list {
  min-height: 112px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

.search-icon {
  width: 19px;
  height: 19px;
  position: relative;
  flex-shrink: 0;
  margin-right: 14px;
  border: 2px solid #151713;
  border-radius: 50%;
  box-sizing: border-box;
}

.search-icon::after {
  content: '';
  position: absolute;
  right: -7px;
  bottom: -5px;
  width: 10px;
  height: 2px;
  background: #151713;
  border-radius: 2px;
  transform: rotate(45deg);
}

.variant-home.is-collapsed .search-icon {
  width: 17px;
  height: 17px;
  margin-right: 11px;
  border-width: 2px;
}

.variant-home.is-collapsed .search-icon::after {
  right: -6px;
  bottom: -4px;
  width: 9px;
}

.search-input {
  min-width: 0;
  flex: 1;
  height: 100%;
  font-size: 16px;
  line-height: 22px;
  font-weight: 500;
  color: #262421;
}

.search-input :deep(input),
.search-input :deep(.uni-input-input) {
  font-size: 16px;
  line-height: 22px;
}

.variant-home.is-collapsed .search-input,
.variant-home.is-collapsed .search-input :deep(input),
.variant-home.is-collapsed .search-input :deep(.uni-input-input) {
  font-size: 14px;
  line-height: 19px;
}

.search-placeholder {
  color: #8b8d85;
}

.variant-home.is-collapsed .search-placeholder {
  font-size: 14px;
}

.clear-button {
  width: 26px;
  height: 26px;
  flex-shrink: 0;
}

.clear-button {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: #eef0e8;
}

.variant-home.is-collapsed .clear-button {
  width: 32px;
  height: 32px;
}

.variant-home.is-collapsed .clear-text {
  font-size: 17px;
  line-height: 18px;
}

.clear-text {
  font-size: 18px;
  line-height: 20px;
  color: #5b6058;
}

.poi-search-content {
  margin-top: 12px;
}

.variant-page .poi-search-content {
  margin-top: 14px;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding-bottom: env(safe-area-inset-bottom);
}

.home-collapse-handle {
  height: 18px;
  margin: -4px 0 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.home-collapse-bar {
  width: 42px;
  height: 4px;
  background: #d5d8cf;
  border-radius: 999px;
}

.variant-home .poi-search-content {
  max-height: min(50vh, 430px);
  overflow-y: auto;
  padding-right: 2px;
}

.variant-home.expanded .poi-search-content {
  flex: 1;
  min-height: 0;
  max-height: none;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding-right: 0;
}

.category-scroll {
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 4px;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
}

.variant-home .category-scroll {
  width: calc(100% - 8px);
  margin: 0 4px;
  padding-bottom: 0;
}

.category-scroll::-webkit-scrollbar {
  display: none;
}

.category-grid {
  --category-gap: 8px;
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(2, minmax(70px, auto));
  grid-auto-columns: calc((100% - 32px) / 5);
  gap: var(--category-gap);
  width: calc(125% + 2px);
  min-width: calc(125% + 2px);
}

.category-item {
  min-width: 0;
  min-height: 70px;
  position: relative;
  padding: 9px 4px 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
  box-sizing: border-box;
  overflow: visible;
  background: #f5f5ed;
  border: 1px solid #dfe2d7;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(21, 23, 19, 0.08);
  transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.14s ease;
}

.category-item.active {
  background: #f2f48d;
  border-color: #bfc100;
}

.home-category-chip.disabled,
.category-item.disabled {
  opacity: 0.42;
  cursor: not-allowed;
}

.category-item.active::after {
  content: '';
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: -1px;
  height: 3px;
  background: var(--museum-accent);
  border-radius: 3px 3px 0 0;
}

.category-icon-shell {
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
}

.category-item:active {
  transform: translateY(1px);
  background: #eef06d;
  border-color: #c8ca00;
}

.variant-home .home-shortcut-grid {
  display: grid;
  grid-auto-flow: row;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  grid-template-rows: repeat(2, 72px);
  grid-auto-columns: auto;
  width: 100%;
  min-width: 320px;
  gap: 6px;
}

.variant-home .category-item {
  min-height: 72px;
  height: 72px;
  padding: 6px 2px 5px;
  gap: 4px;
}

.category-label {
  max-width: 100%;
  font-size: 11px;
  line-height: 15px;
  font-weight: 700;
  color: #151713;
  text-align: center;
  white-space: nowrap;
}

.category-item.active .category-label {
  color: #151713;
}

.section-title-row {
  position: relative;
  margin-top: 16px;
  margin-bottom: 10px;
  padding-left: 12px;
}

.section-title-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: 3px;
  width: 4px;
  height: 16px;
  background: var(--museum-accent);
}

.section-title {
  font-size: 14px;
  line-height: 20px;
  font-weight: 800;
  color: #151713;
}

.facility-chip-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 2px;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}

.facility-chip-row::-webkit-scrollbar {
  display: none;
}

.facility-chip {
  height: 34px;
  flex: 0 0 auto;
  position: relative;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 12px;
  box-sizing: border-box;
  background: #fffef6;
  border: 1px solid #e8e7d2;
  border-radius: 8px;
}

.facility-chip.active {
  background: #fffddc;
  border-color: #d6d700;
}

.facility-chip.active::after {
  content: '';
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: -1px;
  height: 3px;
  background: var(--museum-accent);
}

.chip-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 14px;
  position: relative;
  border: 2px solid currentColor;
  border-radius: 3px;
  color: #151713;
  box-sizing: border-box;
}

.facility-chip.active .chip-icon {
  color: #151713;
}

.facility-chip-text {
  font-size: 12px;
  line-height: 16px;
  font-weight: 600;
  color: #151713;
  white-space: nowrap;
}

.facility-chip.active .facility-chip-text {
  color: #151713;
}

.result-card {
  margin-top: 14px;
  overflow: hidden;
  background: #fffef7;
  border: 1px solid #ecebd8;
  border-radius: 8px;
  box-shadow: 0 10px 24px rgba(75, 78, 60, 0.08);
}

.data-warning {
  display: block;
  padding: 7px 10px;
  font-size: 11px;
  line-height: 16px;
  color: #665b18;
  background: #fffbd8;
  border-bottom: 1px solid #eee9b5;
}

.result-card-warning {
  flex: 0 0 auto;
}

.result-card-header {
  height: 46px;
  padding: 0 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  box-sizing: border-box;
  border-bottom: 1px solid #efeedf;
}

.museum-title,
.result-count {
  font-size: 12px;
  line-height: 16px;
  font-weight: 700;
}

.museum-title {
  color: #151713;
}

.result-count {
  color: #6e746c;
  white-space: nowrap;
}

.result-body {
  display: flex;
  min-height: 168px;
}

.floor-tabs {
  width: 62px;
  flex: 0 0 62px;
  padding: 9px 0;
  box-sizing: border-box;
  background: #f6f5e8;
}

.floor-tab {
  position: relative;
  height: 31px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #676c64;
}

.floor-tab.active {
  color: #151713;
  font-weight: 800;
}

.floor-tab.active::after {
  content: '';
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 2px;
  height: 3px;
  background: var(--museum-accent);
}

.floor-tab-text {
  font-size: 12px;
  line-height: 16px;
}

.result-list {
  min-width: 0;
  flex: 1;
}

.variant-page .category-grid,
.variant-page .category-scroll,
.variant-page .section-title-row,
.variant-page .facility-chip-row {
  flex: 0 0 auto;
}

.variant-page .result-card {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.variant-page .result-body {
  flex: 1;
  min-height: 0;
}

.variant-page .floor-tabs,
.variant-page .result-list {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

.variant-page .result-list {
  padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
  box-sizing: border-box;
}

.variant-home.expanded .category-grid,
.variant-home.expanded .category-scroll,
.variant-home.expanded .section-title-row,
.variant-home.expanded .facility-chip-row {
  flex: 0 0 auto;
}

.variant-home.expanded .result-card {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.variant-home.expanded .result-body {
  flex: 1;
  min-height: 0;
}

.variant-home.expanded .floor-tabs,
.variant-home.expanded .result-list {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

.result-row {
  min-height: 58px;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 12px;
  box-sizing: border-box;
  border-bottom: 1px solid #efefe1;
}

.result-row:last-child {
  border-bottom: 0;
}

.result-row.disabled {
  opacity: 0.58;
  background: #f5f5ef;
  cursor: not-allowed;
}

.result-row.disabled .pin-icon {
  border-color: #8b8f87;
}

.result-row.disabled .pin-icon::after {
  background: #8b8f87;
}

.pin-icon {
  width: 15px;
  height: 18px;
  flex: 0 0 15px;
  position: relative;
  border: 2px solid #151713;
  border-radius: 8px 8px 8px 0;
  transform: rotate(-45deg);
  box-sizing: border-box;
}

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

.result-copy {
  min-width: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.result-name,
.result-meta {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.result-name {
  font-size: 13px;
  line-height: 18px;
  font-weight: 700;
  color: #151713;
}

.result-meta {
  font-size: 11px;
  line-height: 15px;
  color: #6c716a;
}

.result-arrow {
  font-size: 20px;
  line-height: 20px;
  color: #8b9188;
}

.empty-state {
  min-height: 112px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 20px;
  box-sizing: border-box;
}

.empty-title {
  font-size: 14px;
  line-height: 20px;
  font-weight: 700;
  color: #151713;
}

.empty-desc {
  font-size: 12px;
  line-height: 17px;
  color: #7a7f78;
  text-align: center;
}

.retry-button {
  height: 34px;
  margin-top: 6px;
  padding: 0 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #151713;
  border-radius: 8px;
}

.retry-button-text {
  font-size: 12px;
  line-height: 16px;
  font-weight: 700;
  color: var(--museum-accent);
}

@media (max-width: 360px) {
  .category-grid {
    grid-template-rows: repeat(2, minmax(62px, auto));
  }

  .variant-page .category-item,
  .variant-home.expanded .category-item {
    min-height: 62px;
    padding: 7px 4px;
    gap: 5px;
  }

  .floor-tabs {
    width: 52px;
    flex-basis: 52px;
  }
}

</style>
