<script setup>
import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import Sortable from 'sortablejs'
import { api } from '../lib/api'
import { toApiUrl } from '../lib/runtime'
import lockResetIcon from '../assets/icons/lock_reset.svg'
import deleteIcon from '../assets/icons/delete.svg'
import SvgIcon from '../components/SvgIcon.vue'
import { useAuthStore } from '../stores/auth'
import { useToast } from '../composables/useToast'

const auth = useAuthStore()
const toast = useToast()
const globalRightRailOpen = inject('rightRailOpen', ref(true))
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
const isAdmin = computed(() => !!auth.user?.isAdmin)

const activeTab = ref('featured')
const tierlistsMode = ref('requests')

const games = ref([])
const selectedGameId = ref('')
const selectedGame = ref(null)
const featuredGameIds = ref([])
const gameAdminQuery = ref('')
const gameAdminSort = ref('recent')

const customItems = ref([])
const customItemQuery = ref('')
const customItemPage = ref(1)
const customItemLimit = ref(50)
const customItemTotal = ref(0)
const customItemOrphanOnly = ref(false)
const customItemModalTargetGameId = ref('')
const customItemModalGameQuery = ref('')
const customItemModalGameSort = ref('recent')

const adminTierLists = ref([])
const adminTierListQuery = ref('')
const adminTierListPage = ref(1)
const adminTierListLimit = ref(50)
const adminTierListTotal = ref(0)
const templateRequests = ref([])
const importModalOpen = ref(false)
const importModalMode = ref('existing')
const importModalTierList = ref(null)
const importModalItems = ref([])
const importModalTargetGameId = ref('')
const importModalNewGameId = ref('')
const importModalNewGameName = ref('')
const previewModalOpen = ref(false)
const previewTierList = ref(null)
const userEditModalOpen = ref(false)
const userPasswordModalOpen = ref(false)
const userDeleteModalOpen = ref(false)
const userRoleModalOpen = ref(false)
const customItemModalOpen = ref(false)
const customItemDeleteModalOpen = ref(false)
const customItemModalHistoryActive = ref(false)
const modalTargetUser = ref(null)
const modalPasswordDraft = ref('')
const modalRoleNextAdmin = ref(false)
const modalUserDraftEmail = ref('')
const modalUserDraftNickname = ref('')
const modalUserDraftIsAdmin = ref(false)
const modalTargetCustomItem = ref(null)
const customItemModalDraftLabel = ref('')
const customItemModalLabelSaving = ref(false)

const users = ref([])
const userQuery = ref('')
const userSort = ref('recent')
const userSortDirection = ref('desc')
const imageStats = ref(null)
const imageQueue = ref({ concurrency: 1, activeCount: 0, pendingCount: 0 })
const imageRecentJobs = ref([])
const imageStatsMonth = ref('')
const imageStatsLimit = ref(12)
const imageResetModalOpen = ref(false)

const error = ref('')
const success = ref('')

const newGameId = ref('')
const newGameName = ref('')

const uploadFiles = ref([])
const uploadItemDrafts = ref([])
const thumbFile = ref(null)
const itemPreviewUrls = ref([])
const isItemDragOver = ref(false)
const isThumbDragOver = ref(false)
const thumbPreviewUrl = ref('')
const itemFileInput = ref(null)
const thumbFileInput = ref(null)
const featuredListEl = ref(null)
const featuredSortable = ref(null)
const userAvatarInputs = ref({})
const isGameLoading = ref(false)
const gameCreateModalOpen = ref(false)
const previousBodyOverflow = ref('')

const hasSelectedGame = computed(() => !!selectedGame.value?.game?.id)
const canApplyThumbnail = computed(() => !!thumbFile.value && !!selectedGameId.value)
const canAddItem = computed(() => uploadItemDrafts.value.length > 0 && uploadItemDrafts.value.every((item) => !!item.label.trim()) && !!selectedGameId.value)
const customItemPageCount = computed(() => Math.max(1, Math.ceil(customItemTotal.value / customItemLimit.value)))
const adminTierListPageCount = computed(() => Math.max(1, Math.ceil(adminTierListTotal.value / adminTierListLimit.value)))
const featuredGames = computed(() =>
  featuredGameIds.value
    .map((gameId) => games.value.find((game) => game.id === gameId))
    .filter(Boolean)
)
const availableGamesForFeatured = computed(() => games.value.filter((game) => !featuredGameIds.value.includes(game.id)))
const filteredAdminGames = computed(() => {
  const query = gameAdminQuery.value.trim().toLowerCase()
  const list = games.value.filter((game) => {
    if (!query) return true
    const haystack = `${game.name || ''} ${game.id || ''}`.toLowerCase()
    return haystack.includes(query)
  })

  return list.slice().sort((a, b) => {
    if (gameAdminSort.value === 'oldest') return Number(a.createdAt || 0) - Number(b.createdAt || 0)
    return Number(b.createdAt || 0) - Number(a.createdAt || 0)
  })
})
const importModalItemCount = computed(() => importModalItems.value.length)
const activeTabTitle = computed(() => {
  if (activeTab.value === 'featured') return '목록 관리'
  if (activeTab.value === 'game-admin') return '게임 관리'
  if (activeTab.value === 'items') return '아이템 관리'
  if (activeTab.value === 'tierlists') {
    return tierlistsMode.value === 'requests' ? '템플릿 요청 관리' : '전체 티어표 관리'
  }
  return '회원 관리'
})
const activeTabDescription = computed(() => {
  if (activeTab.value === 'featured') {
    return '홈 화면 상단에 고정 노출되는 게임 순서를 따로 관리합니다.'
  }
  if (activeTab.value === 'game-admin') {
    return '게임 생성, 선택, 썸네일, 기본 아이템 관리를 전용 작업 화면으로 분리했습니다.'
  }
  if (activeTab.value === 'items') {
    return '사용자 업로드와 관리자 템플릿 이미지를 함께 검수하고, 필요한 게임에 직접 연결할 수 있어요.'
  }
  if (activeTab.value === 'tierlists') {
    return tierlistsMode.value === 'requests'
      ? '사용자 요청 기반으로 새 템플릿 생성이나 템플릿 업데이트를 승인합니다.'
      : '공개/비공개 포함 전체 티어표를 확인하고, 추가 아이템을 템플릿으로 가져올 수 있어요.'
  }
  return '계정 정보, 권한, 비밀번호와 최근 활동을 더 가볍게 확인하고 수정합니다.'
})
const adminOverviewStats = computed(() => {
  const publishedTierLists = adminTierLists.value.filter((tierList) => tierList.isPublic).length
  const pendingRequests = templateRequests.value.length
  const orphanItems = customItems.value.filter((item) => item.usageCount === 0).length
  const adminCount = users.value.filter((user) => user.isAdmin).length

  if (activeTab.value === 'featured') {
    return [
      { label: '전체 게임', value: `${games.value.length}` },
      { label: '상단 고정', value: `${featuredGameIds.value.length}/50` },
      { label: '추가 가능', value: `${Math.max(0, 50 - featuredGameIds.value.length)}` },
    ]
  }
  if (activeTab.value === 'game-admin') {
    return [
      { label: '전체 게임', value: `${games.value.length}` },
      { label: '선택 상태', value: hasSelectedGame.value ? '활성' : '대기' },
      { label: '기본 아이템', value: `${selectedGame.value?.items?.length || 0}` },
    ]
  }
  if (activeTab.value === 'items') {
    return [
      { label: '검색 결과', value: `${customItemTotal.value}` },
      { label: '미사용', value: `${orphanItems}` },
      { label: '템플릿 아이템', value: `${customItems.value.filter((item) => item.sourceType === 'template').length}` },
    ]
  }
  if (activeTab.value === 'tierlists') {
    return tierlistsMode.value === 'requests'
      ? [
          { label: '대기 요청', value: `${pendingRequests}` },
          { label: '생성 요청', value: `${templateRequests.value.filter((request) => request.type === 'create').length}` },
          { label: '업데이트 요청', value: `${templateRequests.value.filter((request) => request.type === 'update').length}` },
        ]
      : [
          { label: '검색 결과', value: `${adminTierListTotal.value}` },
          { label: '공개 티어표', value: `${publishedTierLists}` },
          { label: '현재 페이지', value: `${adminTierListPage.value}/${adminTierListPageCount.value}` },
        ]
  }
  return [
    { label: '가입 회원', value: `${users.value.length}` },
    { label: '관리자', value: `${adminCount}` },
    { label: '활동 계정', value: `${users.value.filter((user) => user.tierListCount > 0).length}` },
  ]
})
const isAnyModalOpen = computed(
  () =>
    gameCreateModalOpen.value ||
    userEditModalOpen.value ||
    userPasswordModalOpen.value ||
    userDeleteModalOpen.value ||
    userRoleModalOpen.value ||
    importModalOpen.value ||
    customItemModalOpen.value ||
    customItemDeleteModalOpen.value ||
    imageResetModalOpen.value ||
    previewModalOpen.value
)

function handleAdminPopState() {
  if (customItemDeleteModalOpen.value) {
    customItemDeleteModalOpen.value = false
    if (customItemModalOpen.value) pushCustomItemModalHistoryState()
    return
  }
  if (customItemModalOpen.value) {
    closeCustomItemModal({ fromPopState: true })
  }
}

function handleAdminKeydown(event) {
  if (event.key !== 'Escape') return
  if (customItemDeleteModalOpen.value) {
    event.preventDefault()
    closeCustomItemDeleteModal()
    return
  }
  if (customItemModalOpen.value) {
    event.preventDefault()
    closeCustomItemModal()
    return
  }
  if (previewModalOpen.value) {
    event.preventDefault()
    closePreviewModal()
  }
}

onMounted(async () => {
  if (typeof window !== 'undefined') {
    window.addEventListener('popstate', handleAdminPopState)
    window.addEventListener('keydown', handleAdminKeydown)
  }
  await auth.refresh()
  await Promise.all([refreshGames(), refreshCustomItems(), refreshAdminTierLists(), refreshUsers(), refreshTemplateRequests(), refreshImageDiagnostics()])
  await syncFeaturedSortable()
})

onUnmounted(() => {
  if (typeof window !== 'undefined') {
    window.removeEventListener('popstate', handleAdminPopState)
    window.removeEventListener('keydown', handleAdminKeydown)
  }
  if (typeof document !== 'undefined') document.body.style.overflow = previousBodyOverflow.value || ''
  clearPreviewUrl('item')
  clearPreviewUrl('thumb')
  destroyFeaturedSortable()
})

function clearPreviewUrl(kind) {
  if (kind === 'item') {
    itemPreviewUrls.value.forEach((url) => {
      if (url) URL.revokeObjectURL(url)
    })
    itemPreviewUrls.value = []
    return
  }

  if (thumbPreviewUrl.value) {
    URL.revokeObjectURL(thumbPreviewUrl.value)
    thumbPreviewUrl.value = ''
  }
}

function resetFileInput(kind) {
  if (kind === 'item') {
    if (itemFileInput.value) itemFileInput.value.value = ''
    return
  }

  if (thumbFileInput.value) thumbFileInput.value.value = ''
}

watch(error, (message) => {
  if (!message) return
  toast.error(message)
  error.value = ''
})

watch(success, (message) => {
  if (!message) return
  toast.success(message)
  success.value = ''
})

watch(
  () => activeTab.value,
  async (tab) => {
    if (tab === 'game-admin' && selectedGameId.value && !selectedGame.value?.game?.id) {
      await loadGame()
      return
    }

    if (tab === 'items') {
      customItemQuery.value = ''
      customItemOrphanOnly.value = false
      customItemPage.value = 1
      customItemModalGameQuery.value = ''
      await refreshCustomItems()
      return
    }

    if (tab === 'tierlists') {
      if (tierlistsMode.value === 'requests') await refreshTemplateRequests()
      else await refreshAdminTierLists()
      return
    }

    if (tab === 'users') {
      await refreshUsers()
    }
  }
)

watch(
  () => tierlistsMode.value,
  async (mode) => {
    if (activeTab.value !== 'tierlists') return
    if (mode === 'requests') await refreshTemplateRequests()
    else await refreshAdminTierLists()
  }
)

watch(
  () => auth.user?.id,
  async (userId) => {
    if (!userId || !auth.user?.isAdmin) return
    await Promise.all([refreshGames(), refreshCustomItems(), refreshAdminTierLists(), refreshUsers(), refreshTemplateRequests(), refreshImageDiagnostics()])
  }
)


watch(
  () => isAnyModalOpen.value,
  (open) => {
    if (typeof document === 'undefined') return
    if (open) {
      if (!previousBodyOverflow.value) previousBodyOverflow.value = document.body.style.overflow || ''
      document.body.style.overflow = 'hidden'
      return
    }
    document.body.style.overflow = previousBodyOverflow.value || ''
    previousBodyOverflow.value = ''
  },
  { immediate: true }
)

function resetMessages() {
  error.value = ''
  success.value = ''
}

function formatBytes(value) {
  const size = Number(value || 0)
  if (!size) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB']
  let current = size
  let unitIndex = 0
  while (current >= 1024 && unitIndex < units.length - 1) {
    current /= 1024
    unitIndex += 1
  }
  return `${current >= 10 || unitIndex === 0 ? current.toFixed(0) : current.toFixed(1)} ${units[unitIndex]}`
}

const imageDiagnosticsCards = computed(() => {
  const stats = imageStats.value
  if (!stats) return []
  return [
    { label: '실사용 파일', value: `${stats.referencedCount || 0}` },
    { label: '현재 용량', value: formatBytes(stats.referencedByteSize) },
    { label: '추적 자산', value: `${stats.trackedReferencedCount || 0}` },
    { label: '레거시 참조', value: `${stats.legacyReferencedCount || 0}` },
    { label: '절감 용량', value: formatBytes(stats.savedByteSize) },
    { label: '절감률', value: `${Math.round((stats.savingsRatio || 0) * 100)}%` },
  ]
})
const visibleLinkedGames = computed(() =>
  (modalTargetCustomItem.value?.linkedGames || []).filter((game) => game?.id && game.id !== 'freeform')
)
const filteredCustomItemModalGames = computed(() => {
  const query = customItemModalGameQuery.value.trim().toLowerCase()
  const linkedIds = new Set(visibleLinkedGames.value.map((game) => game.id))
  const list = games.value.filter((game) => {
    if (!query) return true
    return `${game.name || ''} ${game.id || ''}`.toLowerCase().includes(query)
  })

  return list.slice().sort((a, b) => {
    const linkedDelta = Number(linkedIds.has(a.id)) - Number(linkedIds.has(b.id))
    if (linkedDelta !== 0) return linkedDelta
    if (customItemModalGameSort.value === 'oldest') return Number(a.createdAt || 0) - Number(b.createdAt || 0)
    return Number(b.createdAt || 0) - Number(a.createdAt || 0)
  })
})

const imageStatsPeriodLabel = computed(() => (imageStatsMonth.value ? `${imageStatsMonth.value} 기준` : '전체 기간'))
const imageStatsYearOptions = computed(() => {
  const currentYear = new Date().getFullYear()
  return Array.from({ length: 6 }, (_, index) => String(currentYear - index))
})
const imageStatsMonthOptions = [
  { value: '01', label: '1월' },
  { value: '02', label: '2월' },
  { value: '03', label: '3월' },
  { value: '04', label: '4월' },
  { value: '05', label: '5월' },
  { value: '06', label: '6월' },
  { value: '07', label: '7월' },
  { value: '08', label: '8월' },
  { value: '09', label: '9월' },
  { value: '10', label: '10월' },
  { value: '11', label: '11월' },
  { value: '12', label: '12월' },
]
const selectedImageStatsYear = computed({
  get: () => (imageStatsMonth.value ? imageStatsMonth.value.slice(0, 4) : ''),
  set: (year) => {
    if (!year) {
      imageStatsMonth.value = ''
      return
    }
    const month = imageStatsMonth.value ? imageStatsMonth.value.slice(5, 7) : '01'
    imageStatsMonth.value = `${year}-${month}`
  },
})
const selectedImageStatsMonthNumber = computed({
  get: () => (imageStatsMonth.value ? imageStatsMonth.value.slice(5, 7) : ''),
  set: (month) => {
    if (!month) {
      imageStatsMonth.value = ''
      return
    }
    const year = imageStatsMonth.value ? imageStatsMonth.value.slice(0, 4) : String(new Date().getFullYear())
    imageStatsMonth.value = `${year}-${month}`
  },
})
function clearImageStatsMonth() {
  imageStatsMonth.value = ''
}

async function refreshImageDiagnostics() {
  try {
    const data = await api.getAdminImageAssetStats({
      month: imageStatsMonth.value || '',
      limit: imageStatsLimit.value,
    })
    imageStats.value = data.stats || null
    imageQueue.value = data.queue || { concurrency: 1, activeCount: 0, pendingCount: 0 }
    imageRecentJobs.value = data.recentJobs || []
  } catch (e) {
    error.value = '이미지 최적화 현황을 불러오지 못했어요.'
  }
}

