<script setup>
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
import { api } from '../lib/api'
import { toApiUrl } from '../lib/runtime'
import { useAuthStore } from '../stores/auth'

const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const gameId = computed(() => route.params.gameId)
const tierListId = computed(() => route.params.tierListId)
const gameName = ref('')

const groups = ref([
  { id: 'gS', name: 'S', itemIds: [] },
  { id: 'gA', name: 'A', itemIds: [] },
  { id: 'gB', name: 'B', itemIds: [] },
  { id: 'gC', name: 'C', itemIds: [] },
  { id: 'gD', name: 'D', itemIds: [] },
])

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

const title = ref('')
const description = ref('')
const isPublic = ref(true)
const error = ref('')
const isSaving = ref(false)
const isExporting = ref(false)
const isSaveModalOpen = ref(false)
const ownerId = ref('')
const authorName = ref('')
const authorAccountName = ref('')
const updatedAt = ref(0)
const isDragActive = ref(false)
const iconSize = ref(80)

const boardEl = ref(null)
const exportBoardEl = ref(null)
const groupListEl = ref(null)
const poolEl = ref(null)
const groupDropEls = ref({})
const fileEl = 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 untitledWarning = computed(
  () =>
    canEdit.value &&
    !hasCustomTitle.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 setGroupDropEl(groupId, el) {
  if (!el) {
    delete groupDropEls.value[groupId]
    return
  }
  groupDropEls.value[groupId] = el
}

function getListByContainer(containerEl) {
  if (!containerEl) return { type: null, groupId: null }
  const t = containerEl.getAttribute('data-list-type')
  if (t === 'pool') return { type: 'pool', groupId: null }
  if (t === 'group') return { type: 'group', groupId: containerEl.getAttribute('data-group-id') }
  return { type: null, groupId: 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
  if (meta.type === 'group') {
    const g = groups.value.find((x) => x.id === meta.groupId)
    if (g) g.itemIds = ids
  }
}

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(([gid, 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() {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  const index = groups.value.length
  if (index < alphabet.length) return alphabet[index]
  return `Tier ${index + 1}`
}

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

async function removeGroup(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)
  delete groupDropEls.value[groupId]
  await syncSortables()
}

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: file.name || 'custom', origin: 'custom', pendingFile: file },
  }
  pool.value = [id, ...pool.value]
}

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

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', 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) => ({
      ...group,
      itemIds: group.itemIds.map((currentId) => (currentId === item.id ? uploaded.id : currentId)),
    }))
  }
}

function buildPayload(existingId) {
  const finalTitle = effectiveTitle.value
  return {
    id: existingId || undefined,
    gameId: gameId.value,
    title: finalTitle,
    description: (description.value || '').trim(),
    isPublic: !!isPublic.value,
    groups: groups.value.map((g) => ({ id: g.id, name: g.name, itemIds: g.itemIds })),
    pool: Object.values(itemsById.value),
  }
}

