<script setup>
import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
import SvgIcon from '../components/SvgIcon.vue'
import addColumnRightIcon from '../assets/icons/add_column_right.svg'
import addRowBelowIcon from '../assets/icons/add_row_below.svg'
import { api } from '../lib/api'
import { toApiUrl } from '../lib/runtime'
import { useAuthStore } from '../stores/auth'
import { useToast } from '../composables/useToast'

const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const toast = useToast()
const globalRightRailOpen = inject('rightRailOpen', ref(true))
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
const gameId = computed(() => route.params.gameId)
const tierListId = computed(() => route.params.tierListId)
const previewMode = computed(() => route.query.preview === '1')
const gameName = ref('')

const columns = ref([{ id: 'col-1', name: '' }])
const groups = ref([
  { id: 'gS', name: 'S', itemIds: [], cells: [[]] },
  { id: 'gA', name: 'A', itemIds: [], cells: [[]] },
  { id: 'gB', name: 'B', itemIds: [], cells: [[]] },
  { id: 'gC', name: 'C', itemIds: [], cells: [[]] },
])

const pool = ref([])
const itemsById = ref({})

const title = ref('')
const thumbnailSrc = ref('')
const pendingThumbnailFile = ref(null)
const thumbnailPreviewUrl = ref('')
const description = ref('')
const isPublic = ref(true)
const showCharacterNames = ref(false)
const error = ref('')
const isSaving = ref(false)
const isExporting = ref(false)
const isSaveModalOpen = ref(false)
const isTemplateRequestModalOpen = ref(false)
const isTemplateUpdateModalOpen = ref(false)
const templateRequestDraftTitle = ref('')
const templateRequestDraftDescription = ref('')
const templateRequestSaveToMyTierList = ref(true)
const isDeleteModalOpen = ref(false)
const isGroupDeleteModalOpen = ref(false)
const isColumnDeleteModalOpen = ref(false)
const pendingRemoveGroupId = ref('')
const pendingRemoveColumnIndex = ref(-1)
const ownerId = ref('')
const authorName = ref('')
const authorAccountName = ref('')
const updatedAt = ref(0)
const sourceTierListId = ref('')
const sourceSnapshotTitle = ref('')
const sourceSnapshotAuthor = ref('')
const isDragActive = ref(false)
const isThumbnailDragActive = ref(false)
const iconSize = ref(80)
const isFavoriteBusy = ref(false)
const favoriteCount = ref(0)
const isFavorited = ref(false)
const isRequestingTemplate = ref(false)
const isDeleting = ref(false)

const boardEl = ref(null)
const exportBoardEl = ref(null)
const groupListEl = ref(null)
const poolEl = ref(null)
const groupDropEls = ref({})
const fileEl = ref(null)
const thumbnailFileEl = ref(null)
const groupSortable = ref(null)
const poolSortable = ref(null)
const dropSortables = ref([])

const isNewTierList = computed(() => tierListId.value === 'new')
const canEdit = computed(() => !!auth.user && (!ownerId.value || ownerId.value === auth.user.id))
const iconSizeOptions = [48, 64, 80, 96, 112]
const hasCustomTitle = computed(() => !!(title.value || '').trim())
const fallbackTimestamp = computed(() => (updatedAt.value ? updatedAt.value : Date.now()))
const effectiveAuthorName = computed(() => {
  const currentNickname = (auth.user?.nickname || '').trim()
  if (currentNickname) return currentNickname
  if ((authorName.value || '').trim()) return authorName.value.trim()
  const currentEmail = (auth.user?.email || '').trim()
  if (currentEmail) return currentEmail.split('@')[0] || currentEmail
  return (authorAccountName.value || '').trim() || 'unknown'
})
const effectiveTitle = computed(() => {
  const customTitle = (title.value || '').trim()
  if (customTitle) return customTitle
  return (gameName.value || gameId.value || 'Tier Maker').trim()
})
const displayThumbnailUrl = computed(() => thumbnailPreviewUrl.value || (thumbnailSrc.value ? resolveItemSrc({ src: thumbnailSrc.value }) : ''))
const untitledWarning = computed(
  () =>
    canEdit.value &&
    !hasCustomTitle.value &&
    '제목 없이 저장된 티어표는 무분별한 도배 방지를 위해 관리자에 의해 임의 삭제될 수 있어요.'
)
const canFavorite = computed(() => !!auth.user && !isNewTierList.value && !canEdit.value)
const canDuplicate = computed(() => !!auth.user && !isNewTierList.value && !canEdit.value)
const copiedFromLabel = computed(() => {
  if (!sourceTierListId.value) return ''
  const parts = []
  if (sourceSnapshotTitle.value) parts.push(`원본 ${sourceSnapshotTitle.value}`)
  if (sourceSnapshotAuthor.value) parts.push(sourceSnapshotAuthor.value)
  return parts.join(' · ') || '복사해 온 티어표'
})
const customItems = computed(() =>
  Object.values(itemsById.value)
    .filter((item) => item?.origin === 'custom')
    .sort((a, b) => (a.label || '').localeCompare(b.label || '', 'ko'))
)
const canRequestTemplateCreate = computed(
  () => canEdit.value && !isNewTierList.value && gameId.value === 'freeform' && customItems.value.length > 0
)
const canRequestTemplateUpdate = computed(
  () => canEdit.value && !isNewTierList.value && gameId.value !== 'freeform' && customItems.value.length > 0
)
const canSubmitTemplateCreateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
const canSubmitTemplateUpdateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
const templateRequestTargetLabel = computed(() => (gameId.value === 'freeform' ? '새로운 템플릿' : (gameName.value || gameId.value || '선택한 게임')))

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

function formatTitleDate(ts) {
  const date = new Date(ts)
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  const hours = String(date.getHours()).padStart(2, '0')
  const minutes = String(date.getMinutes()).padStart(2, '0')
  return `${year}-${month}-${day} ${hours}:${minutes}`
}

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

function setIconSize(nextSize) {
  iconSize.value = nextSize
}

function getGroupCellIds(group, columnIndex) {
  return Array.isArray(group?.cells?.[columnIndex]) ? group.cells[columnIndex] : []
}

function syncGroupItemIds(group) {
  group.itemIds = (group.cells || []).flat()
}

function normalizeLoadedColumns(rawGroups) {
  const fromGroup = Array.isArray(rawGroups) ? rawGroups.find((group) => Array.isArray(group?.columnNames) && group.columnNames.length) : null
  const rawColumns = Array.isArray(fromGroup?.columnNames) ? fromGroup.columnNames : []
  const cellCount = Math.max(1, ...(Array.isArray(rawGroups) ? rawGroups.map((group) => (Array.isArray(group?.cells) ? group.cells.length : 0)) : [0]))
  const size = Math.max(rawColumns.length || 0, cellCount)
  return Array.from({ length: size || 1 }, (_, index) => ({
    id: rawColumns[index]?.id || `col-${index + 1}` ,
    name: typeof rawColumns[index]?.name === 'string' ? rawColumns[index].name.slice(0, 16) : '',
  }))
}

function normalizeLoadedGroups(rawGroups, nextColumns = columns.value) {
  if (!Array.isArray(rawGroups) || !rawGroups.length) {
    return [
      { id: 'gS', name: 'S', itemIds: [], cells: nextColumns.map(() => []) },
      { id: 'gA', name: 'A', itemIds: [], cells: nextColumns.map(() => []) },
      { id: 'gB', name: 'B', itemIds: [], cells: nextColumns.map(() => []) },
      { id: 'gC', name: 'C', itemIds: [], cells: nextColumns.map(() => []) },
    ]
  }
  return rawGroups.map((group, index) => {
    const cells = Array.from({ length: nextColumns.length }, (_, cellIndex) => {
      if (Array.isArray(group?.cells?.[cellIndex])) return [...group.cells[cellIndex]]
      if (cellIndex === 0 && Array.isArray(group?.itemIds)) return [...group.itemIds]
      return []
    })
    return {
      id: typeof group?.id === 'string' && group.id ? group.id : `g-${index + 1}` ,
      name: typeof group?.name === 'string' && group.name ? group.name.slice(0, 16) : 'Tier',
      itemIds: cells.flat(),
      cells,
    }
  })
}

function buildGroupPayload() {
  return groups.value.map((group) => ({
    id: group.id,
    name: group.name,
    itemIds: (group.cells || []).flat(),
    cells: (group.cells || []).map((cell) => [...cell]),
    columnNames: columns.value.map((column) => ({ id: column.id, name: column.name || '' })),
  }))
}

function removeItemFromGroup(groupId, columnIndex, itemId) {
  if (!canEdit.value || !groupId || columnIndex == null || !itemId) return
  const targetGroup = groups.value.find((group) => group.id === groupId)
  if (!targetGroup) return
  const nextCells = [...targetGroup.cells]
  nextCells[columnIndex] = getGroupCellIds(targetGroup, columnIndex).filter((id) => id !== itemId)
  targetGroup.cells = nextCells
  syncGroupItemIds(targetGroup)
  pool.value = [itemId, ...pool.value.filter((id) => id !== itemId)]
}