function openImageResetModal() {
  imageResetModalOpen.value = true
}

function closeImageResetModal() {
  imageResetModalOpen.value = false
}

async function confirmImageReset() {
  try {
    const data = await api.resetAdminImageAssetStats({ month: imageStatsMonth.value || null })
    success.value = imageStatsMonth.value
      ? `${imageStatsMonth.value} 기록 ${data.deletedCount || 0}건을 정리했어요.`
      : `전체 최적화 기록 ${data.deletedCount || 0}건을 정리했어요.`
    closeImageResetModal()
    await refreshImageDiagnostics()
  } catch (e) {
    error.value = '이미지 최적화 기록을 정리하지 못했어요.'
  }
}

function setTab(tab) {
  resetMessages()
  activeTab.value = tab
  if (tab === 'tierlists') {
    tierlistsMode.value = 'requests'
  }
  if (tab === 'items') {
    customItemQuery.value = ''
    customItemOrphanOnly.value = false
    customItemPage.value = 1
    refreshCustomItems()
  }
}

function setTierlistsMode(mode) {
  resetMessages()
  tierlistsMode.value = mode
}

function openGameCreateModal() {
  resetMessages()
  newGameId.value = ''
  newGameName.value = ''
  gameCreateModalOpen.value = true
}

function closeGameCreateModal() {
  gameCreateModalOpen.value = false
}

async function handleSelectedGameChange(event) {
  selectedGameId.value = event?.target?.value || ''
  await loadGame()
}

async function selectAdminGame(gameId) {
  if (!gameId || selectedGameId.value === gameId) return
  selectedGameId.value = gameId
  await loadGame()
}

async function refreshGames() {
  try {
    const data = await api.listGames()
    games.value = data.games || []
    featuredGameIds.value = games.value
      .filter((game) => game.displayRank != null)
      .sort((a, b) => a.displayRank - b.displayRank)
      .map((game) => game.id)
    await syncFeaturedSortable()
  } catch (e) {
    error.value = '게임 목록을 불러오지 못했어요.'
  }
}

function setUserAvatarInput(userId, el) {
  if (!userId) return
  if (!el) {
    delete userAvatarInputs.value[userId]
    return
  }
  userAvatarInputs.value[userId] = el
}

const canManageModalRole = computed(() => {
  if (!auth.user?.isPrimaryAdmin) return false
  if (!modalTargetUser.value) return false
  return !modalTargetUser.value.isPrimaryAdmin
})

const isUserEditDirty = computed(() => {
  if (!modalTargetUser.value) return false
  return (
    modalUserDraftEmail.value.trim() !== (modalTargetUser.value.email || '') ||
    modalUserDraftNickname.value.trim() !== (modalTargetUser.value.nickname || '') ||
    !!modalUserDraftIsAdmin.value !== !!modalTargetUser.value.isAdmin
  )
})

function roleLabelOf(user) {
  if (user?.isPrimaryAdmin) return '최고 관리자'
  if (user?.isAdmin) return '운영자'
  return '일반 회원'
}

function openUserAvatarPicker(user) {
  userAvatarInputs.value[user?.id]?.click()
}

async function uploadUserAvatar(user, file, { remove = false } = {}) {
  resetMessages()
  if (!user?.id) return

  try {
    user.isAvatarBusy = true
    const data = await api.updateAdminUserAvatar(user.id, { file, removeAvatar: remove })
    const updated = data.user
    users.value = users.value.map((entry) =>
      entry.id === updated.id
        ? {
            ...entry,
            ...updated,
            isAvatarBusy: false,
          }
        : entry
    )
    if (modalTargetUser.value?.id === updated.id) {
      modalTargetUser.value = { ...modalTargetUser.value, ...updated }
    }
    if (updated.id === auth.user?.id) await auth.refresh()
    await refreshUsers()
    success.value = remove ? '회원 썸네일을 삭제했어요.' : '회원 썸네일을 업데이트했어요.'
  } catch (e) {
    error.value = remove ? '회원 썸네일 삭제에 실패했어요.' : '회원 썸네일 변경에 실패했어요.'
  } finally {
    const target = users.value.find((entry) => entry.id === user.id)
    if (target) target.isAvatarBusy = false
  }
}

async function onUserAvatarChange(user, event) {
  const file = event.target.files && event.target.files[0] ? event.target.files[0] : null
  event.target.value = ''
  if (!file) return
  await uploadUserAvatar(user, file)
}

async function removeUserAvatar(user) {
  if (!user?.avatarSrc) return
  await uploadUserAvatar(user, null, { remove: true })
}

function destroyFeaturedSortable() {
  if (featuredSortable.value) {
    featuredSortable.value.destroy()
    featuredSortable.value = null
  }
}

async function syncFeaturedSortable() {
  await nextTick()
  destroyFeaturedSortable()
  if (!featuredListEl.value) return

  featuredSortable.value = Sortable.create(featuredListEl.value, {
    animation: 160,
    draggable: '[data-featured-id]',
    handle: '[data-featured-handle]',
    ghostClass: 'ghost',
    chosenClass: 'chosen',
    onEnd: (evt) => {
      if (evt.oldIndex == null || evt.newIndex == null || evt.oldIndex === evt.newIndex) return
      const nextIds = [...featuredGameIds.value]
      const [moved] = nextIds.splice(evt.oldIndex, 1)
      nextIds.splice(evt.newIndex, 0, moved)
      featuredGameIds.value = nextIds
    },
  })
}

async function refreshCustomItems() {
  if (!auth.user?.isAdmin) return
  try {
    const data = await api.listAdminCustomItems({
      q: customItemQuery.value,
      page: customItemPage.value,
      limit: customItemLimit.value,
      orphanOnly: customItemOrphanOnly.value,
    })
    customItems.value = data.items || []
    customItemTotal.value = data.total || 0
    customItemPage.value = data.page || 1
    customItemLimit.value = data.limit || customItemLimit.value
  } catch (e) {
    error.value = '아이템 라이브러리를 불러오지 못했어요.'
  }
}

async function refreshAdminTierLists() {
  if (!auth.user?.isAdmin) return
  try {
    const data = await api.listAdminTierLists({
      q: adminTierListQuery.value,
      page: adminTierListPage.value,
      limit: adminTierListLimit.value,
    })
    adminTierLists.value = data.tierLists || []
    adminTierListTotal.value = data.total || 0
    adminTierListPage.value = data.page || 1
    adminTierListLimit.value = data.limit || adminTierListLimit.value
  } catch (e) {
    error.value = '관리자 티어표 목록을 불러오지 못했어요.'
  }
}

async function refreshTemplateRequests() {
  if (!auth.user?.isAdmin) return
  try {
    const data = await api.listAdminTemplateRequests()
    templateRequests.value = (data.requests || []).map((request) => ({
      ...request,
      draftGameId:
        request.type === 'create'
          ? ('tmpl-' + String(request.id || 'request').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 12).toLowerCase())
          : request.targetGameId || request.sourceGameId || '',
      draftGameName:
        request.type === 'create'
          ? `${request.sourceTierListTitle || request.sourceGameName || '새 템플릿'}`
          : request.targetGameName || request.sourceGameName || '',
    }))
  } catch (e) {
    error.value = '템플릿 요청 목록을 불러오지 못했어요.'
  }
}

async function refreshUsers() {
  if (!auth.user?.isAdmin) return
  try {
    const data = await api.listAdminUsers({ q: userQuery.value, sort: userSort.value, direction: userSortDirection.value })
    users.value = (data.users || []).map((user) => ({
      ...user,
      isAvatarBusy: false,
    }))
  } catch (e) {
    error.value = '회원 목록을 불러오지 못했어요.'
  }
}

function resetUploadState() {
  uploadFiles.value = []
  uploadItemDrafts.value = []
  thumbFile.value = null
  resetFileInput('item')
  resetFileInput('thumb')
  clearPreviewUrl('item')
  clearPreviewUrl('thumb')
}

async function loadGame() {
  resetMessages()
  resetUploadState()

  if (!selectedGameId.value) {
    selectedGame.value = null
    return
  }

  try {
    isGameLoading.value = true
    const data = await api.getGame(selectedGameId.value)
    selectedGame.value = {
      ...data,
      items: (data.items || []).map((item) => ({
        ...item,
        draftLabel: item.label,
      })),
    }
  } catch (e) {
    console.error('[AdminView] loadGame failed', selectedGameId.value, e)
    selectedGame.value = null
    error.value = '게임 정보를 불러오지 못했어요.'
  } finally {
    isGameLoading.value = false
  }
}

async function createGame() {
  resetMessages()
  try {
    const res = await fetch(toApiUrl('/api/admin/games'), {
      method: 'POST',
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id: newGameId.value.trim(), name: newGameName.value.trim() }),
    })
    if (!res.ok) throw new Error('failed')

    const data = await res.json()
    await refreshGames()
    selectedGameId.value = data.game.id
    if (customItemModalOpen.value) customItemModalTargetGameId.value = data.game.id
    closeGameCreateModal()
    await loadGame()
    success.value = '게임이 생성됐어요. 이어서 썸네일과 기본 아이템을 관리할 수 있어요.'
  } catch (e) {
    error.value = '게임 생성 실패(관리자 권한/중복 ID 확인)'
  }
}

function handleThumbFile(file) {
  const nextFile = file && (file.type || '').startsWith('image/') ? file : null
  thumbFile.value = nextFile
  clearPreviewUrl('thumb')
  if (thumbFile.value) thumbPreviewUrl.value = URL.createObjectURL(thumbFile.value)
}

function onThumb(event) {
  handleThumbFile(event.target.files && event.target.files[0] ? event.target.files[0] : null)
}

function openThumbFilePicker() {
  thumbFileInput.value?.click()
}

function onThumbDragEnter(event) {
  event.preventDefault()
  isThumbDragOver.value = true
}

function onThumbDragOver(event) {
  event.preventDefault()
  isThumbDragOver.value = true
}

function onThumbDragLeave(event) {
  if (event.currentTarget === event.target) isThumbDragOver.value = false
}

function onThumbDrop(event) {
  event.preventDefault()
  isThumbDragOver.value = false
  handleThumbFile(event.dataTransfer?.files?.[0] || null)
}

function onFile(event) {
  handleItemFiles(event.target.files)
}

function handleItemFiles(fileList) {
  const files = Array.from(fileList || []).filter((file) => (file.type || '').startsWith('image/'))
  uploadFiles.value = files
  clearPreviewUrl('item')
  uploadItemDrafts.value = []
  if (!files.length) return
  itemPreviewUrls.value = files.map((file) => URL.createObjectURL(file))
  uploadItemDrafts.value = files.map((file, index) => ({
    file,
    previewUrl: itemPreviewUrls.value[index],
    label: (file.name || '').replace(/\.[^.]+$/, '').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 60),
  }))
  resetFileInput('item')
}

function openItemFilePicker() {
  itemFileInput.value?.click()
}

function clearItemFiles() {
  uploadFiles.value = []
  uploadItemDrafts.value = []
  clearPreviewUrl('item')
  resetFileInput('item')
}

function onItemDragEnter(event) {
  event.preventDefault()
  isItemDragOver.value = true
}

function onItemDragOver(event) {
  event.preventDefault()
  isItemDragOver.value = true
}

function onItemDragLeave(event) {
  if (event.currentTarget === event.target) isItemDragOver.value = false
}

function onItemDrop(event) {
  event.preventDefault()
  isItemDragOver.value = false
  handleItemFiles(event.dataTransfer?.files)
}

async function uploadThumbnail() {
  resetMessages()
  if (!thumbFile.value || !selectedGameId.value) {
    error.value = '썸네일 파일을 선택해주세요.'
    return
  }

  try {
    const fd = new FormData()
    fd.append('thumbnail', thumbFile.value)
    const res = await fetch(toApiUrl(`/api/admin/games/${encodeURIComponent(selectedGameId.value)}/thumbnail`), {
      method: 'POST',
      credentials: 'include',
      body: fd,
    })
    if (!res.ok) throw new Error('failed')

    thumbFile.value = null
    resetFileInput('thumb')
    clearPreviewUrl('thumb')
    await refreshGames()
    await loadGame()
    success.value = '썸네일이 반영됐어요.'
  } catch (e) {
    error.value = '썸네일 업로드 실패(관리자 권한/파일 크기 확인)'
  }
}

async function uploadItem() {
  resetMessages()
  if (!uploadItemDrafts.value.length || !selectedGameId.value) {
    error.value = '아이템 파일을 선택해주세요.'
    return
  }

  try {
    const fd = new FormData()
    uploadItemDrafts.value.forEach((entry) => {
      fd.append('images', entry.file)
      fd.append('labels', entry.label.trim())
    })
    const uploadCount = uploadItemDrafts.value.length
    const res = await fetch(toApiUrl(`/api/admin/games/${encodeURIComponent(selectedGameId.value)}/images`), {
      method: 'POST',
      credentials: 'include',
      body: fd,
    })
    if (!res.ok) throw new Error('failed')

    resetUploadState()
    await loadGame()
    success.value = `게임 기본 아이템 ${uploadCount}개 추가를 완료했어요.`
  } catch (e) {
    error.value = '아이템 추가 실패(관리자 권한/파일 크기 확인)'
  }
}

async function removeGameItem(itemId) {
  resetMessages()
  try {
    const res = await fetch(
      toApiUrl(`/api/admin/games/${encodeURIComponent(selectedGameId.value)}/items/${encodeURIComponent(itemId)}`),
      {
        method: 'DELETE',
        credentials: 'include',
      }
    )
    if (!res.ok) throw new Error('failed')

    await loadGame()
    success.value = '게임 기본 아이템을 삭제했어요.'
  } catch (e) {
    error.value = '게임 기본 아이템 삭제에 실패했어요.'
  }
}

async function saveGameItemLabel(item) {
  resetMessages()
  if (!selectedGameId.value) return
  const nextLabel = (item.draftLabel || '').trim()
  if (!nextLabel) {
    error.value = '아이템 이름을 입력해주세요.'
    return
  }
  if (nextLabel === item.label) return

  try {
    item.isSavingLabel = true
    const data = await api.updateAdminGameItem(selectedGameId.value, item.id, { label: nextLabel })
    item.label = data.item.label
    item.draftLabel = data.item.label
    success.value = '기본 아이템 이름을 수정했어요.'
  } catch (e) {
    error.value = '기본 아이템 이름 수정에 실패했어요.'
  } finally {
    item.isSavingLabel = false
  }
}

async function removeGame() {
  resetMessages()
  if (!selectedGameId.value || !selectedGame.value?.game) return

  const ok = window.confirm(`"${selectedGame.value.game.name}" 게임을 삭제할까요? 관련 기본 아이템과 티어표도 함께 삭제됩니다.`)
  if (!ok) return

  try {
    const res = await fetch(toApiUrl(`/api/admin/games/${encodeURIComponent(selectedGameId.value)}`), {
      method: 'DELETE',
      credentials: 'include',
    })
    if (!res.ok) throw new Error('failed')

    const deletedName = selectedGame.value.game.name
    selectedGameId.value = ''
    selectedGame.value = null
    resetUploadState()
    await refreshGames()
    success.value = `${deletedName} 게임을 삭제했어요.`
  } catch (e) {
    error.value = '게임 삭제에 실패했어요.'
  }
}

function openUserEditModal(user) {
  resetMessages()
  modalTargetUser.value = user ? { ...user } : null
  modalUserDraftEmail.value = user?.email || ''
  modalUserDraftNickname.value = user?.nickname || ''
  modalUserDraftIsAdmin.value = !!user?.isAdmin
  userEditModalOpen.value = true
}

function closeUserEditModal() {
  userEditModalOpen.value = false
  modalTargetUser.value = null
  modalUserDraftEmail.value = ''
  modalUserDraftNickname.value = ''
  modalUserDraftIsAdmin.value = false
}

async function saveUserEdit() {
  resetMessages()
  if (!modalTargetUser.value?.id) return

  try {
    const data = await api.updateAdminUser(modalTargetUser.value.id, {
      email: modalUserDraftEmail.value.trim(),
      nickname: modalUserDraftNickname.value.trim(),
      isAdmin: !!modalUserDraftIsAdmin.value,
    })
    const updated = data.user
    users.value = users.value.map((entry) =>
      entry.id === updated.id
        ? {
            ...entry,
            ...updated,
            isAvatarBusy: entry.isAvatarBusy || false,
          }
        : entry
    )
    if (updated.id === auth.user?.id) await auth.refresh()
    closeUserEditModal()
    await refreshUsers()
    success.value = '회원 정보를 저장했어요.'
  } catch (e) {
    error.value = '회원 정보 저장에 실패했어요.'
  }
}

function openUserPasswordModal(user) {
  resetMessages()
  modalTargetUser.value = user ? { ...user } : null
  modalPasswordDraft.value = ''
  userPasswordModalOpen.value = true
}

function closeUserPasswordModal() {
  userPasswordModalOpen.value = false
  modalTargetUser.value = null
  modalPasswordDraft.value = ''
}