async function save() {
  error.value = ''
  isSaving.value = true
  try {
    await uploadPendingCustomItems()
    const payload = buildPayload(tierListId.value && tierListId.value !== 'new' ? tierListId.value : null)
    const res = await api.saveTierList(payload)
    if (tierListId.value === 'new') history.replaceState({}, '', `/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
    isSaveModalOpen.value = true
  } catch (e) {
    error.value = '저장 실패: 로그인 상태인지 확인해주세요.'
  } finally {
    isSaving.value = false
  }
}

function closeSaveModal() {
  isSaveModalOpen.value = false
}

async function removeTierList() {
  if (!canEdit.value || isNewTierList.value) return
  error.value = ''
  try {
    const ok = window.confirm(`"${title.value || gameName.value || '이 티어표'}"를 삭제할까요?`)
    if (!ok) return
    await api.deleteTierList(tierListId.value)
    router.push(gameId.value === 'freeform' ? '/me' : `/games/${gameId.value}`)
  } catch (e) {
    error.value = '티어표 삭제에 실패했어요.'
  }
}

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
        description.value = t.description || ''
        isPublic.value = !!t.isPublic
        authorName.value = t.authorName || ''
        authorAccountName.value = t.authorAccountName || ''
        updatedAt.value = Number(t.updatedAt || 0)
        groups.value = t.groups
        const map = {}
        ;(t.pool || []).forEach((it) => (map[it.id] = it))
        itemsById.value = map
        const grouped = new Set()
        groups.value.forEach((g) => g.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(() => {
  destroySortables()
})
</script>

<template>
  <section class="head">
    <div class="head__meta">
      <div class="kicker">{{ gameName || gameId }}</div>
      <input v-model="title" class="titleInput" placeholder="티어표 이름을 입력하세요" :readonly="!canEdit" />
      <div v-if="untitledWarning" class="titleNotice">{{ untitledWarning }}</div>
      <input
        v-model="description"
        class="descInput"
        placeholder="설명(선택): 이 티어표의 기준/룰"
        :readonly="!canEdit"
      />
      <div class="hint">
        <template v-if="canEdit">
          그룹 이름/순서 변경과 아이템 드래그&드롭이 가능합니다. 저장하려면 <b>저장</b>을 누르세요.
        </template>
        <template v-else>
          공개된 티어표를 보는 중입니다. 로그인한 작성자만 수정할 수 있어요.
        </template>
      </div>
    </div>
    <div class="actions">
      <div class="actions__left">
        <button class="btn btn--download" @click="downloadImage">이미지 다운로드</button>
        <button v-if="canEdit && !isNewTierList" class="btn btn--danger" @click="removeTierList">삭제</button>
      </div>
      <div class="actions__right">
        <label class="toggle" :class="{ 'toggle--disabled': !canEdit }">
        <input v-model="isPublic" type="checkbox" :disabled="!canEdit" />
          <span>{{ isPublic ? '공개 ON' : '공개 OFF' }}</span>
        </label>
        <button v-if="canEdit" class="btn btn--save" :disabled="isSaving" @click="save">{{ isSaving ? '저장중...' : '저장' }}</button>
      </div>
    </div>
  </section>

  <div v-if="error" class="error">{{ error }}</div>

  <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>

  <section class="layout" :style="{ '--thumb-size': `${iconSize}px` }">
    <div ref="boardEl" class="board">
      <div v-if="canEdit && !isExporting" class="boardTools">
        <div class="boardTools__left">
          <button class="btn btn--ghost" @click="addGroup">티어 추가</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 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" :readonly="!canEdit" />
              <button v-if="canEdit" class="rowRemoveBtn" :disabled="groups.length <= 1" @click="removeGroup(g.id)">삭제</button>
            </template>
          </div>
          <div
            class="row__drop"
            :data-list-type="'group'"
            :data-group-id="g.id"
            :ref="(el) => setGroupDropEl(g.id, el)"
          >
            <div v-if="!isExporting" class="row__empty" v-show="g.itemIds.length === 0">여기로 드래그해서 배치</div>
            <div v-for="id in g.itemIds" :key="id" class="cell" :data-item-id="id">
              <img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
            </div>
          </div>
        </div>
      </div>
        <div v-if="isExporting" class="exportBoard__footer">
          <span>{{ effectiveAuthorName }}</span>
          <span>{{ formatExportDate(fallbackTimestamp) }}</span>
        </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" :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>
      </div>
      <div
        v-if="canEdit"
        class="dropzone"
        :class="{ 'dropzone--active': isDragActive }"
        @dragenter.prevent="onDragEnter"
        @dragover.prevent="onDragEnter"
        @dragleave="onDragLeave"
        @drop.prevent="onDropFiles"
      >
        <div class="dropzone__title">커스텀 이미지 추가</div>
        <div class="dropzone__desc">여러 이미지를 한 번에 드래그하거나 파일 선택으로 추가할 수 있어요.</div>
      </div>
      <input ref="fileEl" type="file" accept="image/*" multiple class="hidden" @change="onFileChange" />
      <button v-if="canEdit" class="btn btn--ghost" @click="openFile">파일 선택</button>
    </div>
  </section>
</template>

<style scoped>
.head {
  display: grid;
  gap: 14px;
  padding: 6px 2px 14px;
}
.head__meta {
  display: grid;
  gap: 10px;
}
.kicker {
  font-size: 12px;
  opacity: 0.7;
}
.titleInput {
  width: min(100%, 920px);
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.02em;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
  color: rgba(255, 255, 255, 0.92);
  outline: none;
}
.descInput {
  width: min(100%, 920px);
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(0, 0, 0, 0.18);
  color: rgba(255, 255, 255, 0.92);
  outline: none;
  box-sizing: border-box;
}
.hint {
  opacity: 0.78;
  font-size: 13px;
}
.titleNotice {
  font-size: 13px;
  line-height: 1.5;
  color: rgba(251, 191, 36, 0.94);
}
.actions {
  display: flex;
  gap: 14px;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
}
.actions__left,
.actions__right {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}
.toggle {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  padding: 8px 10px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(0, 0, 0, 0.12);
  font-weight: 800;
  cursor: pointer;
  user-select: none;
}
.toggle input {
  width: 16px;
  height: 16px;
}
.toggle--disabled {
  opacity: 0.55;
  pointer-events: none;
}
.btn {
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.92);
  cursor: pointer;
  font-weight: 700;
}
.btn:hover {
  background: rgba(255, 255, 255, 0.08);
}
.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: 10px;
}
.layout {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: 14px;
  align-items: start;
}
.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 {
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
  border-radius: 16px;
  padding: 20px;
  align-self: start;
}
.modalOverlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(4, 8, 16, 0.68);
  backdrop-filter: blur(4px);
}
.modalCard {
  width: min(100%, 420px);
  border-radius: 20px;
  padding: 24px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: linear-gradient(180deg, rgba(17, 24, 39, 0.96), rgba(11, 18, 32, 0.96));
  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;
}
.boardTools {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: flex-end;
  margin-bottom: 14px;
  flex-wrap: wrap;
}
.boardTools__left,
.boardTools__right {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}
.boardTools__left {
  margin-right: auto;
}
.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: 8px 10px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.05);
  color: rgba(255, 255, 255, 0.92);
  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%),
    rgba(11, 18, 32, 0.98);
}
.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: 180px 1fr;
  gap: 10px;
  align-items: stretch;
}
.row__label {
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: center;
  padding: 10px 8px;
  font-weight: 900;
  overflow: hidden;
}
.grab {
  cursor: grab;
  opacity: 0.85;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.16);
  flex: 0 0 auto;
}
.groupName {
  width: 100%;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.12);
  color: rgba(255, 255, 255, 0.92);
  border-radius: 10px;
  padding: 6px 8px;
  font-weight: 900;
  text-align: center;
  outline: none;
  min-width: 0;
}
.row__exportName {
  width: 100%;
  text-align: center;
  font-weight: 900;
  word-break: break-word;
}
.rowRemoveBtn {
  padding: 6px 10px;
  border-radius: 10px;
  border: 1px solid rgba(239, 68, 68, 0.28);
  background: rgba(239, 68, 68, 0.12);
  color: rgba(255, 255, 255, 0.92);
  cursor: pointer;
  font-weight: 800;
  flex: 0 0 auto;
}
.rowRemoveBtn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.row__drop {
  border-radius: 14px;
  background: rgba(0, 0, 0, 0.18);
  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;
}
.thumb {
  width: var(--thumb-size, 80px);
  height: var(--thumb-size, 80px);
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.06);
  object-fit: cover;
}
.sidebar {
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
  border-radius: 16px;
  padding: 12px;
}
.sidebar__title {
  font-weight: 900;
  margin-bottom: 6px;
}
.sidebar__hint {
  opacity: 0.78;
  font-size: 13px;
  margin-bottom: 10px;
}
.dropzone {
  margin-top: 12px;
  padding: 14px;
  border-radius: 16px;
  border: 1px dashed rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.03);
  text-align: center;
}
.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;
  gap: 10px;
}
.poolItem {
  display: grid;
  grid-template-columns: var(--thumb-size, 80px) 1fr;
  gap: 10px;
  align-items: center;
  padding: 10px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  background: rgba(0, 0, 0, 0.16);
}
.poolItem__label {
  font-weight: 800;
  opacity: 0.9;
}
.hidden {
  display: none;
}
.ghost {
  opacity: 0.3;
}
.chosen {
  outline: 2px solid rgba(110, 231, 183, 0.5);
  border-radius: 14px;
}
@media (max-width: 980px) {
  .layout {
    grid-template-columns: 1fr;
  }
  .actions {
    justify-content: stretch;
  }
  .actions__left,
  .actions__right {
    width: 100%;
  }
  .actions__right {
    justify-content: flex-end;
  }
  .row {
    grid-template-columns: 150px 1fr;
  }
}
</style>