function setGroupDropEl(groupId, columnIndex, el) {
  const key = `${groupId}::${columnIndex}`
  if (!el) {
    delete groupDropEls.value[key]
    return
  }
  groupDropEls.value[key] = el
}

function getListByContainer(containerEl) {
  if (!containerEl) return { type: null, groupId: null, columnIndex: null }
  const t = containerEl.getAttribute('data-list-type')
  if (t === 'pool') return { type: 'pool', groupId: null, columnIndex: null }
  if (t === 'group') {
    return {
      type: 'group',
      groupId: containerEl.getAttribute('data-group-id'),
      columnIndex: Number(containerEl.getAttribute('data-column-index')),
    }
  }
  return { type: null, groupId: null, columnIndex: null }
}

function normalizeSort(containerEl) {
  const ids = Array.from(containerEl.querySelectorAll('[data-item-id]')).map((n) => n.getAttribute('data-item-id'))
  const meta = getListByContainer(containerEl)
  if (meta.type === 'pool') {
    pool.value = ids
    return
  }
  if (meta.type === 'group') {
    const g = groups.value.find((x) => x.id === meta.groupId)
    if (!g || !Number.isInteger(meta.columnIndex)) return
    const nextCells = [...g.cells]
    nextCells[meta.columnIndex] = ids
    g.cells = nextCells
    syncGroupItemIds(g)
  }
}

function resolveItemSrc(item) {
  const src = item?.src || ''
  if (!src) return ''
  if (src.startsWith('blob:')) return src
  return toApiUrl(src)
}

async function initSortables() {
  if (!poolEl.value || !groupListEl.value) return

  destroySortables()

  groupSortable.value = Sortable.create(groupListEl.value, {
    animation: 160,
    handle: '[data-group-handle]',
    ghostClass: 'ghost',
    chosenClass: 'chosen',
    onEnd: (evt) => {
      const next = [...groups.value]
      const [moved] = next.splice(evt.oldIndex, 1)
      next.splice(evt.newIndex, 0, moved)
      groups.value = next
    },
  })

  poolSortable.value = Sortable.create(poolEl.value, {
    group: 'tier-items',
    animation: 160,
    draggable: '[data-item-id]',
    ghostClass: 'ghost',
    chosenClass: 'chosen',
    onSort: () => normalizeSort(poolEl.value),
    onAdd: () => normalizeSort(poolEl.value),
  })

  dropSortables.value = Object.entries(groupDropEls.value).map(([, el]) =>
    Sortable.create(el, {
      group: 'tier-items',
      animation: 160,
      draggable: '[data-item-id]',
      ghostClass: 'ghost',
      chosenClass: 'chosen',
      onSort: () => normalizeSort(el),
      onAdd: () => normalizeSort(el),
    })
  )
}

function destroySortables() {
  if (groupSortable.value) {
    groupSortable.value.destroy()
    groupSortable.value = null
  }
  if (poolSortable.value) {
    poolSortable.value.destroy()
    poolSortable.value = null
  }
  dropSortables.value.forEach((instance) => instance.destroy())
  dropSortables.value = []
}

async function syncSortables() {
  await nextTick()
  if (canEdit.value) {
    await initSortables()
  }
}

function createGroupName(index = groups.value.length) {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  if (index < alphabet.length) return alphabet[index]
  return `Tier ${index + 1}`
}

function createColumnName(index = columns.value.length) {
  return `열 ${index + 1}`
}