async function confirmUserPasswordReset() {
  resetMessages()
  if (!modalTargetUser.value?.id) return

  const password = modalPasswordDraft.value.trim()
  if (!password) {
    error.value = '초기화할 비밀번호를 입력해주세요.'
    return
  }

  try {
    await api.updateAdminUserPassword(modalTargetUser.value.id, { password })
    success.value = `${userDisplayName(modalTargetUser.value)} 계정 비밀번호를 초기화했어요.`
    closeUserPasswordModal()
  } catch (e) {
    error.value = '비밀번호 초기화에 실패했어요.'
  }
}

function openUserDeleteModal(user) {
  resetMessages()
  modalTargetUser.value = user ? { ...user } : null
  userDeleteModalOpen.value = true
}

function closeUserDeleteModal() {
  userDeleteModalOpen.value = false
  modalTargetUser.value = null
}

async function confirmUserDelete() {
  resetMessages()
  if (!modalTargetUser.value?.id) return

  try {
    const deletingSelf = modalTargetUser.value.id === auth.user?.id
    const deletedName = userDisplayName(modalTargetUser.value)
    await api.deleteAdminUser(modalTargetUser.value.id)
    users.value = users.value.filter((entry) => entry.id !== modalTargetUser.value.id)
    closeUserDeleteModal()
    success.value = `${deletedName} 계정을 삭제했어요.`
    if (deletingSelf) await auth.refresh()
  } catch (e) {
    error.value = '회원 삭제에 실패했어요.'
  }
}


function openUserRoleModal(user, nextIsAdmin = !modalUserDraftIsAdmin.value) {
  resetMessages()
  modalTargetUser.value = user ? { ...user } : null
  modalRoleNextAdmin.value = !!nextIsAdmin
  userRoleModalOpen.value = true
}

function closeUserRoleModal() {
  userRoleModalOpen.value = false
  if (!userEditModalOpen.value) modalTargetUser.value = null
  modalRoleNextAdmin.value = false
}

function confirmUserRoleDraft() {
  if (!modalTargetUser.value?.id) return
  modalUserDraftIsAdmin.value = modalRoleNextAdmin.value
  const targetLabel = modalRoleNextAdmin.value ? '운영자 권한을 저장 대기 상태로 반영했어요.' : '운영자 권한 해제를 저장 대기 상태로 반영했어요.'
  closeUserRoleModal()
  success.value = targetLabel
}

function submitUserFilters() {
  refreshUsers()
}

function submitCustomItemSearch() {
  customItemPage.value = 1
  refreshCustomItems()
}

function toggleCustomItemOrphanOnly() {
  customItemPage.value = 1
  refreshCustomItems()
}

function changeCustomItemLimit(limit) {
  customItemLimit.value = limit
  customItemPage.value = 1
  refreshCustomItems()
}

function submitAdminTierListSearch() {
  adminTierListPage.value = 1
  refreshAdminTierLists()
}

function changeAdminTierListLimit(limit) {
  adminTierListLimit.value = limit
  adminTierListPage.value = 1
  refreshAdminTierLists()
}

function moveAdminTierListPage(direction) {
  const nextPage = adminTierListPage.value + direction
  if (nextPage < 1 || nextPage > adminTierListPageCount.value) return
  adminTierListPage.value = nextPage
  refreshAdminTierLists()
}

function moveCustomItemPage(direction) {
  const nextPage = customItemPage.value + direction
  if (nextPage < 1 || nextPage > customItemPageCount.value) return
  customItemPage.value = nextPage
  refreshCustomItems()
}

function pushCustomItemModalHistoryState() {
  if (typeof window === 'undefined') return
  window.history.pushState({ ...(window.history.state || {}), adminCustomItemModal: true }, '', window.location.href)
  customItemModalHistoryActive.value = true
}

function openCustomItemModal(item) {
  modalTargetCustomItem.value = item || null
  customItemModalDraftLabel.value = item?.label || ''
  customItemModalTargetGameId.value = ''
  customItemModalGameQuery.value = ''
  customItemModalGameSort.value = 'recent'
  customItemModalOpen.value = true
  pushCustomItemModalHistoryState()
}

function closeCustomItemModal({ fromPopState = false } = {}) {
  customItemModalOpen.value = false
  customItemDeleteModalOpen.value = false
  modalTargetCustomItem.value = null
  customItemModalDraftLabel.value = ''
  customItemModalLabelSaving.value = false
  customItemModalTargetGameId.value = ''
  customItemModalGameQuery.value = ''
  customItemModalGameSort.value = 'recent'

  if (fromPopState) {
    customItemModalHistoryActive.value = false
    return
  }

  if (customItemModalHistoryActive.value && typeof window !== 'undefined') {
    customItemModalHistoryActive.value = false
    window.history.back()
  }
}

function openCustomItemDeleteModal(item) {
  if (!item) return
  if (item.sourceType === 'user' && (item.usageCount > 0 || item.linkedGames.length > 0)) {
    error.value = '사용 중이거나 템플릿에 연결된 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
    return
  }
  modalTargetCustomItem.value = item
  customItemDeleteModalOpen.value = true
}

function closeCustomItemDeleteModal() {
  customItemDeleteModalOpen.value = false
}

function jumpToGameAdmin(gameId) {
  if (!gameId) return
  closeCustomItemModal()
  setTab('game-admin')
  nextTick(() => {
    selectAdminGame(gameId)
  })
}

async function removeCustomItem(item = modalTargetCustomItem.value) {
  resetMessages()
  if (!item) return
  if (item.sourceType === 'user' && (item.usageCount > 0 || item.linkedGames.length > 0)) {
    error.value = '사용 중이거나 템플릿에 연결된 사용자 업로드 이미지는 먼저 참조를 정리해야 삭제할 수 있어요.'
    return
  }

  try {
    await api.deleteAdminCustomItem(item.id)
    closeCustomItemDeleteModal()
    closeCustomItemModal()
    await refreshCustomItems()
    success.value = item.sourceType === 'template' ? '선택한 템플릿 아이템을 제거했어요.' : '사용자 업로드 이미지를 삭제했어요.'
  } catch (e) {
    error.value = item.sourceType === 'template' ? '템플릿 아이템 제거에 실패했어요.' : '사용자 업로드 이미지 삭제에 실패했어요.'
  }
}

async function removeUnusedCustomItems() {
  resetMessages()
  const ok = window.confirm('현재 검색 조건에 맞는 미사용 커스텀 이미지를 모두 삭제할까요?')
  if (!ok) return

  try {
    const data = await api.deleteAdminUnusedCustomItems({ q: customItemQuery.value })
    await refreshCustomItems()
    success.value = `${data.deletedCount || 0}개의 미사용 사용자 업로드 이미지를 삭제했어요.`
  } catch (e) {
    error.value = '미사용 커스텀 이미지 일괄 삭제에 실패했어요.'
  }
}

async function saveCustomItemModalLabel() {
  const item = modalTargetCustomItem.value
  const nextLabel = customItemModalDraftLabel.value.trim().slice(0, 60)
  if (!item || !nextLabel || nextLabel === item.label || customItemModalLabelSaving.value) return

  try {
    customItemModalLabelSaving.value = true
    const data = await api.updateAdminCustomItemLabel(item.id, { label: nextLabel, sourceType: item.sourceType })
    item.label = data.item?.label || nextLabel
    customItemModalDraftLabel.value = item.label
    customItems.value = customItems.value.map((entry) => (entry.id === item.id ? { ...entry, label: item.label } : entry))
    toast.success('아이템 이름을 변경했어요.')
  } catch (e) {
    error.value = '아이템 이름 변경에 실패했어요.'
  } finally {
    customItemModalLabelSaving.value = false
  }
}

async function promoteCustomItem(item) {
  resetMessages()
  if (!customItemModalTargetGameId.value) {
    error.value = '추가할 게임을 먼저 선택해주세요.'
    return
  }

  try {
    item.isPromoting = true
    await api.promoteAdminCustomItem(item.id, { gameId: customItemModalTargetGameId.value })
    const targetGameName = games.value.find((game) => game.id === customItemModalTargetGameId.value)?.name || customItemModalTargetGameId.value
    if (selectedGameId.value === customItemModalTargetGameId.value) await loadGame()
    closeCustomItemModal()
    success.value = `"${item.label}" 이미지를 ${targetGameName} 템플릿으로 추가했어요.`
  } catch (e) {
    error.value = '선택한 이미지를 템플릿으로 추가하지 못했어요.'
  } finally {
    item.isPromoting = false
  }
}


function buildModalItemFromTierListItem(item, tierList) {
  const matchedItem = customItems.value.find((entry) => entry.id === item?.id || entry.src === item?.src)
  const id = matchedItem?.id || item?.id || ''
  return {
    ...matchedItem,
    ...item,
    id,
    label: item?.label || matchedItem?.label || '이름 없음',
    src: item?.src || matchedItem?.src || '',
    sourceType: matchedItem?.sourceType || (String(id).startsWith('asset:') ? 'template' : 'user'),
    sourceLabel: matchedItem?.sourceLabel || '티어표 추가 아이템',
    ownerName: matchedItem?.ownerName || tierListAuthorDisplayName(tierList),
    linkedGames: Array.isArray(matchedItem?.linkedGames) ? matchedItem.linkedGames : [],
    usageCount: matchedItem?.usageCount || 0,
    canDelete: typeof matchedItem?.canDelete === 'boolean' ? matchedItem.canDelete : false,
    isPromoting: false,
    createdAt: matchedItem?.createdAt || item?.createdAt || tierList?.updatedAt || tierList?.createdAt || Date.now(),
  }
}

function openTierListExtraItemModal(item, tierList) {
  if (!item) return
  openCustomItemModal(buildModalItemFromTierListItem(item, tierList))
}

function tierListThumbUrl(tierList) {
  return tierList.thumbnailSrc ? toApiUrl(tierList.thumbnailSrc) : ''
}

function tierListAuthorDisplayName(tierList) {
  return tierList.authorName || '알 수 없음'
}

function tierListVisibilityLabel(tierList) {
  return tierList.isPublic ? '공개' : '비공개'
}

function openAdminTierList(tierList) {
  previewTierList.value = tierList
  previewModalOpen.value = true
}

function previewRequestItemsById(preview) {
  const items = Array.isArray(preview?.snapshotItems) ? preview.snapshotItems : []
  return items.reduce((acc, item) => {
    if (item?.id) acc[item.id] = item
    return acc
  }, {})
}

function previewRequestGroupItems(preview, group) {
  const itemsById = previewRequestItemsById(preview)
  return (group?.itemIds || []).map((itemId) => itemsById[itemId]).filter(Boolean)
}


function previewRequestColumns(preview) {
  const groups = Array.isArray(preview?.snapshotGroups) ? preview.snapshotGroups : []
  const columnSource = groups.find((group) => Array.isArray(group?.columnNames) && group.columnNames.length) || null
  const namedColumns = Array.isArray(columnSource?.columnNames) ? columnSource.columnNames : []
  const cellCount = Math.max(1, namedColumns.length, ...groups.map((group) => (Array.isArray(group?.cells) ? group.cells.length : 0)))

  return Array.from({ length: cellCount }, (_, index) => ({
    id: namedColumns[index]?.id || ('column-' + index),
    name: namedColumns[index]?.name || '',
  }))
}

function previewRequestHasColumns(preview) {
  const columns = previewRequestColumns(preview)
  return columns.length > 1 || columns.some((column) => column.name)
}

function previewRequestGridStyle(preview) {
  const count = previewRequestColumns(preview).length
  return { gridTemplateColumns: 'repeat(' + count + ', minmax(0, 1fr))' }
}

function previewRequestGroupCellItems(preview, group, columnIndex) {
  const itemsById = previewRequestItemsById(preview)
  if (Array.isArray(group?.cells?.[columnIndex])) {
    return group.cells[columnIndex].map((itemId) => itemsById[itemId]).filter(Boolean)
  }
  if (columnIndex === 0) return previewRequestGroupItems(preview, group)
  return []
}

function previewRequestPoolItems(preview) {
  const groupedIds = new Set(
    (preview?.snapshotGroups || []).flatMap((group) => {
      if (Array.isArray(group?.cells) && group.cells.length) {
        return group.cells.flatMap((cell) => (Array.isArray(cell) ? cell : []))
      }
      return group.itemIds || []
    })
  )
  return (preview?.snapshotItems || []).filter((item) => !groupedIds.has(item.id))
}

function openTemplateRequestPreview(request) {
  const snapshotItems = Array.isArray(request.snapshotItems) && request.snapshotItems.length ? request.snapshotItems : Array.isArray(request.items) ? request.items : []
  previewTierList.value = {
    id: request.id,
    title: request.sourceTierListTitle || '템플릿 요청 미리보기',
    description: request.sourceDescription || '',
    thumbnailSrc: request.thumbnailSrc || '',
    requestPreview: true,
    snapshotGroups: request.snapshotGroups || [],
    snapshotItems,
    snapshotShowCharacterNames: !!request.snapshotShowCharacterNames,
  }
  previewModalOpen.value = true
}

function closePreviewModal() {
  previewModalOpen.value = false
  previewTierList.value = null
}

function previewTierListUrl(tierList) {
  if (!tierList?.gameId || !tierList?.id) return ''
  return `/editor/${tierList.gameId}/${tierList.id}?preview=1`
}