function createCustomItemLabel(fileName = '') {
  const normalized = String(fileName || '')
    .replace(/\.[^.]+$/, '')
    .replace(/[_-]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
  return (normalized || 'custom').slice(0, 60)
}

async function addGroup() {
  groups.value = [
    ...groups.value,
    {
      id: `g-${Date.now()}-${Math.random().toString(16).slice(2, 8)}` ,
      name: createGroupName(),
      itemIds: [],
      cells: columns.value.map(() => []),
    },
  ]
  await syncSortables()
}

async function addColumn() {
  columns.value = [
    ...columns.value,
    { id: `col-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, name: createColumnName() },
  ]
  groups.value = groups.value.map((group) => ({
    ...group,
    cells: [...group.cells, []],
    itemIds: [...group.itemIds],
  }))
  await syncSortables()
}

async function removeColumn(columnIndex) {
  if (!canEdit.value || columns.value.length <= 1) return
  const nextColumns = columns.value.filter((_, index) => index !== columnIndex)
  groups.value = groups.value.map((group) => {
    const nextCells = group.cells.filter((_, index) => index !== columnIndex)
    const removed = Array.isArray(group.cells[columnIndex]) ? group.cells[columnIndex] : []
    if (nextCells[0] && removed.length) nextCells[0] = [...removed, ...nextCells[0]]
    const nextGroup = { ...group, cells: nextCells }
    syncGroupItemIds(nextGroup)
    return nextGroup
  })
  Object.keys(groupDropEls.value).forEach((key) => {
    if (key.endsWith(`::${columnIndex}`)) delete groupDropEls.value[key]
  })
  columns.value = nextColumns
  await syncSortables()
}

function openColumnDeleteModal(columnIndex) {
  if (!canEdit.value || columns.value.length <= 1) return
  pendingRemoveColumnIndex.value = columnIndex
  isColumnDeleteModalOpen.value = true
}

function closeColumnDeleteModal() {
  isColumnDeleteModalOpen.value = false
  pendingRemoveColumnIndex.value = -1
}

async function confirmRemoveColumn() {
  const columnIndex = pendingRemoveColumnIndex.value
  closeColumnDeleteModal()
  if (columnIndex < 0) return
  await removeColumn(columnIndex)
}

async function performRemoveGroup(groupId) {
  if (groups.value.length <= 1) return
  const target = groups.value.find((group) => group.id === groupId)
  if (!target) return
  pool.value = [...target.itemIds, ...pool.value]
  groups.value = groups.value.filter((group) => group.id !== groupId)
  Object.keys(groupDropEls.value).forEach((key) => {
    if (key.startsWith(`${groupId}::`)) delete groupDropEls.value[key]
  })
  await syncSortables()
}

function openGroupDeleteModal(groupId) {
  if (!canEdit.value || groups.value.length <= 1 || !groupId) return
  pendingRemoveGroupId.value = groupId
  isGroupDeleteModalOpen.value = true
}

function closeGroupDeleteModal() {
  isGroupDeleteModalOpen.value = false
  pendingRemoveGroupId.value = ''
}

async function confirmRemoveGroup() {
  const groupId = pendingRemoveGroupId.value
  closeGroupDeleteModal()
  if (!groupId) return
  await performRemoveGroup(groupId)
}

function addCustomImage(file) {
  if (!file || !file.type.startsWith('image/')) return
  const url = URL.createObjectURL(file)
  const id = `c-${Date.now()}-${Math.random().toString(16).slice(2)}`
  itemsById.value = {
    ...itemsById.value,
    [id]: { id, src: url, label: createCustomItemLabel(file.name), origin: 'custom', pendingFile: file },
  }
  pool.value = [id, ...pool.value]
}

function updateCustomItemLabel(itemId, nextLabel) {
  const item = itemsById.value[itemId]
  if (!item || item.origin !== 'custom') return
  itemsById.value = {
    ...itemsById.value,
    [itemId]: {
      ...item,
      label: nextLabel.slice(0, 60),
    },
  }
}

function openFile() {
  if (!canEdit.value) return
  fileEl.value?.click()
}

function openThumbnailFile() {
  if (!canEdit.value) return
  thumbnailFileEl.value?.click()
}

function applyThumbnailFile(file) {
  if (!file || !file.type.startsWith('image/')) return
  if (thumbnailPreviewUrl.value) {
    URL.revokeObjectURL(thumbnailPreviewUrl.value)
    thumbnailPreviewUrl.value = ''
  }
  pendingThumbnailFile.value = file
  thumbnailPreviewUrl.value = URL.createObjectURL(file)
}

function onThumbnailDragEnter() {
  if (!canEdit.value) return
  isThumbnailDragActive.value = true
}

function onThumbnailDragLeave(event) {
  if (!event.currentTarget.contains(event.relatedTarget)) {
    isThumbnailDragActive.value = false
  }
}

function onThumbnailDrop(event) {
  if (!canEdit.value) return
  isThumbnailDragActive.value = false
  const files = Array.from(event.dataTransfer?.files || []).filter((file) => file.type.startsWith('image/'))
  if (!files.length) return
  if (files.length > 1) {
    toast.info('대표 썸네일은 하나만 설정할 수 있어요. 첫 번째 이미지를 사용할게요.')
  }
  applyThumbnailFile(files[0])
}

function onThumbnailChange(event) {
  const files = Array.from(event.target.files || []).filter((file) => file.type.startsWith('image/'))
  if (!files.length) {
    event.target.value = ''
    return
  }
  if (files.length > 1) {
    toast.info('대표 썸네일은 하나만 설정할 수 있어요. 첫 번째 이미지를 사용할게요.')
  }
  applyThumbnailFile(files[0])
  event.target.value = ''
}

function clearThumbnail() {
  if (thumbnailPreviewUrl.value) {
    URL.revokeObjectURL(thumbnailPreviewUrl.value)
    thumbnailPreviewUrl.value = ''
  }
  pendingThumbnailFile.value = null
  thumbnailSrc.value = ''
}

function onFileChange(e) {
  const files = Array.from(e.target.files || [])
  if (!files.length) return
  files.forEach(addCustomImage)
  e.target.value = ''
}

function onDragEnter() {
  if (!canEdit.value) return
  isDragActive.value = true
}

function onDragLeave(event) {
  if (!event.currentTarget.contains(event.relatedTarget)) {
    isDragActive.value = false
  }
}

function onDropFiles(event) {
  if (!canEdit.value) return
  isDragActive.value = false
  const files = Array.from(event.dataTransfer?.files || []).filter((file) => file.type.startsWith('image/'))
  files.forEach(addCustomImage)
}

async function downloadImage() {
  if (!boardEl.value) return
  isExporting.value = true
  await nextTick()

  try {
    const targetEl = exportBoardEl.value || boardEl.value
    const blob = await htmlToImage.toBlob(targetEl, { pixelRatio: 1.5, backgroundColor: '#0b1220' })
    if (!blob) throw new Error('image_export_failed')

    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `${effectiveTitle.value.trim()}.png`
    document.body.appendChild(a)
    a.click()
    a.remove()
    URL.revokeObjectURL(url)
  } finally {
    isExporting.value = false
  }
}

async function uploadPendingCustomItems() {
  const entries = Object.values(itemsById.value).filter((item) => item?.origin === 'custom' && item?.pendingFile)

  for (const item of entries) {
    const fd = new FormData()
    fd.append('label', createCustomItemLabel(item.label || 'custom'))
    fd.append('image', item.pendingFile)

    const res = await fetch(toApiUrl('/api/tierlists/custom-items'), {
      method: 'POST',
      credentials: 'include',
      body: fd,
    })

    if (!res.ok) {
      throw new Error('custom_upload_failed')
    }

    const data = await res.json()
    const uploaded = data.item
    const nextItemsById = { ...itemsById.value }
    delete nextItemsById[item.id]
    nextItemsById[uploaded.id] = {
      id: uploaded.id,
      src: uploaded.src,
      label: uploaded.label,
      origin: 'custom',
    }
    itemsById.value = nextItemsById
    pool.value = pool.value.map((currentId) => (currentId === item.id ? uploaded.id : currentId))
    groups.value = groups.value.map((group) => {
      const nextGroup = {
        ...group,
        cells: group.cells.map((cell) => cell.map((currentId) => (currentId === item.id ? uploaded.id : currentId))),
      }
      syncGroupItemIds(nextGroup)
      return nextGroup
    })
  }
}

async function uploadPendingThumbnail() {
  if (!pendingThumbnailFile.value) return thumbnailSrc.value || ''
  const data = await api.uploadTierListThumbnail(pendingThumbnailFile.value)
  if (thumbnailPreviewUrl.value) {
    URL.revokeObjectURL(thumbnailPreviewUrl.value)
    thumbnailPreviewUrl.value = ''
  }
  pendingThumbnailFile.value = null
  thumbnailSrc.value = data.thumbnailSrc || ''
  return thumbnailSrc.value
}

function buildPayload(existingId) {
  const finalTitle = effectiveTitle.value
  return {
    id: existingId || undefined,
    gameId: gameId.value,
    title: finalTitle,
    thumbnailSrc: thumbnailSrc.value || '',
    description: (description.value || '').trim(),
    isPublic: !!isPublic.value,
    showCharacterNames: !!showCharacterNames.value,
    sourceTierListId: sourceTierListId.value || '',
    sourceSnapshotTitle: sourceSnapshotTitle.value || '',
    sourceSnapshotAuthor: sourceSnapshotAuthor.value || '',
    groups: buildGroupPayload(),
    pool: Object.values(itemsById.value),
  }
}

async function persistTierList({ showModal = false } = {}) {
  await uploadPendingCustomItems()
  await uploadPendingThumbnail()
  const payload = buildPayload(tierListId.value && tierListId.value !== 'new' ? tierListId.value : null)
  const res = await api.saveTierList(payload)
  const savedTierListId = res.tierList?.id || tierListId.value
  if (tierListId.value === 'new' && res.tierList?.id) {
    await router.replace(`/editor/${gameId.value}/${res.tierList.id}`)
  }
  updatedAt.value = Number(res.tierList?.updatedAt || Date.now())
  authorName.value = res.tierList?.authorName || effectiveAuthorName.value
  authorAccountName.value = res.tierList?.authorAccountName || authorAccountName.value
  favoriteCount.value = Number(res.tierList?.favoriteCount || favoriteCount.value || 0)
  isFavorited.value = !!res.tierList?.isFavorited
  if (showModal) isSaveModalOpen.value = true
  return { ...res, savedTierListId }
}

async function save() {
  error.value = ''
  isSaving.value = true
  try {
    await persistTierList({ showModal: true })
  } catch (e) {
    error.value = '저장 실패: 로그인 상태인지 확인해주세요.'
  } finally {
    isSaving.value = false
  }
}

function closeSaveModal() {
  isSaveModalOpen.value = false
}

function resetTemplateRequestDrafts() {
  templateRequestDraftTitle.value = (title.value || '').trim()
  templateRequestDraftDescription.value = (description.value || '').trim()
  templateRequestSaveToMyTierList.value = true
}

function openTemplateRequestModal() {
  resetTemplateRequestDrafts()
  isTemplateRequestModalOpen.value = true
}

function closeTemplateRequestModal() {
  isTemplateRequestModalOpen.value = false
  resetTemplateRequestDrafts()
}

function openTemplateUpdateModal() {
  resetTemplateRequestDrafts()
  isTemplateUpdateModalOpen.value = true
}

function closeTemplateUpdateModal() {
  isTemplateUpdateModalOpen.value = false
  resetTemplateRequestDrafts()
}

function openDeleteModal() {
  isDeleteModalOpen.value = true
}

function closeDeleteModal() {
  isDeleteModalOpen.value = false
}

async function confirmDeleteTierList() {
  if (!canEdit.value || isNewTierList.value || isDeleting.value) return
  error.value = ''
  try {
    isDeleting.value = true
    await api.deleteTierList(tierListId.value)
    closeDeleteModal()
    toast.success('티어표를 삭제했어요.')
    router.push(gameId.value === 'freeform' ? '/me' : `/games/${gameId.value}`)
  } catch (e) {
    error.value = '티어표 삭제에 실패했어요.'
  } finally {
    isDeleting.value = false
  }
}

async function duplicateCurrentTierList() {
  if (!canDuplicate.value) return
  try {
    const data = await api.duplicateTierList(tierListId.value)
    const duplicatedId = data.tierList?.id
    if (!duplicatedId) throw new Error('duplicate_failed')
    toast.success('티어표를 복사해 내 작업으로 가져왔어요.')
    router.push(`/editor/${gameId.value}/${duplicatedId}`)
  } catch (e) {
    error.value = '티어표 복사에 실패했어요.'
  }
}

async function toggleFavorite() {
  if (!canFavorite.value || isFavoriteBusy.value) return
  try {
    isFavoriteBusy.value = true
    const data = isFavorited.value ? await api.unfavoriteTierList(tierListId.value) : await api.favoriteTierList(tierListId.value)
    favoriteCount.value = Number(data.tierList?.favoriteCount || 0)
    isFavorited.value = !!data.tierList?.isFavorited
    toast.success(isFavorited.value ? '즐겨찾기에 추가했어요.' : '즐겨찾기를 해제했어요.')
  } catch (e) {
    error.value = '즐겨찾기 처리에 실패했어요.'
  } finally {
    isFavoriteBusy.value = false
  }
}

async function requestTemplate(type) {
  const shouldSaveToMyTierList = !!templateRequestSaveToMyTierList.value
  try {
    isRequestingTemplate.value = true
    await uploadPendingCustomItems()
    const uploadedThumbnailSrc = await uploadPendingThumbnail()
    const response = await api.requestTierListTemplate({
      type,
      sourceTierListId: tierListId.value !== 'new' ? tierListId.value : '',
      gameId: gameId.value,
      requestTitle: templateRequestDraftTitle.value.trim(),
      requestDescription: templateRequestDraftDescription.value.trim(),
      thumbnailSrc: uploadedThumbnailSrc || thumbnailSrc.value || '',
      isPublic: !!isPublic.value,
      showCharacterNames: !!showCharacterNames.value,
      saveToMyTierList: shouldSaveToMyTierList,
      groups: buildGroupPayload(),
      boardItems: Object.values(itemsById.value),
    })

    const savedTierList = response?.savedTierList
    if (savedTierList) {
      title.value = savedTierList.title || title.value
      description.value = savedTierList.description || ''
      updatedAt.value = Number(savedTierList.updatedAt || Date.now())
      authorName.value = savedTierList.authorName || effectiveAuthorName.value
      authorAccountName.value = savedTierList.authorAccountName || authorAccountName.value
      favoriteCount.value = Number(savedTierList.favoriteCount || favoriteCount.value || 0)
      isFavorited.value = !!savedTierList.isFavorited
      if (tierListId.value === 'new' && savedTierList.id) {
        await router.replace(`/editor/${gameId.value}/${savedTierList.id}`)
      }
    }

    if (type === 'create') closeTemplateRequestModal()
    if (type === 'update') closeTemplateUpdateModal()
    toast.success(
      type === 'create'
        ? shouldSaveToMyTierList
          ? '템플릿 등록 요청과 내 티어표 저장을 함께 완료했어요.'
          : '템플릿 등록 요청을 보냈어요.'
        : shouldSaveToMyTierList
          ? '템플릿 업데이트 요청과 내 티어표 저장을 함께 완료했어요.'
          : '템플릿 업데이트 요청을 보냈어요.'
    )
  } catch (e) {
    if (e?.message === 'custom_upload_failed') {
      toast.error('커스텀 이미지 이름이 너무 길거나 업로드 조건에 맞지 않아 요청 전에 저장하지 못했어요. 아이템 이름을 60자 이하로 줄인 뒤 다시 시도해주세요.')
      return
    }
    if (e?.status === 409) {
      toast.error('이미 처리 대기 중인 같은 요청이 있어요.')
      return
    }
    if (e?.status === 400 && e?.data?.error === 'custom_items_required') {
      toast.error('먼저 커스텀 아이템을 추가한 뒤 요청해주세요.')
      return
    }
    if (e?.status === 400 && e?.data?.error === 'bad_request') {
      toast.error('요청 제목, 설명, 아이템 이름 중 길이 제한을 넘긴 값이 없는지 확인해주세요.')
      return
    }
    if (e?.status === 500 && shouldSaveToMyTierList) {
      toast.error('템플릿 요청 중 내 티어리스트 저장에 실패했어요. 잠시 후 다시 시도해주세요.')
      return
    }
    toast.error(type === 'create' ? '템플릿 등록 요청에 실패했어요.' : '템플릿 업데이트 요청에 실패했어요.')
  } finally {
    isRequestingTemplate.value = false
  }
}

onMounted(() => {
  ;(async () => {
    await auth.refresh()
    authorName.value = (auth.user?.nickname || '').trim()
    authorAccountName.value = ((auth.user?.email || '').trim().split('@')[0] || '').trim()

    if (isNewTierList.value && !auth.user) {
      router.replace(`/login?redirect=/editor/${gameId.value}/new`)
      return
    }

    try {
      const gameRes = await api.getGame(gameId.value)
      gameName.value = gameRes.game?.name || gameId.value
      const base = (gameRes.items || []).map((img) => ({
        id: img.id,
        src: img.src,
        label: img.label,
        origin: 'game',
      }))
      const map = {}
      base.forEach((it) => (map[it.id] = it))
      itemsById.value = map
      pool.value = base.map((it) => it.id)
    } catch (e) {
      error.value = '게임 기본 이미지를 불러오지 못했어요.'
    }

    if (tierListId.value && tierListId.value !== 'new') {
      try {
        const res = await api.getTierList(tierListId.value)
        const t = res.tierList
        ownerId.value = t.authorId
        title.value = t.title
        thumbnailSrc.value = t.thumbnailSrc || ''
        description.value = t.description || ''
        isPublic.value = !!t.isPublic
        showCharacterNames.value = !!t.showCharacterNames
        authorName.value = t.authorName || ''
        authorAccountName.value = t.authorAccountName || ''
        updatedAt.value = Number(t.updatedAt || 0)
        sourceTierListId.value = t.sourceTierListId || ''
        sourceSnapshotTitle.value = t.sourceSnapshotTitle || ''
        sourceSnapshotAuthor.value = t.sourceSnapshotAuthor || ''
        favoriteCount.value = Number(t.favoriteCount || 0)
        isFavorited.value = !!t.isFavorited
        columns.value = normalizeLoadedColumns(t.groups)
        groups.value = normalizeLoadedGroups(t.groups, columns.value)
        const map = {}
        ;(t.pool || []).forEach((it) => (map[it.id] = it))
        itemsById.value = map
        const grouped = new Set()
        groups.value.forEach((group) => group.itemIds.forEach((id) => grouped.add(id)))
        pool.value = Object.keys(itemsById.value).filter((id) => !grouped.has(id))
      } catch (e) {
        error.value = '티어표를 불러오지 못했어요.'
      }
    }

    await nextTick()
    if (canEdit.value) {
      await initSortables()
    }
  })()
})

onUnmounted(() => {
  if (thumbnailPreviewUrl.value) URL.revokeObjectURL(thumbnailPreviewUrl.value)
  destroySortables()
})
</script>

<template>
  <section v-if="previewMode" class="previewOnly">
    <div class="previewOnly__sheet">
      <div class="previewOnly__title">{{ effectiveTitle }}</div>
      <div v-if="description" class="previewOnly__description">{{ description }}</div>
      <div v-if="columns.length > 1" class="previewOnly__columns">
        <div class="previewOnly__columnsSpacer" aria-hidden="true"></div>
        <div class="previewOnly__columnsGrid" :style="{ '--column-count': columns.length }">
          <div v-for="(column, columnIndex) in columns" :key="column.id" class="previewOnly__columnHeader">{{ column.name || '열 ' + (columnIndex + 1) }}</div>
        </div>
      </div>
      <div class="previewOnly__rows">
        <div v-for="g in groups" :key="g.id" class="previewOnly__row">
          <div class="previewOnly__label">{{ g.name }}</div>
          <div class="previewOnly__dropGrid" :style="{ '--column-count': columns.length }">
            <div v-for="(column, columnIndex) in columns" :key="column.id" class="previewOnly__dropColumn">
              <div class="previewOnly__drop">
                <div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="previewOnly__cell">
                  <img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
                  <div v-if="showCharacterNames" class="itemNameOverlay">{{ itemsById[id]?.label || id }}</div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div v-if="pool.length" class="previewOnly__pool">
        <div class="previewOnly__poolTitle">남은 아이템</div>
        <div class="previewOnly__poolGrid">
          <div v-for="id in pool" :key="id" class="previewOnly__poolItem previewOnly__poolItem--inactive">
            <img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
          </div>
        </div>
      </div>
    </div>
  </section>

  <template v-else>

  <div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
    <div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle">
      <div id="saveModalTitle" class="modalCard__title">저장 완료</div>
      <div class="modalCard__desc">티어표가 저장되었어요. 이어서 더 수정한 뒤 다시 저장할 수도 있어요.</div>
      <div class="modalCard__actions">
        <button class="btn btn--save" @click="closeSaveModal">확인</button>
      </div>
    </div>
  </div>

  <div v-if="isTemplateRequestModalOpen" class="modalOverlay" @click.self="closeTemplateRequestModal">
    <div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateRequestTitle">
      <div id="templateRequestTitle" class="modalCard__title">템플릿 등록 요청</div>
      <div class="modalCard__desc">
        티어표에 적어둔 제목과 설명이 있다면 그대로 가져와 두었어요. 비어 있다면 여기서 바로 작성해도 괜찮아요.
      </div>
      <div class="requestChecklist__hint">
        제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 수 있어요.
        예시: 제목 `템플릿 등록 요청`, 설명 `여름 이벤트 한정 캐릭터 중심으로 새 게임 템플릿이 필요합니다.`
      </div>
      <div class="templateRequestDraft">
        <label class="templateRequestDraft__field">
          <span class="templateRequestDraft__label">티어표 제목</span>
          <input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="120" placeholder="예: 템플릿 등록 요청" />
          <span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/120자</span>
        </label>
        <label class="templateRequestDraft__field">
          <span class="templateRequestDraft__label">티어표 설명</span>
          <textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="1000" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
          <span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/1000자</span>
        </label>
        <label class="toggleSwitch">
          <input v-model="templateRequestSaveToMyTierList" type="checkbox" />
          <span class="toggleSwitch__label">내 티어 리스트에도 저장</span>
          <span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
        </label>
        <div class="templateRequestDraft__note">
          저장을 끄면 요청 시점 스냅샷만 관리자에게 전달되고, 내 티어 리스트에는 별도로 남기지 않아요.
        </div>
      </div>
      <div class="modalCard__actions">
        <button class="btn btn--ghost" @click="closeTemplateRequestModal">취소</button>
        <button class="btn btn--save" :disabled="!canSubmitTemplateCreateRequest || isRequestingTemplate" @click="requestTemplate('create')">
          {{ isRequestingTemplate ? '요청중...' : '등록 요청 보내기' }}
        </button>
      </div>
    </div>
  </div>

  <div v-if="isTemplateUpdateModalOpen" class="modalOverlay" @click.self="closeTemplateUpdateModal">
    <div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateUpdateTitle">
      <div id="templateUpdateTitle" class="modalCard__title">템플릿 요청하기</div>
      <div class="modalCard__desc">
        {{ templateRequestTargetLabel }}에 직접 추가한 아이템을 포함해 달라고 관리자에게 요청합니다. 현재 티어표 제목과 설명은 그대로 가져와 두었어요.
      </div>
      <div class="modalCard__note">
        모두가 사용하는 기본 템플릿이니 개인적인 항목이 아닌 공통된 항목만 추가한 뒤 신청해주세요.
        예시: 제목 `템플릿 업데이트 요청`, 설명 `여름 이벤트 한정 캐릭터 추가`
      </div>
      <div class="templateRequestDraft">
        <label class="templateRequestDraft__field">
          <span class="templateRequestDraft__label">티어표 제목</span>
          <input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="120" placeholder="예: 템플릿 업데이트 요청" />
          <span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/120자</span>
        </label>
        <label class="templateRequestDraft__field">
          <span class="templateRequestDraft__label">티어표 설명</span>
          <textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="1000" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
          <span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/1000자</span>
        </label>
        <label class="toggleSwitch">
          <input v-model="templateRequestSaveToMyTierList" type="checkbox" />
          <span class="toggleSwitch__label">내 티어 리스트에도 저장</span>
          <span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
        </label>
        <div class="templateRequestDraft__note">
          저장을 끄면 관리자 확인용 요청 스냅샷만 남고, 현재 작업 중인 티어표는 따로 저장하지 않아요.
        </div>
      </div>
      <div class="modalCard__actions">
        <button class="btn btn--ghost" @click="closeTemplateUpdateModal">요청 취소</button>
        <button class="btn btn--save" :disabled="!canSubmitTemplateUpdateRequest || isRequestingTemplate" @click="requestTemplate('update')">
          {{ isRequestingTemplate ? '요청중...' : '예, 요청할게요' }}
        </button>
      </div>
    </div>
  </div>

  <div v-if="isDeleteModalOpen" class="modalOverlay" @click.self="closeDeleteModal">
    <div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="deleteTierListTitle">
      <div id="deleteTierListTitle" class="modalCard__title">티어표 삭제</div>
      <div class="modalCard__desc">
        "{{ title || gameName || '이 티어표' }}"를 삭제할까요? 삭제 후에는 복구할 수 없어요.
      </div>
      <div class="modalCard__actions">
        <button class="btn btn--ghost" @click="closeDeleteModal">취소</button>
        <button class="btn btn--danger" :disabled="isDeleting" @click="confirmDeleteTierList">
          {{ isDeleting ? '삭제중...' : '삭제하기' }}
        </button>
      </div>
    </div>
  </div>

  <div v-if="isGroupDeleteModalOpen" class="modalOverlay" @click.self="closeGroupDeleteModal">
    <div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="deleteGroupTitle">
      <div id="deleteGroupTitle" class="modalCard__title">티어 행 삭제</div>
      <div class="modalCard__desc">
        이 행을 삭제하면 현재 들어 있는 아이템은 모두 아래 아이템 영역으로 이동합니다. 삭제 후에도 아이템 자체는 유지돼요.
      </div>
      <div class="modalCard__actions">
        <button class="btn btn--ghost" @click="closeGroupDeleteModal">취소</button>
        <button class="btn btn--danger" @click="confirmRemoveGroup">행 삭제</button>
      </div>
    </div>
  </div>

  <div v-if="isColumnDeleteModalOpen" class="modalOverlay" @click.self="closeColumnDeleteModal">
    <div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="deleteColumnTitle">
      <div id="deleteColumnTitle" class="modalCard__title">티어 열 삭제</div>
      <div class="modalCard__desc">
        이 열을 삭제하면 현재 들어 있는 아이템은 모두 첫 번째 열로 이동합니다. 삭제 후에도 아이템 자체는 유지돼요.
      </div>
      <div class="modalCard__actions">
        <button class="btn btn--ghost" @click="closeColumnDeleteModal">취소</button>
        <button class="btn btn--danger" @click="confirmRemoveColumn">열 삭제</button>
      </div>
    </div>
  </div>

  <section class="layout" :style="{ '--thumb-size': `${iconSize}px` }">
    <div class="editorMain">
      <section class="head">
        <div class="editorMain__headCopy">
          <div class="editorMain__title">{{ gameName || gameId }}</div>
          <div class="editorMain__subtitle">
            <template v-if="canEdit">
              행/열 이름과 순서를 바꾸고 아이템을 드래그해서 배치할 수 있어요.
            </template>
            <template v-else>
              공개된 티어표를 보는 중입니다. 로그인한 작성자만 수정할 수 있어요.
            </template>
          </div>
          <div v-if="sourceTierListId" class="editorMain__sourceNote">
            <span>복사본</span>
            <button class="editorMain__sourceLink" type="button" @click="router.push(`/editor/${gameId}/${sourceTierListId}`)">{{ copiedFromLabel }}</button>
          </div>
        </div>
      </section>

      <div class="editorCanvas">
        <div ref="boardEl" class="board">
          <div v-if="canEdit && !isExporting" class="boardTools">
            <div class="boardTools__left">
              <button class="boardActionIcon" type="button" title="행 추가" aria-label="행 추가" @click="addGroup">
                <SvgIcon :src="addRowBelowIcon" :size="18" />
              </button>
              <button class="boardActionIcon" type="button" title="열 추가" aria-label="열 추가" @click="addColumn">
                <SvgIcon :src="addColumnRightIcon" :size="18" />
              </button>
            </div>
            <div class="boardTools__right">
              <span class="boardTools__label">아이콘 크기</span>
              <div class="sizePicker">
                <button
                  v-for="size in iconSizeOptions"
                  :key="size"
                  class="sizePicker__button"
                  :class="{ 'sizePicker__button--active': iconSize === size }"
                  @click="setIconSize(size)"
                >
                  {{ size }}
                </button>
              </div>
            </div>
          </div>
          <div ref="exportBoardEl" class="exportBoard" :class="{ 'exportBoard--active': isExporting }">
            <div v-if="isExporting" class="exportBoard__title">{{ effectiveTitle }}</div>
            <div v-if="isExporting && description" class="exportBoard__description">{{ description }}</div>
            <div v-if="columns.length > 1" class="boardColumnsHeader" :class="{ 'boardColumnsHeader--export': isExporting }">
              <div class="boardColumnsHeader__spacer" aria-hidden="true"></div>
              <div class="boardColumnsHeader__grid" :style="{ '--column-count': columns.length }">
                <div v-for="(column, columnIndex) in columns" :key="column.id" class="boardColumnsHeader__cell">
                  <template v-if="isExporting">
                    <div class="boardColumnsHeader__name">{{ column.name || '열 ' + (columnIndex + 1) }}</div>
                  </template>
                  <template v-else>
                    <div class="columnHeader">
                      <input v-model="column.name" class="columnName" maxlength="16" placeholder="열 이름" />
                      <button class="columnRemoveText" type="button" title="열 삭제" aria-label="열 삭제" :disabled="columns.length <= 1" @click="openColumnDeleteModal(columnIndex)">×</button>
                    </div>
                  </template>
                </div>
              </div>
            </div>
            <div ref="groupListEl" class="rows">
              <div v-for="g in groups" :key="g.id" class="row">
                <div class="row__label">
                  <template v-if="isExporting">
                    <div class="row__exportName">{{ g.name }}</div>
                  </template>
                  <template v-else>
                    <span class="grab" title="드래그로 순서 변경" data-group-handle>↕</span>
                    <input v-model="g.name" class="groupName" maxlength="16" :readonly="!canEdit" />
                    <button
                      v-if="canEdit"
                      class="rowRemoveText"
                      type="button"
                      title="행 삭제"
                      aria-label="행 삭제"
                      :disabled="groups.length <= 1"
                      @click="openGroupDeleteModal(g.id)"
                    >
                      ×
                    </button>
                  </template>
                </div>
                <div class="row__content" :style="{ '--column-count': columns.length }">
                  <div v-for="(column, columnIndex) in columns" :key="column.id" class="row__column">
                    <div
                      class="row__drop"
                      :data-list-type="'group'"
                      :data-group-id="g.id"
                      :data-column-index="columnIndex"
                      :ref="(el) => setGroupDropEl(g.id, columnIndex, el)"
                    >
                      <div v-if="!isExporting" class="row__empty" v-show="getGroupCellIds(g, columnIndex).length === 0">여기로 드래그해서 배치</div>
                      <div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="cell" :data-item-id="id">
                        <img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
                        <div v-if="showCharacterNames" class="itemNameOverlay">{{ itemsById[id]?.label || id }}</div>
                        <button
                          v-if="canEdit && !isExporting"
                          class="cellRemoveBtn"
                          type="button"
                          title="아이템 빼내기"
                          @pointerdown.stop
                          @click.stop="removeItemFromGroup(g.id, columnIndex, id)"
                        >
                          ×
                        </button>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
            <div v-if="isExporting" class="exportBoard__footer">
              <span>{{ effectiveAuthorName }}</span>
              <span>{{ formatExportDate(fallbackTimestamp) }}</span>
            </div>
          </div>

          <div
            v-if="canEdit"
            class="dropzone dropzone--board"
            :class="{ 'dropzone--active': isDragActive }"
            @dragenter.prevent="onDragEnter"
            @dragover.prevent="onDragEnter"
            @dragleave="onDragLeave"
            @drop.prevent="onDropFiles"
          >
            <div>
              <div class="dropzone__title">커스텀 이미지 추가</div>
              <div class="dropzone__desc">이곳으로 이미지를 드래그하거나 파일 선택으로 한 번에 추가할 수 있어요.</div>
            </div>
            <div class="dropzone__actions">
              <input ref="fileEl" type="file" accept="image/*" multiple class="hidden" @change="onFileChange" />
              <button class="btn btn--ghost dropzone__button" @click="openFile">파일 선택</button>
            </div>
          </div>
        </div>

        <div class="sidebar">
          <div class="sidebar__title">아이템</div>
          <div class="sidebar__hint">
            {{ canEdit ? '등록된 아이템 리스트입니다. 드래그해서 표에 넣을 수 있습니다.' : '공개 티어표는 보기 전용입니다.' }}
          </div>
          <div ref="poolEl" class="pool" data-list-type="pool">
            <div v-for="id in pool" :key="id" class="poolItem" :class="{ 'poolItem--readonly': !canEdit }" :data-item-id="id">
              <img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
              <div class="poolItem__label">{{ itemsById[id]?.label || id }}</div>
              <div v-if="!canEdit" class="poolItem__state">미배치</div>
            </div>
          </div>
        </div>
      </div>

    </div>

  </section>

  <Teleport :to="localRightRailTarget">
    <template v-if="globalRightRailOpen">
      <div class="editorSidebar__section">
        <div class="editorSidebar__label">Title</div>
        <input v-model="title" class="editorSidebar__input" maxlength="120" placeholder="Title Text" :readonly="!canEdit" />
        <div class="editorSidebar__hint">{{ title.length }}/120자</div>
        <div v-if="untitledWarning" class="editorSidebar__hint editorSidebar__hint--warn">{{ untitledWarning }}</div>
      </div>

      <div class="editorSidebar__section">
        <div class="editorSidebar__label">Desc</div>
        <textarea
          v-model="description"
          class="editorSidebar__textarea"
          placeholder="Description Text"
          maxlength="1000"
          :readonly="!canEdit"
        ></textarea>
        <div class="editorSidebar__hint">{{ description.length }}/1000자</div>
      </div>

      <div class="editorSidebar__section">
        <div class="editorSidebar__label">대표 썸네일</div>
        <input ref="thumbnailFileEl" type="file" accept="image/*" class="hidden" @change="onThumbnailChange" />
        <div
          class="editorSidebar__thumbFrame"
          :class="{ 'editorSidebar__thumbFrame--active': isThumbnailDragActive }"
          @dragenter.prevent="onThumbnailDragEnter"
          @dragover.prevent="onThumbnailDragEnter"
          @dragleave="onThumbnailDragLeave"
          @drop.prevent="onThumbnailDrop"
        >
          <img v-if="displayThumbnailUrl" class="editorSidebar__thumbImage" :src="displayThumbnailUrl" alt="썸네일 미리보기" />
          <div v-else class="editorSidebar__thumbEmpty">대표 썸네일</div>
          <div class="editorSidebar__thumbOverlay">드래그 또는 클릭으로 썸네일 추가</div>
        </div>
        <button v-if="canEdit" class="btn btn--ghost editorSidebar__button" @click="openThumbnailFile">파일 업로드</button>
        <div v-if="pendingThumbnailFile" class="editorSidebar__fileName">{{ pendingThumbnailFile.name }}</div>
      </div>

      <div v-if="canFavorite" class="editorSidebar__section">
        <button class="editorSidebar__favorite" :disabled="isFavoriteBusy" @click="toggleFavorite">
          <span>♡ 즐겨찾기</span>
          <span>{{ favoriteCount }}</span>
        </button>
      </div>

      <div v-if="canEdit && customItems.length" class="editorSidebar__section">
        <div class="editorSidebar__label">커스텀 이름 정리</div>
        <div class="customItemEditor customItemEditor--sidebar">
          <div class="customItemEditor__desc">
            아래에서 이름만 정리해두면 관리자 요청 시 그대로 전달됩니다.
          </div>
          <div class="customItemEditor__list">
            <label v-for="item in customItems" :key="item.id" class="customItemEditor__row">
              <img class="customItemEditor__thumb" :src="resolveItemSrc(item)" :alt="item.label" />
              <input
                class="customItemEditor__input"
                :value="item.label"
                maxlength="60"
                placeholder="아이템 이름"
                @input="updateCustomItemLabel(item.id, $event.target.value)"
              />
            </label>
          </div>
        </div>
      </div>

      <div class="editorSidebar__section editorSidebar__section--footer">
        <label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': !canEdit }">
          <input v-model="isPublic" type="checkbox" :disabled="!canEdit" />
          <span class="toggleSwitch__label">공개</span>
          <span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
        </label>
        <label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': !canEdit }">
          <input v-model="showCharacterNames" type="checkbox" :disabled="!canEdit" />
          <span class="toggleSwitch__label">캐릭터 이름 표시</span>
          <span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
        </label>
        <div class="editorSidebar__actionGrid">
          <button class="btn btn--ghost editorSidebar__button" @click="downloadImage">이미지 다운로드</button>
          <button v-if="canEdit" class="btn btn--save editorSidebar__button" :disabled="isSaving" @click="save">{{ isSaving ? '저장중...' : '저장' }}</button>
        </div>
        <div class="editorSidebar__utilityLinks">
          <button v-if="canEdit && !isNewTierList" class="editorSidebar__utilityLink editorSidebar__utilityLink--danger" @click="openDeleteModal">삭제하기</button>
          <button v-if="canDuplicate" class="editorSidebar__utilityLink" @click="duplicateCurrentTierList">복사해서 내 티어표로 가져오기</button>
          <button
            v-if="canRequestTemplateCreate"
            class="editorSidebar__utilityLink"
            :disabled="isRequestingTemplate"
            @click="openTemplateRequestModal"
          >
            템플릿 등록 요청
          </button>
          <button
            v-if="canRequestTemplateUpdate"
            class="editorSidebar__utilityLink"
            :disabled="isRequestingTemplate"
            @click="openTemplateUpdateModal"
          >
            {{ isRequestingTemplate ? '요청중...' : '템플릿 업데이트 요청' }}
          </button>
        </div>
      </div>
    </template>
  </Teleport>
  </template>
</template>

<style scoped>
.head {
  display: grid;
  gap: 8px;
  padding: 2px 2px 8px;
}
.editorMain {
  min-width: 0;
  display: grid;
  gap: 14px;
}
.editorCanvas {
  display: grid;
  grid-template-columns: minmax(0, clamp(680px, 58vw, 960px)) minmax(280px, 1fr);
  gap: 16px;
  align-items: start;
}
.editorMain__title {
  font-size: 28px;
  font-weight: 900;
  letter-spacing: -0.04em;
}
.editorMain__subtitle {
  color: var(--theme-text-soft);
  font-size: 13px;
  line-height: 1.5;
}
.editorMain__sourceNote {
  margin-top: 4px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--theme-text-soft);
}
.editorMain__sourceLink {
  border: 0;
  padding: 0;
  background: transparent;
  color: color-mix(in srgb, var(--theme-accent-bg) 78%, white);
  font: inherit;
  cursor: pointer;
}
.previewOnly {
  min-height: 100vh;
  padding: 20px;
  box-sizing: border-box;
  background:
    radial-gradient(circle at top, rgba(96, 165, 250, 0.14), transparent 38%),
    var(--theme-shell-bg);
}
.previewOnly__sheet {
  display: grid;
  gap: 16px;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
}
.previewOnly__title {
  font-size: 28px;
  font-weight: 900;
  letter-spacing: -0.03em;
}
.previewOnly__description {
  margin-top: -8px;
  font-size: 14px;
  line-height: 1.6;
  opacity: 0.76;
}
.previewOnly__columns {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 10px;
  margin-bottom: 10px;
}
.previewOnly__columnsGrid {
  display: grid;
  grid-template-columns: repeat(var(--column-count, 1), minmax(0, 1fr));
  gap: 10px;
}
.previewOnly__columnHeader {
  min-height: 20px;
  font-size: 12px;
  font-weight: 800;
  text-align: center;
  opacity: 0.72;
}
.previewOnly__rows {
  display: grid;
  gap: 10px;
}
.previewOnly__row {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 10px;
}
.previewOnly__dropGrid {
  display: grid;
  grid-template-columns: repeat(var(--column-count, 1), minmax(0, 1fr));
  gap: 10px;
}
.previewOnly__dropColumn {
  display: grid;
  gap: 8px;
}
.previewOnly__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);
}
.previewOnly__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;
}
.previewOnly__cell {
  display: inline-flex;
  position: relative;
}
.previewOnly__pool {
  display: grid;
  gap: 10px;
  padding-top: 8px;
}
.previewOnly__poolTitle {
  font-weight: 900;
  opacity: 0.82;
}
.previewOnly__poolGrid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.previewOnly__poolItem {
  display: inline-flex;
  position: relative;
}
.previewOnly__poolItem--inactive {
  opacity: 0.52;
  filter: grayscale(0.22) brightness(0.78);
}
.toggleSwitch {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
  cursor: pointer;
  user-select: none;
}
.toggleSwitch input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.toggleSwitch__track {
  position: relative;
  width: 42px;
  height: 24px;
  border-radius: 999px;
  background: var(--theme-surface-soft-3);
  border: 1px solid var(--theme-border-strong);
  transition: background 180ms ease, border-color 180ms ease;
  flex: 0 0 auto;
}
.toggleSwitch__thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  background: var(--theme-text-strong);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.24);
  transition: transform 180ms ease;
}
.toggleSwitch__label {
  font-weight: 800;
  color: var(--theme-text);
}
.toggleSwitch input:checked ~ .toggleSwitch__track {
  background: rgba(96, 165, 250, 0.34);
  border-color: rgba(96, 165, 250, 0.42);
}
.toggleSwitch input:checked ~ .toggleSwitch__track .toggleSwitch__thumb {
  transform: translateX(18px);
}
.toggleSwitch--disabled {
  opacity: 0.55;
  pointer-events: none;
}
.btn {
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid var(--theme-border-strong);
  background: var(--theme-surface-soft-2);
  color: var(--theme-text);
  cursor: pointer;
  font-weight: 700;
}
.btn:hover {
  background: var(--theme-surface-soft-3);
}
.btn--primary {
  background: rgba(110, 231, 183, 0.18);
}
.btn--primary:hover {
  background: rgba(110, 231, 183, 0.24);
}
.btn--download {
  justify-self: flex-start;
}
.btn--save {
  min-width: 112px;
  padding: 12px 18px;
  font-size: 15px;
  font-weight: 900;
  background: rgba(96, 165, 250, 0.22);
  border-color: rgba(96, 165, 250, 0.36);
}
.btn--save:hover {
  background: rgba(96, 165, 250, 0.3);
}
.btn--danger {
  background: rgba(239, 68, 68, 0.14);
  border-color: rgba(239, 68, 68, 0.28);
}
.btn--danger:hover {
  background: rgba(239, 68, 68, 0.22);
}
.btn--ghost {
  width: 100%;
  margin-top: 0;
}
.layout {
  display: block;
  min-width: 0;
}
.error {
  margin: 10px 0 14px;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid rgba(239, 68, 68, 0.3);
  background: rgba(239, 68, 68, 0.12);
}
.board {
  width: min(100%, 960px);
  border: 1px solid var(--theme-border);
  background: linear-gradient(180deg, color-mix(in srgb, var(--theme-card-bg) 94%, transparent), color-mix(in srgb, var(--theme-card-bg-hover) 88%, transparent));
  border-radius: 22px;
  padding: 20px;
  align-self: start;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.modalOverlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: 20px;
  background: color-mix(in srgb, var(--theme-body-bg) 76%, transparent);
  backdrop-filter: blur(4px);
}
.modalCard {
  width: min(100%, 420px);
  border-radius: 20px;
  padding: 24px;
  border: 1px solid var(--theme-border-strong);
  background: linear-gradient(180deg, color-mix(in srgb, var(--theme-main-bg) 98%, transparent), color-mix(in srgb, var(--theme-shell-bg) 98%, transparent));
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.38);
  display: grid;
  gap: 10px;
}
.modalCard__title {
  font-size: 22px;
  font-weight: 900;
  letter-spacing: -0.02em;
}
.modalCard__desc {
  line-height: 1.6;
  opacity: 0.82;
}
.modalCard__actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 8px;
  flex-wrap: wrap;
  gap: 8px;
}
.modalCard__actions .btn {
  width: auto;
  min-width: 120px;
}
.requestChecklist {
  display: grid;
  gap: 10px;
}
.requestChecklist__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid rgba(251, 191, 36, 0.22);
  background: rgba(251, 191, 36, 0.08);
  line-height: 1.5;
}
.requestChecklist__item--passed {
  border-color: rgba(52, 211, 153, 0.24);
  background: rgba(52, 211, 153, 0.1);
}
.requestChecklist__label {
  min-width: 0;
  overflow-wrap: anywhere;
}
.requestChecklist__icon {
  flex: 0 0 auto;
  min-width: 68px;
  font-size: 12px;
  font-weight: 900;
  opacity: 0.9;
  text-align: right;
  white-space: nowrap;
}
.requestChecklist__hint {
  font-size: 13px;
  line-height: 1.6;
  opacity: 0.78;
  white-space: pre-line;
}
.templateRequestDraft {
  display: grid;
  gap: 12px;
}
.templateRequestDraft__field {
  display: grid;
  gap: 6px;
}
.templateRequestDraft__label {
  font-size: 12px;
  color: var(--theme-text-soft);
}
.templateRequestDraft__hint {
  font-size: 12px;
  color: var(--theme-text-faint);
}
.templateRequestDraft__note {
  font-size: 12px;
  line-height: 1.6;
  color: var(--theme-text-soft);
}
.templateRequestDraft__input {
  width: 100%;
  padding: 14px 0;
  border: 0;
  border-bottom: 1px solid var(--theme-border-strong);
  background: transparent;
  color: var(--theme-text-strong);
  outline: none;
  font-size: 18px;
  line-height: 1.5;
  letter-spacing: -0.02em;
  resize: none;
}
.templateRequestDraft__input:focus {
  border-bottom-color: rgba(96, 165, 250, 0.9);
}
.templateRequestDraft__input::placeholder {
  color: var(--theme-text-faint);
}
.templateRequestDraft__textarea {
  min-height: 92px;
  resize: vertical;
}
.boardTools {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: flex-end;
  margin-bottom: 16px;
  flex-wrap: wrap;
  padding: 10px 12px;
  border-radius: 16px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
}
.boardTools__left,
.boardTools__right {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}
.boardTools__left {
  margin-right: auto;
}
.boardActionIcon {
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  border: 1px solid var(--theme-border-strong);
  background: var(--theme-surface-soft);
  color: var(--theme-text);
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
}
.boardActionIcon:hover {
  background: rgba(96, 165, 250, 0.12);
  border-color: rgba(96, 165, 250, 0.28);
  color: rgba(255, 255, 255, 0.98);
}
.boardColumnsHeader {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 10px;
  margin-bottom: 10px;
}
.boardColumnsHeader__grid {
  display: grid;
  grid-template-columns: repeat(var(--column-count, 1), minmax(0, 1fr));
  gap: 10px;
}
.boardColumnsHeader__cell {
  min-width: 0;
  position: relative;
}
.boardColumnsHeader__name {
  min-height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 12px;
  text-align: center;
  font-size: 12px;
  line-height: 1.2;
  font-weight: 800;
  opacity: 0.74;
}
.boardTools__label {
  font-size: 13px;
  opacity: 0.76;
  font-weight: 800;
}
.sizePicker {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.sizePicker__button {
  margin: 0;
  min-width: 48px;
  padding: 9px 10px;
  border-radius: 12px;
  border: 1px solid var(--theme-border-strong);
  background: var(--theme-surface-soft);
  color: var(--theme-text);
  cursor: pointer;
  font-weight: 800;
}
.sizePicker__button--active {
  background: rgba(96, 165, 250, 0.24);
  border-color: rgba(96, 165, 250, 0.38);
}
.exportBoard--active {
  display: grid;
  gap: 12px;
  width: 960px;
  max-width: none;
  box-sizing: border-box;
  padding: 44px 52px;
  border-radius: 28px;
  background:
    radial-gradient(circle at top, rgba(96, 165, 250, 0.14), transparent 38%),
    var(--theme-shell-bg);
}
.exportBoard__title {
  font-size: 28px;
  font-weight: 900;
  letter-spacing: -0.03em;
  text-align: left;
}
.exportBoard__description {
  margin-top: -4px;
  font-size: 14px;
  line-height: 1.5;
  opacity: 0.74;
  text-align: left;
}
.exportBoard__footer {
  margin-top: 12px;
  padding-top: 18px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  display: flex;
  justify-content: space-between;
  gap: 16px;
  font-size: 15px;
  opacity: 0.8;
}
.rows {
  display: grid;
  gap: 10px;
}
.row {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 10px;
  align-items: stretch;
}
.row__label {
  position: relative;
  border-radius: 16px;
  background: var(--theme-surface-soft-3);
  border: 1px solid var(--theme-border-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px 28px;
  font-weight: 900;
  overflow: hidden;
}
.row__content {
  display: grid;
  grid-template-columns: repeat(var(--column-count, 1), minmax(0, 1fr));
  gap: 10px;
}
.row__column {
  display: grid;
  gap: 8px;
  min-width: 0;
}
.columnHeader {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 38px;
  padding: 0 28px;
}
.columnName {
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--theme-border-strong);
  background: transparent;
  color: var(--theme-text);
  padding: 4px 0;
  text-align: center;
  font-size: 12px;
  line-height: 1.2;
  font-weight: 800;
  outline: none;
}
.columnName::placeholder {
  color: var(--theme-text-faint);
}
.columnRemoveText {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--theme-text-soft);
  font-size: 16px;
  line-height: 1;
  font-weight: 800;
  cursor: pointer;
}
.columnRemoveText:hover {
  color: var(--theme-text);
  background: var(--theme-surface-soft-2);
}
.columnRemoveText:disabled {
  opacity: 0.32;
  cursor: not-allowed;
}
.grab {
  position: absolute;
  top: 10px;
  left: 10px;
  cursor: grab;
  opacity: 0.72;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  border: 1px solid var(--theme-border);
  background: rgba(0, 0, 0, 0.16);
  font-size: 12px;
}
.groupName {
  width: 100%;
  border: 1px solid var(--theme-border-strong);
  background: var(--theme-pill-bg);
  color: var(--theme-text);
  border-radius: 10px;
  padding: 8px 10px;
  font-weight: 900;
  text-align: center;
  outline: none;
  min-width: 0;
}
.rowRemoveText {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--theme-text-soft);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  font-weight: 800;
}
.rowRemoveText:hover {
  color: var(--theme-text);
  background: var(--theme-surface-soft-2);
}
.rowRemoveText:disabled {
  opacity: 0.32;
  cursor: not-allowed;
}
.row__exportName {
  width: 100%;
  text-align: center;
  font-weight: 900;
  word-break: break-word;
}
.row__drop {
  border-radius: 16px;
  background: var(--theme-pill-bg);
  border: 1px solid rgba(255, 255, 255, 0.10);
  min-height: calc(var(--thumb-size, 80px) + 24px);
  padding: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-content: flex-start;
  overflow: hidden;
  position: relative;
}
.row__empty {
  opacity: 0.6;
  font-size: 13px;
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.cell {
  display: inline-flex;
  flex: 0 0 auto;
  position: relative;
}
.itemNameOverlay {
  position: absolute;
  inset: auto 0 0 0;
  min-height: 26px;
  padding: 18px 8px 6px;
  border-radius: 0 0 10px 10px;
  background: linear-gradient(180deg, rgba(7, 10, 18, 0), rgba(7, 10, 18, 0.92));
  color: rgba(255, 255, 255, 0.96);
  font-size: 11px;
  line-height: 1.2;
  font-weight: 800;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  pointer-events: none;
}
.cellRemoveBtn {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  border: 1px solid rgba(239, 68, 68, 0.32);
  background: rgba(11, 18, 32, 0.92);
  color: var(--theme-text);
  font-size: 16px;
  line-height: 1;
  font-weight: 900;
  cursor: pointer;
  z-index: 2;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.24);
}
.cellRemoveBtn:hover {
  background: rgba(239, 68, 68, 0.9);
}
.thumb {
  width: var(--thumb-size, 80px);
  height: var(--thumb-size, 80px);
  border-radius: 10px;
  border: 1px solid var(--theme-border-strong);
  background: var(--theme-surface-soft-2);
  object-fit: cover;
}
.sidebar {
  min-width: 0;
  border: 1px solid var(--theme-border);
  background: linear-gradient(180deg, color-mix(in srgb, var(--theme-card-bg) 94%, transparent), color-mix(in srgb, var(--theme-card-bg-hover) 88%, transparent));
  border-radius: 22px;
  padding: 14px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
  position: sticky;
  top: 14px;
}

.dropzone--board {
  margin-top: 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}

.dropzone__actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 0 0 auto;
}

.dropzone__button {
  min-width: 148px;
}
.editorSidebar__section {
  display: grid;
  gap: 10px;
  padding: 12px;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.06);
  background: rgba(255, 255, 255, 0.02);
}
.editorSidebar__label {
  font-size: 11px;
  font-weight: 800;
  color: var(--theme-text-faint);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
.editorSidebar__input,
.editorSidebar__textarea {
  width: 100%;
  border-radius: 14px;
  border: 1px solid var(--theme-border);
  background: var(--theme-surface-soft);
  color: var(--theme-text);
  padding: 11px 12px;
  outline: none;
  resize: vertical;
}
.editorSidebar__textarea {
  min-height: 92px;
}
.editorSidebar__hint {
  font-size: 12px;
  line-height: 1.5;
  color: var(--theme-text-soft);
  word-break: keep-all;
}
.editorSidebar__hint--warn {
  color: rgba(251, 191, 36, 0.92);
}
.editorSidebar__thumbFrame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--theme-border);
  background: var(--theme-thumb-fallback-bg);
}

.editorSidebar__thumbFrame--active {
  border-color: rgba(96, 165, 250, 0.8);
  box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.18);
}
.editorSidebar__thumbImage {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.editorSidebar__thumbEmpty {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  color: var(--theme-text-faint);
  font-size: 13px;
}

.editorSidebar__thumbOverlay {
  position: absolute;
  inset: auto 0 0 0;
  padding: 10px 12px;
  background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.72));
  color: rgba(255, 255, 255, 0.82);
  font-size: 12px;
  font-weight: 700;
}
.editorSidebar__button {
  width: 100%;
  margin-top: 0;
}
.editorSidebar__fileName {
  font-size: 12px;
  color: var(--theme-text-soft);
  word-break: break-word;
}
.editorSidebar__favorite {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 11px 12px;
  border-radius: 14px;
  border: 1px solid var(--theme-border);
  background: var(--theme-pill-bg);
  color: var(--theme-text);
  font-weight: 800;
  cursor: pointer;
}
.editorSidebar__section--footer {
  padding-top: 12px;
}
.editorSidebar__actionGrid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.editorSidebar__utilityLinks {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding-top: 4px;
}

.editorSidebar__utilityLink {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--theme-text-muted);
  font-size: 14px;
  cursor: pointer;
}

.editorSidebar__utilityLink:disabled {
  cursor: default;
  opacity: 0.5;
}

.editorSidebar__utilityLink--danger {
  color: rgba(248, 113, 113, 0.96);
}
.sidebar__title {
  font-weight: 900;
  margin-bottom: 8px;
  font-size: 18px;
  letter-spacing: -0.02em;
}
.sidebar__hint {
  opacity: 0.78;
  font-size: 13px;
  margin-bottom: 12px;
  line-height: 1.5;
  word-break: keep-all;
}
.customItemEditor {
  margin-top: 0;
  padding: 0;
  border-radius: 0;
  border: 0;
  background: transparent;
}
.customItemEditor--sidebar {
  display: grid;
  gap: 12px;
}
.customItemEditor__title {
  font-weight: 900;
}
.customItemEditor__desc {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.5;
  opacity: 0.72;
}
.customItemEditor__list {
  margin-top: 12px;
  display: grid;
  gap: 10px;
}
.customItemEditor__row {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  gap: 10px;
  align-items: center;
}
.customItemEditor__thumb {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  object-fit: cover;
  border: 1px solid var(--theme-border-strong);
}
.customItemEditor__input {
  width: 100%;
  min-width: 0;
  padding: 9px 10px;
  border-radius: 12px;
  border: 1px solid var(--theme-border-strong);
  background: var(--theme-pill-bg);
  color: var(--theme-text);
  outline: none;
  box-sizing: border-box;
}
.dropzone {
  margin-top: 12px;
  padding: 14px;
  border-radius: 16px;
  border: 1px dashed rgba(255, 255, 255, 0.18);
  background: var(--theme-surface-soft);
}
.dropzone--active {
  border-color: rgba(110, 231, 183, 0.6);
  background: rgba(110, 231, 183, 0.08);
}
.dropzone__title {
  font-weight: 900;
}
.dropzone__desc {
  margin-top: 6px;
  opacity: 0.74;
  font-size: 13px;
  line-height: 1.4;
}
.pool {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px;
  align-content: start;
}
.poolItem {
  min-width: 0;
  display: grid;
  grid-template-columns: 1fr;
  justify-items: center;
  align-content: start;
  gap: 8px;
  padding: 10px 8px;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  background: var(--theme-pill-bg);
}
.poolItem--readonly {
  opacity: 0.58;
  filter: grayscale(0.25) brightness(0.78);
}
.poolItem .thumb {
  width: 100%;
  max-width: var(--thumb-size, 80px);
  height: auto;
  aspect-ratio: 1 / 1;
}
.poolItem__label {
  width: 100%;
  min-width: 0;
  font-size: 11px;
  line-height: 1.35;
  font-weight: 800;
  text-align: center;
  opacity: 0.9;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.poolItem__state {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--theme-text-soft);
}
.hidden {
  display: none;
}
.ghost {
  opacity: 0.3;
}
.chosen {
  outline: 2px solid rgba(110, 231, 183, 0.5);
  border-radius: 14px;
}
@media (max-width: 980px) {
  .previewOnly__row {
    grid-template-columns: 140px 1fr;
  }
  .heroCard {
    grid-template-columns: 1fr;
  }
  .editorCanvas {
    grid-template-columns: 1fr;
  }
  .row__content {
    grid-template-columns: 1fr;
  }
  .row {
    grid-template-columns: 150px 1fr;
  }
  .sidebar {
    position: static;
  }
  .pool {
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 8px;
  }
  .editorSidebar__actionGrid {
    grid-template-columns: 1fr;
  }
  .editorSidebar__utilityLinks {
    flex-direction: column;
    align-items: flex-start;
  }
  .requestChecklist__item {
    grid-template-columns: 1fr;
  }
  .requestChecklist__icon {
    text-align: left;
  }
  .modalCard__actions .btn {
    width: 100%;
  }
}
@media (max-width: 720px) {
  .previewOnly {
    padding: 14px;
  }
  .previewOnly__columns,
  .previewOnly__row,
  .boardColumnsHeader,
  .row {
    grid-template-columns: 1fr;
  }
  .previewOnly__columnsSpacer,
  .boardColumnsHeader__spacer {
    display: none;
  }
  .previewOnly__dropGrid,
  .boardColumnsHeader__grid {
    grid-template-columns: 1fr;
  }
  .pool {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}
@media (max-width: 560px) {
  .pool {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
</style>