function openTierListImportModal(tierList, items) {
  resetMessages()
  const nextItems = (items || []).filter(Boolean)
  if (!nextItems.length) {
    error.value = '가져올 아이템이 없어요.'
    return
  }

  importModalTierList.value = tierList
  importModalItems.value = nextItems
  importModalMode.value = 'existing'
  importModalTargetGameId.value = ''
  importModalNewGameId.value = tierList.gameId === 'freeform' ? '' : `${tierList.gameId}-copy`
  importModalNewGameName.value =
    tierList.gameId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.gameName || tierList.gameId} 파생 템플릿`
  importModalOpen.value = true
}

function closeTierListImportModal() {
  importModalOpen.value = false
  importModalTierList.value = null
  importModalItems.value = []
}

async function confirmTierListImport() {
  resetMessages()
  if (!importModalTierList.value || !importModalItems.value.length) {
    error.value = '가져올 티어표 정보를 확인하지 못했어요.'
    return
  }

  const tierList = importModalTierList.value
  const itemIds = importModalItems.value.map((item) => item.id)

  try {
    if (importModalMode.value === 'existing') {
      if (!importModalTargetGameId.value) {
        error.value = '아이템을 추가할 기존 게임을 선택해주세요.'
        return
      }

      const data = await api.promoteAdminTierListItems(tierList.id, {
        gameId: importModalTargetGameId.value,
        itemIds,
      })
      if (selectedGameId.value === importModalTargetGameId.value) await loadGame()
      success.value = `${data.items?.length || 0}개의 아이템을 기존 템플릿에 추가했어요.`
    } else {
      const nextGameId = (importModalNewGameId.value || '').trim()
      const nextGameName = (importModalNewGameName.value || '').trim()
      if (!nextGameId || !nextGameName) {
        error.value = '새 게임 ID와 이름을 모두 입력해주세요.'
        return
      }

      const data = await api.createAdminGameTemplateFromTierList(tierList.id, {
        gameId: nextGameId,
        name: nextGameName,
        itemIds,
      })
      await refreshGames()
      success.value = `"${data.game?.name || nextGameName}" 템플릿을 생성했어요.`
    }

    closeTierListImportModal()
  } catch (e) {
    error.value = '티어표 아이템 가져오기에 실패했어요.'
  }
}

function templateRequestTypeLabel(request) {
  return request.type === 'create' ? '템플릿 등록 요청' : '템플릿 업데이트 요청'
}

function templateRequestTargetLabel(request) {
  return request.type === 'create' ? '새 게임 템플릿 생성' : request.targetGameName || request.targetGameId || request.sourceGameName
}

async function approveTemplateRequest(request) {
  resetMessages()
  try {
    request.isHandling = true
    if (request.type === 'create') {
      const nextGameId = (request.draftGameId || '').trim()
      const nextGameName = (request.draftGameName || '').trim()
      if (!nextGameId || !nextGameName) {
        error.value = '새 게임 ID와 이름을 모두 입력해주세요.'
        return
      }
      await api.approveAdminTemplateRequest(request.id, {
        gameId: nextGameId,
        name: nextGameName,
      })
      await refreshGames()
      success.value = `"${nextGameName}" 템플릿 생성을 승인했어요.`
    } else {
      const data = await api.approveAdminTemplateRequest(request.id)
      if (selectedGameId.value === (request.targetGameId || request.sourceGameId)) await loadGame()
      success.value = `${data.items?.length || 0}개의 아이템 추가 요청을 승인했어요.`
    }
    await refreshTemplateRequests()
    await refreshAdminTierLists()
  } catch (e) {
    error.value = request.type === 'create' ? '템플릿 등록 요청 승인에 실패했어요.' : '템플릿 업데이트 요청 승인에 실패했어요.'
  } finally {
    request.isHandling = false
  }
}

async function rejectTemplateRequest(request) {
  resetMessages()
  try {
    request.isHandling = true
    await api.rejectAdminTemplateRequest(request.id)
    await refreshTemplateRequests()
    success.value = '요청을 반려했어요.'
  } catch (e) {
    error.value = '요청 반려에 실패했어요.'
  } finally {
    request.isHandling = false
  }
}

const displayThumbnailUrl = computed(() => {
  if (thumbPreviewUrl.value) return thumbPreviewUrl.value
  if (selectedGame.value?.game?.thumbnailSrc) return toApiUrl(selectedGame.value.game.thumbnailSrc)
  return ''
})

function fmt(ts) {
  return new Date(ts).toLocaleString(undefined, {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  })
}

function userAvatarUrl(user) {
  return user?.avatarSrc ? toApiUrl(user.avatarSrc) : ''
}

function userDisplayName(user) {
  return user?.nickname || user?.email?.split('@')[0] || '알 수 없음'
}

function userAvatarFallback(user) {
  return (user?.email?.trim()?.[0] || '?').toUpperCase()
}

function addFeaturedGame(gameId) {
  resetMessages()
  if (!gameId || featuredGameIds.value.includes(gameId)) return
  if (featuredGameIds.value.length >= 50) {
    error.value = '상단 고정 게임은 최대 50개까지만 설정할 수 있어요.'
    return
  }
  featuredGameIds.value = [...featuredGameIds.value, gameId]
  syncFeaturedSortable()
}

function removeFeaturedGame(gameId) {
  resetMessages()
  featuredGameIds.value = featuredGameIds.value.filter((id) => id !== gameId)
  syncFeaturedSortable()
}

function moveFeaturedGame(gameId, direction) {
  const currentIndex = featuredGameIds.value.indexOf(gameId)
  const nextIndex = currentIndex + direction
  if (currentIndex < 0 || nextIndex < 0 || nextIndex >= featuredGameIds.value.length) return
  const nextIds = [...featuredGameIds.value]
  const [moved] = nextIds.splice(currentIndex, 1)
  nextIds.splice(nextIndex, 0, moved)
  featuredGameIds.value = nextIds
  syncFeaturedSortable()
}

async function saveFeaturedOrder() {
  resetMessages()
  try {
    const data = await api.updateAdminGameDisplayOrder({ gameIds: featuredGameIds.value })
    games.value = data.games || []
    featuredGameIds.value = games.value
      .filter((game) => game.displayRank != null)
      .sort((a, b) => a.displayRank - b.displayRank)
      .map((game) => game.id)
    success.value = '홈 화면 게임 순서를 저장했어요.'
  } catch (e) {
    error.value = '게임 순서 저장에 실패했어요.'
  }
}
</script>

<template>
  <section class="wrap">
    <!-- <h2 class="title">관리자</h2> -->
    <div class="card">
      <!-- <div class="desc">기능이 많아진 만큼 관리 영역을 게임, 아이템, 회원 관리로 나눠서 정리합니다.</div> -->
      <div v-if="!auth.user" class="warn">로그인이 필요해요.</div>
      <div v-else-if="!isAdmin" class="warn">이 계정은 관리자 권한이 없어요.</div>

      <template v-else>
        <div class="adminWorkspace">
          <div class="adminMain">
            <header class="adminHero">
              <div class="adminHero__eyebrow">Admin Workspace</div>
              <h2 class="adminHero__title">{{ activeTabTitle }}</h2>
              <p class="adminHero__desc">{{ activeTabDescription }}</p>
              <div class="adminHero__stats">
                <article v-for="stat in adminOverviewStats" :key="stat.label" class="adminHeroStat">
                  <span class="adminHeroStat__label">{{ stat.label }}</span>
                  <strong class="adminHeroStat__value">{{ stat.value }}</strong>
                </article>
              </div>
            </header>

            <template v-if="activeTab === 'featured'">
          <div class="panel">
            <div class="sectionHeader">
              <div>
                <div class="panel__title">홈 화면 상단 고정 순서</div>
                <div class="hint hint--tight">여기에 넣은 게임은 지정한 순서대로 먼저 노출되고, 나머지 게임은 최근 생성순으로 뒤에 이어집니다. 최대 50개까지 설정할 수 있어요.</div>
              </div>
              <button class="btn btn--primary" @click="saveFeaturedOrder">순서 저장</button>
            </div>

            <div class="featuredOrderPanel">
              <div class="featuredOrderPanel__list">
                <div class="section__title">상단 고정 목록</div>
                <div v-if="!featuredGames.length" class="hint">아직 상단 고정 게임이 없어요.</div>
                <div v-else ref="featuredListEl" class="featuredList">
                  <article v-for="(game, index) in featuredGames" :key="game.id" class="featuredCard" :data-featured-id="game.id">
                    <div class="featuredCard__meta">
                      <span class="featuredCard__rank">{{ index + 1 }}</span>
                      <div>
                        <div class="featuredCard__title">{{ game.name }}</div>
                        <div class="featuredCard__id">{{ game.id }}</div>
                      </div>
                    </div>
                    <div class="featuredCard__actions">
                      <button class="btn btn--ghost btn--small" data-featured-handle>드래그</button>
                      <button class="btn btn--ghost btn--small" :disabled="index === 0" @click="moveFeaturedGame(game.id, -1)">위로</button>
                      <button class="btn btn--ghost btn--small" :disabled="index === featuredGames.length - 1" @click="moveFeaturedGame(game.id, 1)">아래로</button>
                      <button class="btn btn--danger btn--small" @click="removeFeaturedGame(game.id)">제외</button>
                    </div>
                  </article>
                </div>
              </div>

              <div class="featuredOrderPanel__picker">
                <div class="section__title">게임 추가</div>
                <div class="featuredPickerList">
                  <button
                    v-for="game in availableGamesForFeatured"
                    :key="game.id"
                    class="featuredPickerItem"
                    :disabled="featuredGameIds.length >= 50"
                    @click="addFeaturedGame(game.id)"
                  >
                    <span>{{ game.name }}</span>
                    <span class="featuredPickerItem__id">{{ game.id }}</span>
                  </button>
                </div>
              </div>
            </div>
          </div>
          </template>

            <template v-else-if="activeTab === 'game-admin'">

          <div v-if="isGameLoading" class="panel panel--empty">
            <div class="emptyState">
              <div class="emptyState__title">게임 정보를 불러오는 중이에요.</div>
              <div class="emptyState__desc">선택한 게임의 썸네일과 기본 아이템을 곧 표시합니다.</div>
            </div>
          </div>
          <div v-else-if="hasSelectedGame" class="panel">
            <div class="detailHead">
              <div>
                <div class="panel__title">선택된 게임 정보</div>
                <div class="selectedGame__name">{{ selectedGame.game.name }}</div>
                <div class="selectedGame__id">{{ selectedGame.game.id }}</div>
              </div>
            </div>

            <div class="section">
              <section class="adminCard">
                <div class="section__title">기본 아이템 추가</div>
                <div class="itemComposer">
                  <div class="itemComposer__form">
                    <input ref="itemFileInput" type="file" accept="image/*" multiple class="srOnlyInput" @change="onFile" />
                    <div
                      class="dropZone"
                      :class="{ 'dropZone--active': isItemDragOver }"
                      @dragenter="onItemDragEnter"
                      @dragover="onItemDragOver"
                      @dragleave="onItemDragLeave"
                      @drop="onItemDrop"
                    >
                      <div class="dropZone__title">이미지를 드래그해서 기본 아이템으로 추가</div>
                      <div class="dropZone__desc">여러 파일을 한 번에 올릴 수 있고, 저장 라벨은 파일명으로 자동 생성됩니다.</div>
                      <div class="dropZone__actions">
                        <button class="btn btn--ghost btn--small" type="button" @click="openItemFilePicker">파일 선택</button>
                        <button class="btn btn--danger btn--small" type="button" :disabled="!uploadFiles.length" @click="clearItemFiles">선택 비우기</button>
                      </div>
                    </div>
                    <button class="btn" :disabled="!canAddItem" @click="uploadItem">
                      아이템 {{ uploadItemDrafts.length || 0 }}개 추가
                    </button>
                  </div>
                  <div class="itemPreviewCard">
                    <div v-if="uploadItemDrafts.length" class="itemDraftList">
                      <div v-for="draft in uploadItemDrafts" :key="draft.previewUrl || draft.file.name" class="itemDraftRow">
                        <div class="itemDraftRow__preview">
                          <img class="itemPreviewImage" :src="draft.previewUrl" :alt="draft.file.name || 'item preview'" />
                        </div>
                        <div class="itemDraftRow__body">
                          <input v-model="draft.label" class="input input--labelEdit input--dense" maxlength="60" placeholder="아이템 이름" />
                          <div class="hint hint--tight">{{ draft.file.name }}</div>
                        </div>
                      </div>
                    </div>
                    <div v-else class="itemPreviewEmpty">선택한 기본 아이템 미리보기가 여기에 표시됩니다.</div>
                    <div class="thumbLabel thumbLabel--preview">
                      {{ uploadItemDrafts.length ? `추가 예정 아이템 ${uploadItemDrafts.length}개` : '아직 선택된 파일이 없어요.' }}
                    </div>
                  </div>
                </div>
              </section>
            </div>

            <div class="section">
              <div class="section__title">현재 기본 아이템 목록</div>
              <div v-if="!selectedGame?.items?.length" class="hint">아직 등록된 기본 아이템이 없어요.</div>
              <div v-else class="thumbGrid">
                <div v-for="item in selectedGame.items" :key="item.id" class="thumbCard">
                  <img class="thumb thumb--game" :src="toApiUrl(item.src)" :alt="item.label" />
                  <input v-model="item.draftLabel" class="input input--labelEdit" placeholder="아이템 이름" />
                  <div class="thumbCard__actions">
                    <button
                      class="btn btn--ghost btn--small"
                      :disabled="item.isSavingLabel || !item.draftLabel?.trim() || item.draftLabel.trim() === item.label"
                      @click="saveGameItemLabel(item)"
                    >
                      {{ item.isSavingLabel ? '저장중...' : '이름 저장' }}
                    </button>
                    <button class="btn btn--danger btn--small" @click="removeGameItem(item.id)">아이템 삭제</button>
                  </div>
                </div>
              </div>
            </div>
          </div>
            <div v-else class="panel panel--empty">
              <div class="emptyState">
                <div class="emptyState__title">게임을 선택해 주세요.</div>
                <div v-if="selectedGameId" class="hint hint--tight">선택한 게임을 찾지 못했거나 로딩 중 오류가 발생했어요. 다시 선택해보세요.</div>
              </div>
            </div>
          </template>

            <template v-else-if="activeTab === 'items'">
          <div class="panel">
            <div v-if="!customItems.length" class="hint">조건에 맞는 관리 대상 아이템이 없어요.</div>
            <div v-else class="customItemGrid">
              <button v-for="item in customItems" :key="item.id" type="button" class="customItemCard" @click="openCustomItemModal(item)">
                <span class="customItemCard__badge" :class="{ 'customItemCard__badge--template': item.sourceType === 'template' }">{{ item.sourceLabel }}</span>
                <img class="customItemCard__image" :src="toApiUrl(item.src)" :alt="item.label" />
                <div class="customItemCard__title" :title="item.label">{{ item.label }}</div>
              </button>
            </div>

            <div class="pager">
              <button class="btn btn--ghost" :disabled="customItemPage <= 1" @click="moveCustomItemPage(-1)">이전</button>
              <div class="pager__info">{{ customItemPage }} / {{ customItemPageCount }} 페이지 · 총 {{ customItemTotal }}개</div>
              <button class="btn btn--ghost" :disabled="customItemPage >= customItemPageCount" @click="moveCustomItemPage(1)">다음</button>
            </div>
          </div>
          </template>

            <template v-else-if="activeTab === 'tierlists'">
          <div v-if="tierlistsMode === 'requests'" class="panel">
            <div class="sectionHeader">
              <div>
                <div class="panel__title">사용자 템플릿 요청</div>
                <div class="hint hint--tight">freeform 템플릿 등록 요청과 기존 게임 템플릿 업데이트 요청을 여기서 승인하거나 반려할 수 있어요. 반려한 요청은 대기 목록에서 바로 제외됩니다.</div>
              </div>
            </div>

            <div v-if="!templateRequests.length" class="hint">현재 처리 대기 중인 템플릿 요청이 없어요.</div>
                        <div v-else class="templateRequestList">
              <article v-for="request in templateRequests" :key="request.id" class="tierAdminCard templateRequestCard templateRequestCard--aligned">
                <div class="templateRequestCard__side">
                  <button class="tierAdminCard__preview templateRequestCard__preview" type="button" @click="openTemplateRequestPreview(request)">
                    <img v-if="request.thumbnailSrc" class="tierAdminCard__thumb" :src="toApiUrl(request.thumbnailSrc)" :alt="request.sourceTierListTitle" />
                    <div v-else class="tierAdminCard__thumb tierAdminCard__thumb--empty"></div>
                  </button>

                  <div class="templateRequestCard__thumbMeta">
                    <template v-if="request.type === 'create'">
                      <label class="templateRequestField">
                        <span class="templateRequestField__label">게임 이름</span>
                        <input v-model="request.draftGameName" class="input" placeholder="새 게임 이름" />
                      </label>
                      <label class="templateRequestField">
                        <span class="templateRequestField__label">게임 ID</span>
                        <input v-model="request.draftGameId" class="input" placeholder="임시 게임 ID" />
                      </label>
                    </template>
                    <template v-else>
                      <div class="templateRequestCard__thumbLabel">게임 이름</div>
                      <div class="templateRequestCard__thumbValue">{{ request.draftGameName || request.sourceGameName || '-' }}</div>
                      <div class="templateRequestCard__thumbLabel">게임 ID</div>
                      <div class="templateRequestCard__thumbValue">{{ request.draftGameId || request.sourceGameId || '-' }}</div>
                    </template>
                  </div>
                </div>

                <div class="tierAdminCard__body">
                  <div class="tierAdminCard__head">
                    <div>
                      <div class="tierAdminCard__title">{{ request.sourceTierListTitle }}</div>
                      <div v-if="request.sourceDescription" class="tierAdminCard__desc">{{ request.sourceDescription }}</div>
                      <div class="tierAdminCard__meta">
                        {{ templateRequestTypeLabel(request) }} · {{ request.requesterName }} · {{ fmt(request.createdAt) }}
                      </div>
                      <div class="tierAdminCard__meta">{{ templateRequestTargetLabel(request) }}</div>
                    </div>
                  </div>

                  <div class="tierAdminCard__stats">
                    <span class="pill">추가 아이템 {{ request.items?.length || 0 }}개</span>
                    <span class="pill">{{ request.type === 'create' ? '새 템플릿' : '기존 템플릿 업데이트' }}</span>
                  </div>

                  <div v-if="request.items?.length" class="tierAdminItemList templateRequestCard__items">
                    <button v-for="item in request.items" :key="item.id" class="tierAdminItem" type="button">
                      <img class="tierAdminItem__thumb" :src="toApiUrl(item.src)" :alt="item.label" />
                      <div class="tierAdminItem__title">{{ item.label }}</div>
                    </button>
                  </div>

                  <div class="templateRequestCard__actions">
                    <button class="btn btn--primary" :disabled="request.isHandling" @click="approveTemplateRequest(request)">
                      {{ request.isHandling ? '처리중...' : '승인' }}
                    </button>
                    <button class="btn btn--danger" :disabled="request.isHandling" @click="rejectTemplateRequest(request)">반려 후 숨김</button>
                  </div>
                </div>
              </article>
            </div>
          </div>

          <div v-else class="panel">
            <div class="sectionHeader">
              <div>
                <div class="panel__title">전체 티어표 관리</div>
                <div class="hint hint--tight">공개/비공개를 포함한 최근 티어표를 모두 확인하고, 추가 아이템을 기존 게임 템플릿으로 승격하거나 커스텀 티어표를 새 게임 템플릿으로 만들 수 있어요. 여기는 요청 목록과 별개로 전체 저장 티어표를 보는 영역입니다.</div>
              </div>
            </div>

            <div v-if="!adminTierLists.length" class="hint">조건에 맞는 티어표가 없어요.</div>
            <div v-else class="tierAdminList">
              <article v-for="tierList in adminTierLists" :key="tierList.id" class="tierAdminCard">
                <button class="tierAdminCard__preview" type="button" @click="openAdminTierList(tierList)">
                  <img v-if="tierListThumbUrl(tierList)" class="tierAdminCard__thumb" :src="tierListThumbUrl(tierList)" :alt="tierList.title" />
                  <div v-else class="tierAdminCard__thumb tierAdminCard__thumb--empty"></div>
                </button>

                <div class="tierAdminCard__body">
                  <div class="tierAdminCard__head">
                    <div>
                      <div class="tierAdminCard__title">{{ tierList.title }}</div>
                      <div v-if="tierList.description" class="tierAdminCard__desc">{{ tierList.description }}</div>
                      <div class="tierAdminCard__meta">
                        {{ tierList.gameName || tierList.gameId }} · {{ tierListAuthorDisplayName(tierList) }} · {{ tierListVisibilityLabel(tierList) }}
                      </div>
                      <div class="tierAdminCard__meta">{{ fmt(tierList.updatedAt) }}</div>
                    </div>
                  </div>

                  <div class="tierAdminCard__stats">
                    <span class="pill">전체 아이템 {{ tierList.itemCount }}개</span>
                    <span class="pill" :class="{ 'pill--accent': tierList.extraItemCount > 0 }">추가 아이템 {{ tierList.extraItemCount }}개</span>
                  </div>

                  <div v-if="tierList.extraItems?.length" class="tierAdminSection">
                    <div class="tierAdminSection__title">추가로 넣은 아이템</div>
                    <div class="tierAdminItemList">
                      <button v-for="item in tierList.extraItems" :key="item.id" class="tierAdminItem" @click="openTierListExtraItemModal(item, tierList)">
                        <img class="tierAdminItem__thumb" :src="toApiUrl(item.src)" :alt="item.label" />
                        <div class="tierAdminItem__title">{{ item.label }}</div>
                      </button>
                    </div>
                    <div class="tierAdminSection__actions">
                      <button class="btn btn--ghost btn--small" @click="openTierListImportModal(tierList, tierList.extraItems)">추가 아이템 전체 가져오기</button>
                      <button v-if="tierList.gameId === 'freeform'" class="btn btn--primary btn--small" @click="openTierListImportModal(tierList, tierList.extraItems)">
                        새 템플릿으로 가져오기
                      </button>
                    </div>
                  </div>
                </div>
              </article>
            </div>

            <div class="pager">
              <button class="btn btn--ghost" :disabled="adminTierListPage <= 1" @click="moveAdminTierListPage(-1)">이전</button>
              <div class="pager__info">{{ adminTierListPage }} / {{ adminTierListPageCount }} 페이지 · 총 {{ adminTierListTotal }}개</div>
              <button class="btn btn--ghost" :disabled="adminTierListPage >= adminTierListPageCount" @click="moveAdminTierListPage(1)">다음</button>
            </div>
          </div>
          </template>

            <template v-else>
          <div class="panel">
            <div class="sectionHeader">
              <div>
                <div class="panel__title">회원 관리</div>
                <div class="hint hint--tight">회원 프로필을 정리하고, 필요한 경우에만 권한 변경과 비밀번호 초기화를 진행할 수 있어요.</div>
              </div>
            </div>

            <div class="toolbar toolbar--secondary">
              <input
                v-model="userQuery"
                class="input toolbar__search"
                placeholder="이메일, 닉네임 검색"
                @keydown.enter.prevent="submitUserFilters"
              />
              <select v-model="userSort" class="select toolbar__select" @change="submitUserFilters">
                <option value="recent">최근 활동순</option>
                <option value="created">가입순</option>
                <option value="tierlists">작성 티어표 많은 순</option>
              </select>
              <select v-model="userSortDirection" class="select toolbar__select" @change="submitUserFilters">
                <option value="desc">내림차순</option>
                <option value="asc">오름차순</option>
              </select>
              <button class="btn btn--ghost toolbar__button" type="button" @click="submitUserFilters">조회</button>
            </div>

            <div v-if="!users.length" class="hint">아직 가입한 회원이 없어요.</div>
            <div v-else class="userList">
              <article v-for="user in users" :key="user.id" class="userCard">
                <div class="userCard__head">
                  <div class="userCard__identity">
                    <input
                      :ref="(el) => setUserAvatarInput(user.id, el)"
                      type="file"
                      accept="image/*"
                      class="srOnlyInput"
                      @change="onUserAvatarChange(user, $event)"
                    />
                    <div class="userAvatarWrap">
                      <button class="userAvatar userAvatarButton" type="button" :disabled="user.isAvatarBusy" @click="openUserAvatarPicker(user)">
                        <img v-if="userAvatarUrl(user)" class="userAvatar__image" :src="userAvatarUrl(user)" :alt="userDisplayName(user)" />
                        <span v-else class="userAvatar__fallback">{{ userAvatarFallback(user) }}</span>
                        <span class="userAvatarButton__overlay">{{ user.isAvatarBusy ? '업데이트중...' : '수정' }}</span>
                      </button>
                      <button
                        v-if="user?.avatarSrc"
                        class="userAvatarRemoveButton"
                        type="button"
                        title="회원 썸네일 삭제"
                        :disabled="user.isAvatarBusy"
                        @click.stop="removeUserAvatar(user)"
                      >
                        <SvgIcon class="userAvatarRemoveIcon" :src="deleteIcon" :size="12" />
                      </button>
                    </div>
                    <div class="userCard__identityMeta">
                      <div class="userCard__title">{{ userDisplayName(user) }}</div>
                      <div class="userCard__meta">{{ user.email }}</div>
                    </div>
                  </div>
                </div>

                <div v-if="user.isAdmin" class="roleBadge userCard__roleBadge">{{ roleLabelOf(user) }}</div>

                <div class="userInfoList">
                  <div class="userInfoLine"><span>가입일</span><strong>{{ fmt(user.createdAt) }}</strong></div>
                  <div class="userInfoLine"><span>작성 티어표</span><strong>{{ user.tierListCount }}개</strong></div>
                  <div class="userInfoLine"><span>최근 활동</span><strong>{{ fmt(user.recentActivityAt || user.createdAt) }}</strong></div>
                  <div class="userInfoLine"><span>계정명</span><strong>{{ user.email }}</strong></div>
                  <div class="userInfoLine"><span>닉네임</span><strong>{{ user.nickname || '미설정' }}</strong></div>
                  <div class="userInfoLine"><span>권한</span><strong>{{ roleLabelOf(user) }}</strong></div>
                </div>

                <div class="userCard__actions userCard__actions--compact">
                  <button class="iconActionButton" type="button" title="비밀번호 초기화" @click="openUserPasswordModal(user)">
                    <SvgIcon class="iconActionButton__icon" :src="lockResetIcon" :size="18" />
                  </button>
                  <button class="iconActionButton iconActionButton--danger" type="button" title="회원 삭제" @click="openUserDeleteModal(user)">
                    <SvgIcon class="iconActionButton__icon" :src="deleteIcon" :size="18" />
                  </button>
                  <button class="btn btn--ghost userSaveButton" type="button" @click="openUserEditModal(user)">회원 정보 수정</button>
                </div>
              </article>
            </div>
          </div>
            </template>

            <div v-if="gameCreateModalOpen" class="modalOverlay" @click.self="closeGameCreateModal">
              <div class="modalCard" role="dialog" aria-modal="true">
                <div class="modalCard__title">새 게임 만들기</div>
                <div class="modalCard__desc">게임 이름과 고유 ID를 입력한 뒤 생성하면 바로 아래 상세 관리 화면으로 이어집니다.</div>
                <div class="modalCard__form">
                  <label class="field">
                    <span class="field__label">게임 이름</span>
                    <input v-model="newGameName" class="field__input" maxlength="60" placeholder="게임 이름" />
                    <span class="field__hint">{{ newGameName.length }}/60자</span>
                  </label>
                  <label class="field">
                    <span class="field__label">게임 ID</span>
                    <input
                      v-model="newGameId"
                      class="field__input"
                      maxlength="120"
                      placeholder="game id (영문/숫자)"
                      @keydown.enter.prevent="createGame"
                    />
                    <span class="field__hint">영문, 숫자, 하이픈 조합 권장 · {{ newGameId.length }}/120자</span>
                  </label>
                </div>
                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeGameCreateModal">취소</button>
                  <button class="btn btn--primary" :disabled="!newGameId.trim() || !newGameName.trim()" @click="createGame">게임 생성</button>
                </div>
              </div>
            </div>

            <div v-if="userEditModalOpen" class="modalOverlay" @click.self="closeUserEditModal">
              <div class="modalCard modalCard--userEdit" role="dialog" aria-modal="true">
                <div class="modalCard__title">회원 정보 수정</div>
                <div class="modalCard__desc">{{ modalTargetUser ? `${userDisplayName(modalTargetUser)} 계정의 정보와 권한을 조정할 수 있어요.` : '' }}</div>
                <div class="userEditForm">
                  <label class="field">
                    <span class="field__label">이메일</span>
                    <input v-model="modalUserDraftEmail" class="field__input" maxlength="255" placeholder="계정 이메일" />
                    <span class="field__hint">로그인 계정으로 사용하는 이메일입니다. {{ modalUserDraftEmail.length }}/255자</span>
                  </label>

                  <label class="field">
                    <span class="field__label">닉네임</span>
                    <input v-model="modalUserDraftNickname" class="field__input" maxlength="40" placeholder="표시용 닉네임" />
                    <span class="field__hint">티어표 작성자명과 프로필에 표시됩니다. {{ modalUserDraftNickname.length }}/40자</span>
                  </label>

                  <button
                    v-if="canManageModalRole"
                    class="userRoleAction"
                    type="button"
                    @click="openUserRoleModal(modalTargetUser, !modalUserDraftIsAdmin)"
                  >
                    {{ modalUserDraftIsAdmin ? '운영자 권한 해제' : '운영자 권한 부여' }}
                  </button>
                </div>
                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeUserEditModal">취소</button>
                  <button class="btn btn--primary" :disabled="!isUserEditDirty" @click="saveUserEdit">회원 정보 저장</button>
                </div>
              </div>
            </div>

            <div v-if="userPasswordModalOpen" class="modalOverlay" @click.self="closeUserPasswordModal">
              <div class="modalCard" role="dialog" aria-modal="true">
                <div class="modalCard__title">비밀번호 초기화</div>
                <div class="modalCard__desc">{{ modalTargetUser ? `${userDisplayName(modalTargetUser)} 계정에 설정할 새 비밀번호를 입력해주세요.` : '' }}</div>
                <div class="modalCard__form">
                  <label class="field">
                    <span class="field__label">새 비밀번호</span>
                    <input
                      v-model="modalPasswordDraft"
                      class="field__input"
                      type="password"
                      maxlength="120"
                      placeholder="초기화할 비밀번호 입력"
                      @keydown.enter.prevent="confirmUserPasswordReset"
                    />
                    <span class="field__hint">6~120자 권장 · {{ modalPasswordDraft.length }}/120자</span>
                  </label>
                </div>
                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeUserPasswordModal">취소</button>
                  <button class="btn btn--primary" :disabled="!modalPasswordDraft.trim()" @click="confirmUserPasswordReset">초기화</button>
                </div>
              </div>
            </div>

            <div v-if="userDeleteModalOpen" class="modalOverlay" @click.self="closeUserDeleteModal">
              <div class="modalCard" role="dialog" aria-modal="true">
                <div class="modalCard__title">회원 삭제</div>
                <div class="modalCard__desc">{{ modalTargetUser ? `${modalTargetUser.email} 계정을 삭제할까요? 작성한 티어표와 커스텀 이미지도 함께 삭제됩니다.` : '' }}</div>
                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeUserDeleteModal">취소</button>
                  <button class="btn btn--danger" @click="confirmUserDelete">삭제</button>
                </div>
              </div>
            </div>

            <div v-if="userRoleModalOpen" class="modalOverlay" @click.self="closeUserRoleModal">
              <div class="modalCard" role="dialog" aria-modal="true">
                <div class="modalCard__title">운영자 권한 변경</div>
                <div class="modalCard__desc">
                  {{
                    modalTargetUser
                      ? modalRoleNextAdmin
                        ? `${userDisplayName(modalTargetUser)} 사용자를 운영자로 지정할까요?`
                        : `${userDisplayName(modalTargetUser)} 사용자의 운영자 권한을 해제할까요?`
                      : ''
                  }}
                </div>
                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeUserRoleModal">취소</button>
                  <button class="btn btn--primary" @click="confirmUserRoleDraft">확인</button>
                </div>
              </div>
            </div>

            <div v-if="importModalOpen" class="modalOverlay" @click.self="closeTierListImportModal">
              <div class="modalCard modalCard--import" role="dialog" aria-modal="true">
                <div class="modalCard__title">티어표 아이템 가져오기</div>
                <div class="modalCard__desc">
                  "{{ importModalTierList?.title }}"의 아이템 {{ importModalItemCount }}개를 어디로 가져올지 선택해주세요.
                </div>

                <div class="importModeTabs">
                  <button class="modeTab" :class="{ 'modeTab--active': importModalMode === 'existing' }" @click="importModalMode = 'existing'">
                    기존 템플릿에 추가
                  </button>
                  <button class="modeTab" :class="{ 'modeTab--active': importModalMode === 'new' }" @click="importModalMode = 'new'">
                    새 템플릿 만들기
                  </button>
                </div>

                <div v-if="importModalMode === 'existing'" class="modalCard__form">
                  <select v-model="importModalTargetGameId" class="select">
                    <option value="">기존 게임 선택</option>
                    <option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
                  </select>
                </div>

                <div v-else class="modalCard__form">
                  <input v-model="importModalNewGameId" class="input" placeholder="새 게임 ID" />
                  <input v-model="importModalNewGameName" class="input" placeholder="새 게임 이름" />
                </div>

                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeTierListImportModal">취소</button>
                  <button class="btn btn--primary" @click="confirmTierListImport">
                    {{ importModalMode === 'existing' ? '여기로 가져오기' : '새 템플릿 생성' }}
                  </button>
                </div>
              </div>
            </div>

            <div v-if="customItemModalOpen" class="modalOverlay" @click.self="closeCustomItemModal">
              <div class="modalCard modalCard--customItem" role="dialog" aria-modal="true">
                <div v-if="modalTargetCustomItem" class="customItemModal">
                  <aside class="customItemModal__pickerPanel">
                    <div class="customItemModal__pickerHead">
                      <div class="customItemModal__pickerEyebrow">GAME PICKER</div>
                      <div class="customItemModal__pickerTitle">템플릿으로 추가할 게임</div>
                      <button class="btn btn--ghost btn--small customItemModal__createGameButton" type="button" @click="openGameCreateModal">새 템플릿 만들기</button>
                    </div>
                    <div class="customItemModal__pickerControls">
                      <input v-model="customItemModalGameQuery" class="input input--dense" placeholder="게임 이름, ID 검색" />
                      <select v-model="customItemModalGameSort" class="select">
                        <option value="recent">최신순</option>
                        <option value="oldest">오래된순</option>
                      </select>
                    </div>
                    <div class="customItemModal__gameList">
                      <button
                        v-for="game in filteredCustomItemModalGames"
                        :key="game.id"
                        type="button"
                        class="customItemModal__gameItem"
                        :class="{
                          'customItemModal__gameItem--active': customItemModalTargetGameId === game.id,
                          'customItemModal__gameItem--linked': visibleLinkedGames.some((entry) => entry.id === game.id),
                        }"
                        @click="customItemModalTargetGameId = game.id"
                      >
                        <span class="customItemModal__gameName">{{ game.name }}</span>
                        <span class="customItemModal__gameMeta">{{ game.id }}</span>
                        <span v-if="visibleLinkedGames.some((entry) => entry.id === game.id)" class="customItemModal__gameState">이미 포함됨</span>
                      </button>
                    </div>
                  </aside>
                  <div class="customItemModal__body">
                    <button class="customItemModal__close" type="button" @click="closeCustomItemModal">닫기</button>
                    <div class="customItemModal__content">
                      <div class="customItemModal__titleRow">
                        <div>
                          <div class="customItemModal__title">{{ modalTargetCustomItem.label }}</div>
                          <div class="customItemModal__source">{{ modalTargetCustomItem.sourceLabel }}</div>
                        </div>
                      </div>
                      <img class="customItemModal__image" :src="toApiUrl(modalTargetCustomItem.src)" :alt="modalTargetCustomItem.label" />
                      <div class="customItemModal__labelEditor">
                        <label class="field">
                          <span class="field__label">아이템 이름</span>
                          <input v-model="customItemModalDraftLabel" class="field__input" type="text" maxlength="60" placeholder="아이템 이름" />
                        </label>
                        <button class="btn btn--ghost customItemModal__renameButton" type="button" :disabled="customItemModalLabelSaving || !customItemModalDraftLabel.trim() || customItemModalDraftLabel.trim() === modalTargetCustomItem.label" @click="saveCustomItemModalLabel">
                          {{ customItemModalLabelSaving ? '저장중...' : '이름 저장' }}
                        </button>
                      </div>
                    <div class="customItemModal__metaList">
                      <div class="customItemModal__metaRow"><span>파일</span><strong :title="modalTargetCustomItem.src.split('/').pop()">{{ modalTargetCustomItem.src.split('/').pop() }}</strong></div>
                      <div class="customItemModal__metaRow"><span>업로더/출처</span><strong :title="modalTargetCustomItem.ownerName">{{ modalTargetCustomItem.ownerName }}</strong></div>
                      <div class="customItemModal__metaRow"><span>템플릿 연결</span><strong>{{ visibleLinkedGames.length }}개 게임</strong></div>
                      <div class="customItemModal__metaRow"><span>등록일</span><strong>{{ fmt(modalTargetCustomItem.createdAt) }}</strong></div>
                    </div>
                    <div class="customItemModal__linked">
                      <span class="customItemModal__label">템플릿에 사용 중인 게임</span>
                      <div v-if="visibleLinkedGames.length" class="customItemModal__chips">
                        <button v-for="game in visibleLinkedGames" :key="game.id" type="button" class="pill pill--link" @click="jumpToGameAdmin(game.id)">{{ game.name }}</button>
                      </div>
                      <div v-else class="hint hint--tight">아직 템플릿에 연결된 게임이 없어요.</div>
                    </div>
                      <div class="customItemModal__actions">
                        <a class="btn btn--ghost customItemModal__action" :href="toApiUrl(modalTargetCustomItem.src)" :download="modalTargetCustomItem.label">이미지 다운로드</a>
                        <button class="btn btn--ghost customItemModal__action" :disabled="!customItemModalTargetGameId || modalTargetCustomItem.isPromoting" @click="promoteCustomItem(modalTargetCustomItem)">
                          {{ modalTargetCustomItem.isPromoting ? '추가중...' : '기본 템플릿에 추가' }}
                        </button>
                        <button v-if="modalTargetCustomItem.canDelete" class="btn btn--danger customItemModal__action" :disabled="modalTargetCustomItem.sourceType === 'user' && (modalTargetCustomItem.usageCount > 0 || visibleLinkedGames.length > 0)" @click="openCustomItemDeleteModal(modalTargetCustomItem)">삭제</button>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>

            <div v-if="customItemDeleteModalOpen" class="modalOverlay" @click.self="closeCustomItemDeleteModal">
              <div class="modalCard" role="dialog" aria-modal="true">
                <div class="modalCard__title">아이템 삭제</div>
                <div class="modalCard__desc">{{ !modalTargetCustomItem ? '' : modalTargetCustomItem.sourceType === 'template' ? '"' + modalTargetCustomItem.label + '" 항목을 정리할까요? 게임에 연결된 항목이면 해당 템플릿과 저장된 같은 게임의 티어표에서도 함께 빠질 수 있고, 보관 자산이면 라이브러리에서만 제거됩니다.' : '"' + modalTargetCustomItem.label + '" 이미지를 삭제할까요? 사용자 업로드이면서 어디에도 연결되지 않은 이미지에만 삭제를 허용합니다.' }}</div>
                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeCustomItemDeleteModal">취소</button>
                  <button class="btn btn--danger" @click="removeCustomItem()">삭제</button>
                </div>
              </div>
            </div>

            <div v-if="imageResetModalOpen" class="modalOverlay" @click.self="closeImageResetModal">
              <div class="modalCard" role="dialog" aria-modal="true">
                <div class="modalCard__title">최적화 기록 비우기</div>
                <div class="modalCard__desc">
                  {{ imageStatsMonth ? `${imageStatsMonth} 기간의 최적화 기록만 삭제합니다.` : '전체 최적화 작업 기록을 비웁니다. 실제 이미지 파일은 삭제되지 않아요.' }}
                </div>
                <div class="modalCard__actions">
                  <button class="btn btn--ghost" @click="closeImageResetModal">취소</button>
                  <button class="btn btn--danger" @click="confirmImageReset">기록 비우기</button>
                </div>
              </div>
            </div>

            <div v-if="previewModalOpen" class="modalOverlay" @click.self="closePreviewModal">
              <div class="modalCard modalCard--preview" role="dialog" aria-modal="true">
                <div class="modalCard__titleRow">
                  <div class="modalCard__title">{{ previewTierList?.title || '티어표 미리보기' }}</div>
                  <button class="btn btn--ghost btn--small" @click="closePreviewModal">닫기</button>
                </div>
                
                <div v-if="previewTierList?.requestPreview" class="requestPreview">
                  <div class="requestPreview__sheet">
                    <div class="requestPreview__title">{{ previewTierList.title || '티어표 미리보기' }}</div>
                    <div v-if="previewTierList.description" class="requestPreview__description">{{ previewTierList.description }}</div>
                    <div class="requestPreview__meta">
                      {{ previewRequestHasColumns(previewTierList) ? (previewRequestColumns(previewTierList).length + '열 구성') : '단일 열 구성' }} ·
                      {{ previewTierList.snapshotGroups?.length || 0 }}개 행 ·
                      {{ previewTierList.snapshotItems?.length || 0 }}개 아이템
                    </div>
                    <div v-if="previewRequestHasColumns(previewTierList)" class="requestPreview__columns">
                      <div class="requestPreview__columnsSpacer" aria-hidden="true"></div>
                      <div class="requestPreview__columnsGrid" :style="previewRequestGridStyle(previewTierList)">
                        <div
                          v-for="(column, columnIndex) in previewRequestColumns(previewTierList)"
                          :key="column.id"
                          class="requestPreview__columnHeader"
                        >
                          {{ column.name || ('열 ' + (columnIndex + 1)) }}
                        </div>
                      </div>
                    </div>
                    <div class="requestPreview__rows">
                      <div v-for="group in previewTierList.snapshotGroups" :key="group.id" class="requestPreview__row">
                        <div class="requestPreview__label">{{ group.name }}</div>
                        <div class="requestPreview__dropGrid" :style="previewRequestGridStyle(previewTierList)">
                          <div
                            v-for="(column, columnIndex) in previewRequestColumns(previewTierList)"
                            :key="group.id + '-' + column.id"
                            class="requestPreview__dropColumn"
                          >
                            <div class="requestPreview__drop">
                              <div
                                v-for="item in previewRequestGroupCellItems(previewTierList, group, columnIndex)"
                                :key="item.id"
                                class="requestPreview__item"
                              >
                                <img class="thumb requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
                                <div v-if="previewTierList.snapshotShowCharacterNames" class="itemNameOverlay requestPreview__itemLabel">{{ item.label }}</div>
                              </div>
                            </div>
                          </div>
                        </div>
                      </div>
                    </div>
                    <div v-if="previewRequestPoolItems(previewTierList).length" class="requestPreview__pool">
                      <div class="requestPreview__poolTitle">남은 아이템</div>
                      <div class="requestPreview__poolGrid">
                        <div
                          v-for="item in previewRequestPoolItems(previewTierList)"
                          :key="item.id"
                          class="requestPreview__poolItem requestPreview__item requestPreview__item--muted"
                        >
                          <img class="thumb requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
                          <div v-if="previewTierList.snapshotShowCharacterNames" class="itemNameOverlay requestPreview__itemLabel">{{ item.label }}</div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
                <iframe
                  v-else-if="previewTierList"
                  class="previewFrame"
                  :src="previewTierListUrl(previewTierList)"
                  title="티어표 미리보기"
                />
              </div>
            </div>
          </div>
        </div>
      </template>
    </div>
  </section>

  <Teleport :to="localRightRailTarget">
    <aside v-show="globalRightRailOpen" class="adminSidebar">
      <section class="adminSidebar__panel">
        <div class="adminSidebar__label">Mode</div>
        <div class="adminSidebar__tabs">
          <button class="tab" :class="{ 'tab--active': activeTab === 'featured' }" @click="setTab('featured')">목록 관리</button>
          <button class="tab" :class="{ 'tab--active': activeTab === 'game-admin' }" @click="setTab('game-admin')">게임 관리</button>
          <button class="tab" :class="{ 'tab--active': activeTab === 'items' }" @click="setTab('items')">아이템 관리</button>
          <button class="tab" :class="{ 'tab--active': activeTab === 'tierlists' }" @click="setTab('tierlists')">티어표 관리</button>
          <button class="tab" :class="{ 'tab--active': activeTab === 'users' }" @click="setTab('users')">회원 관리</button>
        </div>
      </section>

      <section v-if="activeTab === 'game-admin'" class="adminSidebar__panel">
        <div class="adminSidebar__label">Game</div>
        <div class="adminSidebar__group">
          <button class="btn btn--primary" @click="openGameCreateModal">새 게임 생성</button>
          <input v-model="gameAdminQuery" class="input" placeholder="게임 이름 또는 ID 검색" />
          <select v-model="gameAdminSort" class="select">
            <option value="recent">최신순</option>
            <option value="oldest">오래된순</option>
          </select>
          <div class="adminGamePicker">
            <button
              v-for="game in filteredAdminGames"
              :key="game.id"
              class="adminGamePicker__item"
              :class="{ 'adminGamePicker__item--active': selectedGameId === game.id }"
              type="button"
              @click="selectAdminGame(game.id)"
            >
              <span class="adminGamePicker__name">{{ game.name }}</span>
              <span class="adminGamePicker__meta">{{ game.id }}</span>
            </button>
            <div v-if="!filteredAdminGames.length" class="hint hint--tight">검색 결과가 없어요.</div>
          </div>
          <div v-if="selectedGameId && !hasSelectedGame && !isGameLoading" class="hint hint--tight">선택된 게임 ID: {{ selectedGameId }}</div>
        </div>
        <div v-if="hasSelectedGame" class="adminSidebar__group">
          <div class="selectedGameSidebar__name">{{ selectedGame.game.name }}</div>
          <div class="selectedGameSidebar__id">{{ selectedGame.game.id }}</div>
          <input ref="thumbFileInput" type="file" accept="image/*" class="srOnlyInput" @change="onThumb" />
          <button
            class="thumbDropZone"
            :class="{ 'thumbDropZone--active': isThumbDragOver }"
            type="button"
            @click="openThumbFilePicker"
            @dragenter="onThumbDragEnter"
            @dragover="onThumbDragOver"
            @dragleave="onThumbDragLeave"
            @drop="onThumbDrop"
          >
            <img v-if="displayThumbnailUrl" class="selectedThumb selectedThumb--sidebar" :src="displayThumbnailUrl" :alt="selectedGame.game.name" />
            <div v-else class="selectedThumb selectedThumb--empty selectedThumb--sidebar">대표 썸네일</div>
            <div class="thumbDropZone__copy">
              <div class="thumbDropZone__title">클릭 or 드래그</div>
            </div>
          </button>
          <div class="adminSidebar__actions adminSidebar__actions--stack">
            <button class="btn" :disabled="!canApplyThumbnail" @click="uploadThumbnail">썸네일 적용</button>
            <button class="btn btn--danger" @click="removeGame">게임 삭제</button>
          </div>
        </div>
      </section>

      <section v-else-if="activeTab === 'items'" class="adminSidebar__panel">
        <div class="adminSidebar__label">Filters</div>
        <div class="adminSidebar__group">
          <input v-model="customItemQuery" class="input" placeholder="파일명, 라벨, 업로더 검색" @keydown.enter.prevent="submitCustomItemSearch" />
          <button class="btn btn--ghost" @click="submitCustomItemSearch">검색</button>
        </div>
        <div class="adminSidebar__group">
          <select :value="customItemLimit" class="select" @change="changeCustomItemLimit(Number($event.target.value))">
            <option :value="50">50개씩 보기</option>
            <option :value="200">200개씩 보기</option>
          </select>
          <label class="checkRow checkRow--compact">
            <input v-model="customItemOrphanOnly" type="checkbox" @change="toggleCustomItemOrphanOnly" />
            <span>미사용 사용자 업로드만 보기</span>
          </label>
        </div>
        <div class="adminSidebar__actions">
          <button class="btn btn--ghost" @click="refreshCustomItems">새로고침</button>
          <button class="btn btn--danger" :disabled="!customItems.length" @click="removeUnusedCustomItems">미사용 이미지 일괄 삭제</button>
        </div>
        <div class="adminSidebar__stats">
          <div class="sidebarStat">
            <span class="sidebarStat__label">현재 페이지</span>
            <strong class="sidebarStat__value">{{ customItemPage }}/{{ customItemPageCount }}</strong>
          </div>
          <div class="sidebarStat">
            <span class="sidebarStat__label">검색 결과</span>
            <strong class="sidebarStat__value">{{ customItemTotal }}</strong>
          </div>
        </div>
      </section>

      <section v-else-if="activeTab === 'tierlists'" class="adminSidebar__panel">
        <div class="adminSidebar__label">Tierlists</div>
        <div class="modeTabs modeTabs--stack">
          <button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'requests' }" @click="setTierlistsMode('requests')">
            템플릿 요청 관리
          </button>
          <button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'lists' }" @click="setTierlistsMode('lists')">
            전체 티어표 관리
          </button>
        </div>
        <template v-if="tierlistsMode === 'requests'"></template>
        <template v-else>
          <div class="adminSidebar__group">
            <input
              v-model="adminTierListQuery"
              class="input"
              placeholder="제목, 작성자, 게임 이름 검색"
              @keydown.enter.prevent="submitAdminTierListSearch"
            />
            <button class="btn btn--ghost" @click="submitAdminTierListSearch">검색</button>
            <select :value="adminTierListLimit" class="select" @change="changeAdminTierListLimit(Number($event.target.value))">
              <option :value="50">50개씩 보기</option>
              <option :value="200">200개씩 보기</option>
            </select>
          </div>
          <div class="adminSidebar__actions">
            <button class="btn btn--ghost" @click="refreshAdminTierLists">새로고침</button>
          </div>
          <div class="adminSidebar__stats">
            <div class="sidebarStat">
              <span class="sidebarStat__label">현재 페이지</span>
              <strong class="sidebarStat__value">{{ adminTierListPage }}/{{ adminTierListPageCount }}</strong>
            </div>
            <div class="sidebarStat">
              <span class="sidebarStat__label">검색 결과</span>
              <strong class="sidebarStat__value">{{ adminTierListTotal }}</strong>
            </div>
          </div>
        </template>
      </section>


      <section v-if="activeTab === 'featured'" class="adminSidebar__panel">
        <div class="adminSidebar__label">Image Optimization</div>
        <div class="adminSidebar__group adminSidebar__group--monthPicker">
          <div class="monthPicker">
            <select v-model="selectedImageStatsYear" class="select monthPicker__select monthPicker__select--year">
              <option value="">전체 기간</option>
              <option v-for="year in imageStatsYearOptions" :key="year" :value="year">{{ year }}년</option>
            </select>
            <select v-if="selectedImageStatsYear" v-model="selectedImageStatsMonthNumber" class="select monthPicker__select monthPicker__select--month">
              <option value="">월 선택</option>
              <option v-for="month in imageStatsMonthOptions" :key="month.value" :value="month.value">{{ month.label }}</option>
            </select>
            <button v-if="imageStatsMonth" class="btn btn--ghost btn--tiny" type="button" @click="clearImageStatsMonth">전체</button>
          </div>
          <select v-model.number="imageStatsLimit" class="select">
            <option :value="6">최근 6건</option>
            <option :value="12">최근 12건</option>
            <option :value="24">최근 24건</option>
          </select>
        </div>
        <div class="adminSidebar__actions adminSidebar__actions--split">
          <button class="btn btn--ghost" @click="refreshImageDiagnostics">현황 새로고침</button>
          <button class="btn btn--ghost" @click="openImageResetModal">기록 비우기</button>
        </div>
        <div class="hint hint--tight">{{ imageStatsPeriodLabel }}</div>
        <div v-if="imageDiagnosticsCards.length" class="adminSidebar__stats adminSidebar__stats--grid">
          <article v-for="stat in imageDiagnosticsCards" :key="stat.label" class="sidebarStat">
            <span class="sidebarStat__label">{{ stat.label }}</span>
            <strong class="sidebarStat__value">{{ stat.value }}</strong>
          </article>
        </div>
        <div class="adminSidebar__stats">
          <div class="sidebarStat">
            <span class="sidebarStat__label">큐 상태</span>
            <strong class="sidebarStat__value">{{ imageQueue.activeCount }} 실행 / {{ imageQueue.pendingCount }} 대기</strong>
          </div>
          <div class="sidebarStat">
            <span class="sidebarStat__label">작업 누적</span>
            <strong class="sidebarStat__value">{{ imageStats?.completedCount || 0 }} 완료 · {{ imageStats?.failedCount || 0 }} 실패</strong>
          </div>
          <div class="sidebarStat">
            <span class="sidebarStat__label">중복 재사용</span>
            <strong class="sidebarStat__value">{{ imageStats?.reusedCount || 0 }}건</strong>
          </div>
          <div class="sidebarStat">
            <span class="sidebarStat__label">누락 파일</span>
            <strong class="sidebarStat__value">{{ imageStats?.missingReferencedCount || 0 }}건</strong>
          </div>
        </div>
        <div class="adminSidebar__group">
          <div class="section__title">최근 최적화 작업</div>
          <div class="hint hint--tight">현재 {{ imageRecentJobs.length }}건 표시 중</div>
          <div v-if="!imageRecentJobs.length" class="hint hint--tight">아직 기록된 최적화 작업이 없어요.</div>
          <div v-else class="imageJobList">
            <article v-for="job in imageRecentJobs" :key="job.id" class="imageJobRow">
              <div class="imageJobRow__head">
                <strong>{{ job.sourceCategory || 'asset' }}</strong>
                <span class="imageJobRow__status">{{ job.status }}</span>
              </div>
              <div class="hint hint--tight">{{ formatBytes(job.originalByteSize) }} → {{ formatBytes(job.optimizedByteSize) }}</div>
              <div class="hint hint--tight">{{ fmt(job.queuedAt) }}</div>
            </article>
          </div>
        </div>
      </section>
    </aside>
  </Teleport>
</template>

<style scoped>
.wrap {
  display: grid;
  gap: 16px;
}
.adminWorkspace {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
.adminMain {
  min-width: 0;
  display: grid;
  gap: 14px;
}
.adminHero {
  display: grid;
  gap: 10px;
  padding: 22px 24px;
  border-radius: 24px;
  border: 1px solid var(--theme-border);
  background:
    linear-gradient(180deg, var(--theme-surface-soft-2), var(--theme-pill-bg)),
    var(--theme-pill-bg);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.22);
}
.adminHero__eyebrow {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--theme-text-faint);
}
.adminHero__title {
  margin: 0;
  font-size: 28px;
  line-height: 1.05;
  font-weight: 900;
  letter-spacing: -0.04em;
}
.adminHero__desc {
  margin: 0;
  color: var(--theme-text-muted);
  line-height: 1.6;
}
.adminHero__stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  margin-top: 6px;
}
.adminHeroStat {
  display: grid;
  gap: 6px;
  padding: 14px 16px;
  border-radius: 16px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
}
.adminHeroStat__label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--theme-text-faint);
}
.adminHeroStat__value {
  font-size: 22px;
  line-height: 1;
  font-weight: 900;
  letter-spacing: -0.04em;
}
.adminSidebar {
  display: grid;
  gap: 12px;
}
.adminSidebar__panel {
  display: grid;
  gap: 12px;
  padding: 16px;
  border-radius: 22px;
  border: 1px solid var(--theme-border);
  background:
    linear-gradient(180deg, var(--theme-surface-soft), var(--theme-pill-bg)),
    color-mix(in srgb, var(--theme-rail-bg) 98%, transparent);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
}

.adminSidebar__stats--grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.imageJobList {
  display: grid;
  gap: 8px;
}

.imageJobRow {
  border: 1px solid var(--theme-border);
  border-radius: 14px;
  padding: 10px 12px;
  background: var(--theme-pill-bg);
  display: grid;
  gap: 4px;
}

.imageJobRow__head {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  align-items: center;
  font-size: 12px;
}

.imageJobRow__status {
  color: var(--theme-text-soft);
  text-transform: capitalize;
}
.adminSidebar__label {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--theme-text-faint);
}
.adminSidebar__tabs,
.adminSidebar__group,
.adminSidebar__actions,
.adminSidebar__stats {
  display: grid;
  gap: 10px;
}
.adminSidebar__group--monthPicker {
  align-items: start;
}
.monthPicker {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.monthPicker__select {
  min-width: 0;
}
.monthPicker__select--year {
  flex: 1 1 132px;
}
.monthPicker__select--month {
  flex: 1 1 108px;
}
.adminSidebar__actions--stack .btn {
  width: 100%;
}
.adminSidebar__actions--split {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.adminSidebar__groupTitle {
  font-size: 13px;
  font-weight: 800;
  color: var(--theme-text);
}
.adminGamePicker {
  display: grid;
  gap: 8px;
  max-height: 640px;
  overflow: auto;
  padding-right: 4px;
}
.adminGamePicker__item {
  display: grid;
  /* gap: 2px; */
  padding: 11px 12px;
  text-align: left;
  border-radius: 14px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
  color: var(--theme-text);
  cursor: pointer;
}
.adminGamePicker__item--active {
  border-color: rgba(77, 127, 233, 0.58);
  background: rgba(77, 127, 233, 0.12);
}
.adminGamePicker__name {
  font-size: 13px;
  font-weight: 800;
}
.adminGamePicker__meta {
  font-size: 11px;
  color: var(--theme-text-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidebarStat {
  display: grid;
  gap: 4px;
  padding: 12px 14px;
  border-radius: 14px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
}
.sidebarStat__label {
  font-size: 12px;
  color: var(--theme-text-soft);
}
.sidebarStat__value {
  font-size: 14px;
  font-weight: 900;
}
.card {
  border: 0;
  background: transparent;
  border-radius: 0;
  padding: 0;
}
.desc {
  opacity: 0.82;
  line-height: 1.5;
}
.warn,
.error,
.success {
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: 12px;
}
.warn {
  border: 1px solid rgba(245, 158, 11, 0.35);
  background: rgba(245, 158, 11, 0.14);
}
.error {
  border: 1px solid rgba(239, 68, 68, 0.3);
  background: rgba(239, 68, 68, 0.12);
}
.success {
  border: 1px solid rgba(52, 211, 153, 0.32);
  background: rgba(52, 211, 153, 0.14);
}
.tabs,
.modeTabs {
  margin-top: 0;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.modeTabs--stack {
  display: grid;
  gap: 8px;
}
.tab,
.modeTab {
  padding: 12px 14px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: var(--theme-surface-soft);
  color: var(--theme-text);
  cursor: pointer;
  font-weight: 800;
  transition:
    border-color 0.16s ease,
    background 0.16s ease,
    transform 0.16s ease;
}
.tab:hover,
.modeTab:hover {
  border-color: rgba(255, 255, 255, 0.18);
  background: var(--theme-surface-soft-2);
  transform: translateY(-1px);
}
.tab--active,
.modeTab--active {
  background: rgba(96, 165, 250, 0.14);
  border-color: rgba(96, 165, 250, 0.28);
  color: rgba(239, 246, 255, 0.98);
}
.adminSidebar__tabs .tab,
.modeTabs--stack .modeTab {
  width: 100%;
  text-align: left;
}
.panel {
  border: 1px solid rgba(255, 255, 255, 0.1);
  background:
    linear-gradient(180deg, var(--theme-surface-soft), var(--theme-pill-bg)),
    color-mix(in srgb, var(--theme-card-bg) 96%, transparent);
  border-radius: 24px;
  padding: 18px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.18);
}
.panel--empty {
  min-height: 240px;
  display: grid;
  place-items: center;
}
.panel--compact {
  max-width: 520px;
}
.emptyState {
  max-width: 520px;
  display: grid;
  gap: 8px;
  text-align: center;
}
.emptyState__title {
  font-size: 18px;
  font-weight: 900;
}
.emptyState__desc {
  color: var(--theme-text-muted);
  line-height: 1.6;
}
.featuredOrderPanel {
  margin-top: 14px;
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.95fr);
  gap: 16px;
}
.featuredOrderPanel__list,
.featuredOrderPanel__picker {
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: color-mix(in srgb, var(--theme-pill-bg) 85%, transparent);
  border-radius: 18px;
  padding: 16px;
}
.featuredList,
.featuredPickerList {
  margin-top: 10px;
  display: grid;
  gap: 10px;
  max-height: 420px;
  overflow: auto;
}
.featuredCard {
  display: flex;
  gap: 12px;
  justify-content: space-between;
  align-items: center;
  padding: 14px;
  border-radius: 16px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
}
.featuredCard__meta {
  display: flex;
  gap: 12px;
  align-items: center;
  min-width: 0;
}
.featuredCard__rank {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: rgba(96, 165, 250, 0.18);
  font-weight: 900;
  flex: 0 0 auto;
}
.featuredCard__title {
  font-weight: 900;
}
.featuredCard__id {
  margin-top: 4px;
  opacity: 0.68;
  font-size: 12px;
  word-break: break-all;
}
.featuredCard__actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.featuredCard [data-featured-handle] {
  cursor: grab;
}
.featuredPickerItem {
  width: 100%;
  display: flex;
  gap: 10px;
  justify-content: space-between;
  align-items: center;
  padding: 14px;
  border-radius: 16px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
  color: var(--theme-text);
  cursor: pointer;
  text-align: left;
}
.featuredPickerItem:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.featuredPickerItem__id {
  opacity: 0.64;
  font-size: 12px;
}
.panel__title,
.section__title {
  font-weight: 900;
}
.section {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--theme-border);
}
.section--topGrid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}
.gameManagerGrid {
  margin-top: 14px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}
.gameManagerGrid--single {
  grid-template-columns: minmax(0, 1fr);
}
.gameManagerCard__body {
  margin-top: 10px;
  display: grid;
  gap: 10px;
}
.adminCard {
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: color-mix(in srgb, var(--theme-pill-bg) 85%, transparent);
  border-radius: 18px;
  padding: 16px;
  min-width: 0;
}
.adminCard--muted {
  background: var(--theme-pill-bg);
}
.sectionHeader {
  display: flex;
  gap: 12px;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
}
.toolbar {
  margin-top: 14px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: 10px;
  align-items: end;
}
.toolbar--secondary {
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
}
.toolbar__search,
.toolbar__select {
  margin-top: 0;
}
.toolbar__button {
  margin-top: 0;
}
.uploadPreviewCard {
  margin-top: 10px;
  display: flex;
  justify-content: center;
}
.uploadControls {
  margin-top: 14px;
  display: grid;
  gap: 12px;
  justify-items: center;
}
.select,
.input {
  width: 100%;
  box-sizing: border-box;
  padding: 11px 13px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-pill-bg);
  color: var(--theme-text);
  outline: none;
  /* margin-top: 10px; */
}
.input--compact {
  max-width: 320px;
}
.input--labelEdit {
  margin-top: 10px;
}
.input--dense {
  margin-top: 0;
  padding-top: 9px;
  padding-bottom: 9px;
}
.hint {
  margin-top: 10px;
  opacity: 0.78;
  font-size: 13px;
}
.hint--tight {
  margin-top: 6px;
}
.inputFile {
  width: 100%;
  max-width: 360px;
}
.inputFile--tight {
  margin-top: 0;
}
.srOnlyInput {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.btn {
  height: 100%;
  font-size: 12px;
  line-height: 1.2;
  white-space: nowrap;
  word-break: keep-all;
  padding: 11px 13px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.06);
  color: var(--theme-text);
  cursor: pointer;
  font-weight: 800;
  text-align: center;
  text-decoration: none;
  transition:
    background 0.16s ease,
    border-color 0.16s ease,
    transform 0.16s ease;
}
.btn:hover:not(:disabled) {
  transform: translateY(-1px);
  border-color: rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.1);
}
.btn--small {
  margin-top: 0;
  padding: 8px 10px;
  font-size: 11px;
}
.ghost {
  opacity: 0.4;
}
.chosen {
  outline: 2px solid rgba(96, 165, 250, 0.45);
}
.btn:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}
.btn--primary {
  background: rgba(96, 165, 250, 0.2);
  border-color: rgba(96, 165, 250, 0.26);
}
.btn--danger {
  background: rgba(239, 68, 68, 0.14);
  border-color: rgba(239, 68, 68, 0.28);
}
.btn--ghost {
  background: var(--theme-pill-bg);
}
.detailHead {
  display: flex;
  gap: 12px;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
}
.detailHead__actions {
  display: flex;
  gap: 8px;
}
.selectedGame__name {
  margin-top: 8px;
  font-size: 22px;
  font-weight: 900;
}
.selectedGame__id {
  margin-top: 6px;
  opacity: 0.72;
  word-break: break-all;
}
.selectedThumb {
  width: min(100%, 256px);
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-surface-soft);
}
.selectedThumb--empty {
  display: grid;
  place-items: center;
  color: var(--theme-text-soft);
  background: var(--theme-card-bg);
}
.selectedThumb--sidebar {
  width: 100%;
}
.selectedGameSidebar__name {
  font-size: 18px;
  font-weight: 900;
}
.selectedGameSidebar__id {
  font-size: 12px;
  opacity: 0.68;
  word-break: break-all;
}
.thumbDropZone {
  position: relative;
  width: 100%;
  display: block;
  padding: 0;
  overflow: hidden;
  border-radius: 18px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-pill-bg);
  text-align: left;
  transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.thumbDropZone--active {
  border-color: rgba(96, 165, 250, 0.56);
  box-shadow: 0 0 0 1px rgba(96, 165, 250, 0.18);
  transform: translateY(-1px);
}
.thumbDropZone__copy {
  position: absolute;
  inset: auto 0 0 0;
  display: grid;
  place-items: center;
  min-height: 52px;
  padding: 12px 16px;
  background: linear-gradient(180deg, transparent 0%, color-mix(in srgb, var(--theme-main-bg) 82%, transparent) 46%, color-mix(in srgb, var(--theme-main-bg) 94%, transparent) 100%);
}
.thumbDropZone__title {
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 0.01em;
}
.itemComposer {
  margin-top: 10px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(160px, 192px);
  gap: 16px;
  align-items: start;
}
.itemComposer__form {
  display: grid;
  gap: 12px;
  align-items: start;
}
.dropZone {
  padding: 18px;
  border-radius: 16px;
  border: 1px dashed var(--theme-border-strong);
  background: var(--theme-pill-bg);
  transition:
    border-color 0.16s ease,
    background 0.16s ease,
    transform 0.16s ease;
}
.dropZone--active {
  border-color: rgba(96, 165, 250, 0.56);
  background: rgba(96, 165, 250, 0.08);
  transform: translateY(-1px);
}
.dropZone__title {
  font-weight: 900;
}
.dropZone__desc {
  margin-top: 8px;
  font-size: 13px;
  opacity: 0.74;
  line-height: 1.5;
}
.dropZone__actions {
  margin-top: 12px;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.itemPreviewCard {
  padding: 12px;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-surface-soft);
}
.itemPreviewGrid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.itemDraftList {
  display: grid;
  gap: 10px;
}
.itemDraftRow {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
}
.itemDraftRow__preview {
  width: 72px;
  height: 72px;
  overflow: hidden;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-pill-bg);
}
.itemDraftRow__body {
  min-width: 0;
  display: grid;
  gap: 6px;
}
.itemPreviewFrame {
  aspect-ratio: 1 / 1;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-pill-bg);
}
.itemPreviewImage {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.itemPreviewEmpty {
  min-height: 192px;
  display: grid;
  place-items: center;
  color: var(--theme-text-soft);
  font-size: 13px;
  text-align: center;
  line-height: 1.5;
}
.thumbGrid {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 12px;
}
.thumbCard {
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 16px;
  background: var(--theme-surface-soft);
  padding: 12px;
  min-width: 0;
}
.thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.thumb--game {
  max-width: 150px;
  margin: 0 auto;
  display: block;
}
.thumbLabel {
  margin-top: 8px;
  font-weight: 900;
  font-size: 13px;
  opacity: 0.9;
  word-break: break-word;
}
.thumbCard__actions {
  margin-top: 10px;
  display: grid;
  gap: 8px;
}
.thumbLabel--preview {
  text-align: center;
}
.customItemGrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 12px;
}
.customItemCard {
  appearance: none;
  position: relative;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 18px;
  background: var(--theme-surface-soft);
  overflow: hidden;
  display: grid;
  gap: 10px;
  padding: 10px;
  min-width: 0;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}
.customItemCard__badge {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  padding: 5px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--theme-main-bg) 82%, transparent);
  color: var(--theme-text);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.02em;
}
.customItemCard__badge--template {
  background: rgba(96, 165, 250, 0.18);
}
.customItemCard:hover {
  border-color: rgba(126, 162, 255, 0.42);
  background: rgba(255, 255, 255, 0.06);
  transform: translateY(-1px);
}
.customItemCard__image {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  display: block;
  border-radius: 14px;
  background: var(--theme-pill-bg);
}
.customItemCard__title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 800;
  font-size: 13px;
  line-height: 1.3;
  color: var(--theme-text);
}
.customItemModal {
  display: grid;
  grid-template-columns: 340px minmax(0, 1fr);
  min-height: min(860px, calc(100dvh - 40px));
  align-items: stretch;
}
.customItemModal__pickerPanel {
  display: grid;
  align-content: start;
  gap: 18px;
  min-width: 0;
  padding: 28px 22px;
  border-right: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
}
.customItemModal__pickerHead {
  display: grid;
  gap: 10px;
}
.customItemModal__pickerEyebrow {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--theme-text-faint);
}
.customItemModal__pickerTitle {
  font-size: 18px;
  font-weight: 900;
}
.customItemModal__pickerControls {
  display: grid;
  gap: 10px;
}
.customItemModal__gameList {
  display: grid;
  gap: 8px;
  max-height: 440px;
  overflow: auto;
}
.customItemModal__createGameButton {
  justify-self: start;
}
.customItemModal__gameItem {
  display: grid;
  gap: 4px;
  padding: 12px 13px;
  border-radius: 16px;
  border: 1px solid var(--theme-border);
  background: var(--theme-surface-soft);
  color: var(--theme-text);
  text-align: left;
  cursor: pointer;
}
.customItemModal__gameItem--active {
  border-color: rgba(96, 165, 250, 0.42);
  background: rgba(96, 165, 250, 0.12);
}
.customItemModal__gameItem--linked {
  border-style: dashed;
}
.customItemModal__gameName {
  font-size: 13px;
  font-weight: 800;
}
.customItemModal__gameMeta,
.customItemModal__gameState {
  font-size: 11px;
  color: var(--theme-text-soft);
}
.customItemModal__body {
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 16px;
  padding: 24px 28px 28px;
}
.customItemModal__content {
  min-width: 0;
  min-height: 0;
  display: grid;
  align-content: start;
  gap: 18px;
  overflow: auto;
  padding-right: 0;
  overscroll-behavior: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.customItemModal__content::-webkit-scrollbar {
  width: 0;
  height: 0;
}
.customItemModal__labelEditor {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.customItemModal__renameButton {
  white-space: nowrap;
}
.customItemModal__titleRow,
.customItemModal__linked {
  display: grid;
  gap: 8px;
}
.customItemModal__linked {
  padding: 14px 16px;
  border-radius: 18px;
  border: 1px solid var(--theme-border);
  background: var(--theme-surface-soft);
}
.customItemModal__close {
  justify-self: end;
  border: 0;
  background: transparent;
  color: var(--theme-text-muted);
  cursor: pointer;
  font-size: 13px;
}
.customItemModal__image {
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: min(360px, 34dvh);
  object-fit: cover;
  border-radius: 24px;
  background: radial-gradient(circle at top, rgba(77, 127, 233, 0.18), rgba(255, 255, 255, 0.02) 52%), rgba(255, 255, 255, 0.03);
  border: 1px solid var(--theme-border);
}
.customItemModal__label {
  font-size: 11px;
  color: var(--theme-text-faint);
}
.customItemModal__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.customItemModal__title {
  font-size: 19px;
  font-weight: 900;
  line-height: 1.35;
  word-break: break-word;
}
.customItemModal__source {
  margin-top: 4px;
  font-size: 12px;
  color: var(--theme-text-soft);
}
.customItemModal__metaList {
  display: grid;
  gap: 10px;
  padding: 14px 16px;
  border-radius: 18px;
  border: 1px solid var(--theme-border);
  background: var(--theme-surface-soft);
}
.customItemModal__metaRow {
  display: grid;
  gap: 4px;
  min-width: 0;
}
.customItemModal__metaRow span {
  font-size: 11px;
  color: var(--theme-text-faint);
}
.customItemModal__metaRow strong {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--theme-text);
}
.customItemModal__actions {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  align-self: end;
}
.customItemModal__action {
  width: 100%;
  min-width: 0;
  padding-inline: 12px;
  white-space: normal;
  line-height: 1.35;
  text-align: center;
}
.modalCard--customItem {
  width: min(1480px, calc(100vw - 40px));
  min-width: min(980px, calc(100vw - 40px));
  height: min(860px, calc(100dvh - 40px));
  max-height: calc(100dvh - 40px);
  padding: 0;
  overflow: hidden;
  border-radius: 28px;
  border: 1px solid var(--theme-border-strong);
  background: linear-gradient(180deg, rgba(34, 34, 34, 0.98), rgba(18, 18, 18, 0.98));
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.42);
}
.pager {
  margin-top: 16px;
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.pager__info {
  opacity: 0.82;
  font-size: 14px;
}
.userList {
  margin-top: 14px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}
.userCard {
  position: relative;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 18px;
  background: var(--theme-surface-soft);
  padding: 24px 16px 16px;
  overflow: visible;
}
.userCard__head {
  display: block;
}
.userCard__identityMeta {
  min-width: 0;
}
.userCard__identity {
  display: flex;
  gap: 12px;
  align-items: center;
  min-width: 0;
}
.userAvatarWrap {
  position: relative;
  width: 56px;
  height: 56px;
  flex: 0 0 auto;
}
.userCard__title {
  font-weight: 900;
}
.userCard__meta {
  margin-top: 4px;
  opacity: 0.72;
  font-size: 13px;
}
.userAvatar {
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(96, 165, 250, 0.18);
}
.userAvatarButton {
  position: relative;
  padding: 0;
  cursor: pointer;
}
.userAvatarButton:disabled {
  cursor: wait;
}
.userAvatarRemoveButton {
  position: absolute;
  top: -4px;
  right: -4px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(10, 14, 22, 0.96);
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.3);
  cursor: pointer;
  z-index: 2;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(2px) scale(0.96);
  transition: opacity 160ms ease, transform 160ms ease, background 160ms ease, visibility 160ms ease;
}
.userAvatarRemoveIcon {
  color: rgba(255, 255, 255, 0.96);
}
.userAvatarRemoveButton:disabled {
  opacity: 0.45;
  cursor: wait;
}
.userAvatarRemoveButton:hover {
  background: rgba(255, 255, 255, 0.12);
}
.userAvatarButton__overlay {
  position: absolute;
  inset: auto 0 0 0;
  padding: 10px 0 6px;
  background: linear-gradient(180deg, rgba(7, 10, 18, 0), rgba(7, 10, 18, 0.88));
  color: var(--theme-text);
  font-size: 10px;
  font-weight: 800;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 160ms ease, transform 160ms ease;
}
.userAvatarWrap:hover .userAvatarButton__overlay,
.userAvatarWrap:focus-within .userAvatarButton__overlay {
  opacity: 1;
  transform: translateY(0);
}
.userAvatarWrap:hover .userAvatarRemoveButton,
.userAvatarWrap:focus-within .userAvatarRemoveButton {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0) scale(1);
}
.userAvatar__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.userAvatar__fallback {
  font-size: 18px;
  font-weight: 900;
}
.userInfoList {
  margin-top: 14px;
  display: grid;
  gap: 8px;
}
.userInfoLine {
  display: flex;
  gap: 12px;
  justify-content: space-between;
  align-items: baseline;
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.userInfoLine span {
  font-size: 12px;
  color: var(--theme-text-soft);
}
.userInfoLine strong {
  min-width: 0;
  text-align: right;
  font-size: 14px;
  font-weight: 900;
}
.field {
  display: grid;
  gap: 8px;
}
.field__label {
  font-size: 13px;
  color: var(--theme-text-soft);
}
.field__input {
  width: 100%;
  padding: 14px 0;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  background: transparent;
  color: var(--theme-text-strong);
  outline: none;
  font-size: 18px;
  letter-spacing: -0.02em;
}
.field__input:focus {
  border-bottom-color: rgba(96, 165, 250, 0.9);
}
.field__hint {
  font-size: 12px;
  color: var(--theme-text-faint);
}

.userEditForm {
  display: grid;
  gap: 18px;
}
.userEditForm .field {
  gap: 10px;
}
.modalCard--userEdit {
  max-width: 520px;
}

.userCard__actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.userCard__actions--compact {
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: center;
  margin-top: 12px;
}
.roleBadge {
  width: fit-content;
  padding: 6px 10px;
  border-radius: 999px;
  border: 1px solid rgba(96, 165, 250, 0.28);
  background: rgba(96, 165, 250, 0.1);
  color: color-mix(in srgb, var(--theme-accent-bg) 80%, white);
  font-size: 12px;
  font-weight: 700;
}
.userCard__roleBadge {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 2;
  box-shadow: 0 10px 24px rgba(7, 10, 18, 0.28);
}
.iconActionButton {
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-surface-soft);
  cursor: pointer;
}
.iconActionButton__icon {
  color: var(--theme-text);
}
.iconActionButton:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}
.iconActionButton--danger {
  border-color: rgba(239, 68, 68, 0.24);
  background: rgba(239, 68, 68, 0.1);
}
.userSaveButton:disabled {
  opacity: 0.4;
}
.userRoleAction {
  width: fit-content;
  margin-top: 8px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--theme-text-faint);
  font-size: 9px;
  line-height: 1.4;
  letter-spacing: 0.01em;
  cursor: pointer;
}
.userRoleAction:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.templateRequestList {
  margin-top: 14px;
  display: grid;
  gap: 14px;
}
.templateRequestCard {
}
.templateRequestCard--aligned {
  align-items: start;
}
.templateRequestCard__side {
  display: grid;
  gap: 12px;
  align-self: start;
  align-content: start;
}
.templateRequestCard__preview {
  align-self: start;
  display: block;
  width: 100%;
  line-height: 0;
  vertical-align: top;
}
.templateRequestCard__thumbMeta {
  display: grid;
  gap: 10px;
}
.templateRequestCard__thumbLabel {
  font-size: 11px;
  color: var(--theme-text-faint);
}
.templateRequestCard__thumbValue {
  font-size: 14px;
  font-weight: 800;
  color: var(--theme-text);
  word-break: break-word;
}
.templateRequestCard__items {
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.templateRequestCard__actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  flex-wrap: wrap;
}
.requestPreview {
  display: grid;
}
.requestPreview__sheet {
  display: grid;
  gap: 16px;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  padding: 28px;
  border-radius: 24px;
  border: 1px solid var(--theme-border);
  background: color-mix(in srgb, var(--theme-main-bg) 92%, transparent);
  max-height: min(78vh, 980px);
  overflow: auto;
  overscroll-behavior: contain;
}
.requestPreview__title {
  font-size: 28px;
  font-weight: 900;
  letter-spacing: -0.03em;
}
.requestPreview__description {
  margin-top: -8px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--theme-text-muted);
}
.requestPreview__meta {
  color: var(--theme-text-soft);
  font-size: 13px;
}
.requestPreview__columns {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 10px;
  margin-bottom: 10px;
}
.requestPreview__columnsSpacer {
  min-width: 0;
}
.requestPreview__columnsGrid {
  display: grid;
  gap: 10px;
}
.requestPreview__columnHeader {
  min-height: 20px;
  font-size: 12px;
  font-weight: 800;
  text-align: center;
  opacity: 0.72;
}
.requestPreview__rows {
  display: grid;
  gap: 10px;
}
.requestPreview__row {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 10px;
}
.requestPreview__label {
  display: grid;
  place-items: center;
  padding: 10px 12px;
  text-align: center;
  font-weight: 900;
  border-radius: 14px;
  background: var(--theme-surface-soft-2);
  border: 1px solid var(--theme-border-strong);
}
.requestPreview__dropGrid {
  display: grid;
  gap: 10px;
}
.requestPreview__dropColumn {
  display: grid;
  gap: 8px;
}
.requestPreview__drop {
  border-radius: 14px;
  background: var(--theme-pill-bg);
  border: 1px solid var(--theme-border);
  min-height: calc(var(--thumb-size, 80px) + 24px);
  padding: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-content: flex-start;
}
.requestPreview__item {
  display: inline-flex;
  position: relative;
  overflow: hidden;
  border-radius: 16px;
}
.requestPreview__item--muted {
  opacity: 0.52;
  filter: grayscale(0.22) brightness(0.78);
}
.requestPreview__itemThumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  display: block;
}
.requestPreview__itemLabel {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 8px;
  padding: 4px 6px;
  border-radius: 8px;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0.7));
  font-size: 11px;
  font-weight: 700;
  text-align: center;
  line-height: 1.3;
}
.requestPreview__pool {
  display: grid;
  gap: 10px;
  padding-top: 8px;
}
.requestPreview__poolTitle {
  font-weight: 900;
  opacity: 0.82;
}
.requestPreview__poolGrid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.requestPreview__poolItem {
  display: inline-flex;
  position: relative;
}
.tierAdminList {
  margin-top: 14px;
  display: grid;
  gap: 14px;
}
.tierAdminCard {
  display: grid;
  grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
  gap: 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 20px;
  background: var(--theme-pill-bg);
  padding: 16px;
}
.tierAdminCard__preview {
  cursor: pointer;
  appearance: none;
  border: 0;
  padding: 0;
  background: transparent;
  text-align: left;
  align-self: start;
  display: block;
  width: 100%;
  line-height: 0;
}
.tierAdminCard__thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  object-position: top center;
  display: block;
  border-radius: 14px;
  background: var(--theme-surface-soft);
}
.tierAdminCard__thumb--empty {
  background: linear-gradient(135deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
}
.tierAdminCard__body {
  min-width: 0;
  display: grid;
  gap: 14px;
}
.tierAdminCard__head {
  display: flex;
  gap: 12px;
  justify-content: space-between;
  align-items: flex-start;
}
.tierAdminCard__title {
  font-size: 18px;
  font-weight: 900;
}
.tierAdminCard__desc {
  margin-top: 6px;
  color: var(--theme-text-muted);
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.tierAdminCard__meta {
  margin-top: 4px;
  opacity: 0.74;
  font-size: 13px;
  word-break: break-word;
}
.tierAdminCard__stats {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.pill {
  display: inline-flex;
  align-items: center;
  padding: 7px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--theme-surface-soft);
  color: var(--theme-text);
  font-size: 12px;
  font-weight: 800;
}
.pill--accent {
  border-color: rgba(251, 191, 36, 0.32);
  background: rgba(251, 191, 36, 0.12);
  color: rgba(253, 230, 138, 0.96);
}
.pill--link {
  color: var(--theme-text);
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease, transform 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.pill--link:hover {
  color: var(--theme-text-strong);
  border-color: rgba(96, 165, 250, 0.4);
  background: color-mix(in srgb, var(--theme-surface-soft) 76%, rgba(96, 165, 250, 0.2));
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18);
  transform: translateY(-1px);
}
.pill--link:focus-visible {
  outline: 2px solid rgba(96, 165, 250, 0.42);
  outline-offset: 2px;
}
.tierAdminSection {
  display: grid;
  gap: 10px;
  padding: 14px;
  border-radius: 16px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
}
.tierAdminSection__title {
  font-weight: 800;
}
.tierAdminSection__actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.tierAdminItemList {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 10px;
}
.tierAdminItem {
  display: grid;
  gap: 8px;
  justify-items: center;
  padding: 12px 10px;
  border-radius: 14px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
  color: var(--theme-text);
  cursor: pointer;
  text-align: center;
  min-width: 0;
}
.tierAdminItem__thumb {
  width: min(100%, 72px);
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 12px;
}
.tierAdminItem__title {
  width: 100%;
  font-weight: 800;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}
.modalOverlay {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 20px;
  background: color-mix(in srgb, var(--theme-body-bg) 76%, transparent);
  backdrop-filter: blur(6px);
  overscroll-behavior: contain;
}
.modalCard {
  width: min(560px, 100%);
  display: grid;
  gap: 14px;
  padding: 20px;
  border-radius: 24px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: color-mix(in srgb, var(--theme-main-bg) 96%, transparent);
}
.modalCard--preview {
  width: min(1200px, 100%);
  max-height: calc(100dvh - 40px);
  overflow: auto;
  overscroll-behavior: contain;
}
.modalCard__titleRow {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
}
.modalCard__title {
  font-size: 18px;
  font-weight: 900;
}
.modalCard__desc {
  opacity: 0.78;
  line-height: 1.5;
}
.modalCard__form {
  display: grid;
  gap: 10px;
}
.modalCard__actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 8px;
}
.previewFrame {
  width: 100%;
  min-height: min(80vh, 820px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
  background: var(--theme-pill-bg);
}
.importModeTabs {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.checkRow {
  margin-top: 12px;
  display: inline-flex;
  gap: 8px;
  align-items: center;
  opacity: 0.88;
}
.checkRow--compact {
  margin-top: 0;
}
.checkRow--toolbar {
  margin-top: 0;
}
@media (max-width: 980px) {
  .adminHero__stats {
    grid-template-columns: 1fr;
  }
  .customItemModal {
    grid-template-columns: 1fr;
    min-height: auto;
  }
  .requestPreview__summary,
  .requestPreview__frame {
    padding: 18px;
    gap: 18px;
  }
  .requestPreview__boardHead,
  .requestPreview__row {
    grid-template-columns: 1fr;
  }
  .modalCard--customItem {
    width: min(100%, calc(100vw - 24px));
    min-width: 0;
    height: min(100%, calc(100dvh - 24px));
  }
  .customItemModal__pickerPanel {
    border-right: 0;
    border-bottom: 1px solid var(--theme-border);
    padding: 20px 18px;
  }
  .customItemModal__body {
    min-height: 0;
    padding: 20px 18px 18px;
  }
  .customItemModal__content {
    min-height: 0;
  }
  .customItemModal__labelEditor {
    grid-template-columns: 1fr;
  }
  .customItemModal__actions {
    grid-template-columns: 1fr;
  }
  .adminSidebar {
    display: none;
  }
  .featuredOrderPanel,
  .section--topGrid,
  .gameManagerGrid,
  .toolbar,
  .itemComposer,
  .tierAdminCard,
  .templateRequestCard__form,
  .toolbar--secondary {
    grid-template-columns: 1fr;
  }
  .itemPreviewCard {
    max-width: none;
  }
  .userCard__identity {
    width: 100%;
  }
  .userInfoLine {
    display: grid;
    gap: 4px;
  }
  .userInfoLine strong {
    text-align: left;
  }
  .userCard__actions--compact {
    grid-template-columns: repeat(3, minmax(0, auto));
  }
  .userSaveButton {
    width: 100%;
  }
}


@media (max-width: 640px) {
  .adminHero {
    padding: 16px;
  }
  .adminHero__title {
    font-size: 24px;
  }
  .thumbGrid,
  .userList {
    grid-template-columns: 1fr;
  }
  .customItemGrid {
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  }
  .tierAdminCard__head {
    display: grid;
  }
  .customItemCard {
    align-items: stretch;
    padding: 10px;
  }
  .customItemCard__image {
    width: 100%;
    aspect-ratio: 1 / 1;
  }
}
</style>
