<script setup lang="ts">
import type { CategoryNode } from '~/components/index/CategoryTreeNode.vue'
import type { CategoryFormMode } from '~/components/index/CategoryFormModal.vue'
import type { CardDetail } from '~/components/index/CardDetailModal.vue'
import { request } from '~/utils/http/factory'
import { cardTypeRegistry, type CardType, type CardItemLike } from '~/config/cardTypes'

definePageMeta({
  layout: 'home',
})

const { $toast } = useNuxtApp()
const { isFavorited, toggle: toggleFavorite, syncFromCards } = useFavorite()
const route = useRoute()
const isMobile = useMediaQuery('(max-width: 767.98px)')
const drawerOpen = ref(false)
const sidebarMode = computed<'rail' | 'drawer'>(() => isMobile.value ? 'drawer' : 'rail')

// confirmation dialog
const confirmShow = ref(false)
const confirmMsg = ref('')
let confirmAction: (() => void) | null = null

function showConfirm(msg: string, action: () => void) {
  confirmMsg.value = msg
  confirmAction = action
  confirmShow.value = true
}

function onConfirm() {
  confirmAction?.()
  confirmShow.value = false
}

// ── Server response types ──

interface ServerCategory {
  id: string
  name: string
  slug: string
  image: string | null
  parentId: string | null
  sortOrder: number
  count: number
  createdAt: Date
  updatedAt: Date
  children: ServerCategory[]
}

interface ServerCard {
  id: number
  type: CardType
  title: string
  description: string | null
  aspectRatio: number | null
  categoryId: string | null
  createdAt: Date
  updatedAt: Date
  images: { id: number; url: string; sortOrder: number }[]
  tags: { id: number; name: string; slug: string }[]
}

interface ServerTag {
  id: number
  name: string
  slug: string
}

// ── Frontend card item (mapped from server) ──

interface CardItem extends CardItemLike {
  id: number
}

function mapCard(server: ServerCard): CardItem {
  const imgUrls = server.images.map((img) => img.url)
  return {
    id: server.id,
    type: server.type,
    image: imgUrls[0],
    images: imgUrls.length > 1 ? imgUrls : undefined,
    title: server.title,
    description: server.description ?? undefined,
    tags: server.tags.length > 0 ? server.tags.map((t) => t.name) : undefined,
    aspectRatio: server.aspectRatio ?? 0.75,
    categoryId: server.categoryId,
    createdAt: server.createdAt ? new Date(server.createdAt).toISOString() : undefined,
  }
}

function mapCategoryTree(server: ServerCategory[]): CategoryNode[] {
  function map(node: ServerCategory): CategoryNode {
    return {
      id: node.id,
      name: node.name,
      image: node.image ?? undefined,
      count: node.count,
      children: node.children.length > 0 ? node.children.map(map) : undefined,
    }
  }
  return server.map(map)
}

// ── Sidebar state ──

const activeToolKey = ref<string>('home')
const activeCategoryId = ref<string>('all')

const categories = ref<CategoryNode[]>([])
const categoriesLoading = ref(true)

async function fetchCategories() {
  categoriesLoading.value = true
  try {
    const raw = await request<{ code: number; data: ServerCategory[] }>('/api/categories')
    const tree = mapCategoryTree(raw.data)
    // Prepend virtual "未分类" node for cards without a category
    const uncategorized: CategoryNode = {
      id: '__uncategorized__',
      name: '未分类',
    }
    categories.value = [uncategorized, ...tree]
  } catch {
    $toast.error('分类加载失败')
  } finally {
    categoriesLoading.value = false
  }
}

const activeCategoryName = computed(() => {
  function find(nodes: CategoryNode[]): CategoryNode | null {
    for (const n of nodes) {
      if (n.id === activeCategoryId.value) return n
      if (n.children) {
        const r = find(n.children)
        if (r) return r
      }
    }
    return null
  }
  if (activeCategoryId.value === '__uncategorized__') return '未分类'
  return find(categories.value)?.name ?? '全部卡片'
})

// ── Tool selection ──

const showTagsModal = ref(false)
const tagList = ref<ServerTag[]>([])
const tagSearch = ref('')
const tagsLoading = ref(false)

async function fetchTags() {
  tagsLoading.value = true
  try {
    const raw = await request<{ code: number; data: { list: ServerTag[] } }>('/api/tags')
    tagList.value = raw.data.list
  } catch {
    $toast.error('标签加载失败')
  } finally {
    tagsLoading.value = false
  }
}

const filteredTagList = computed(() => {
  const q = tagSearch.value.trim().toLowerCase()
  if (!q) return tagList.value
  return tagList.value.filter(
    (t) => t.name.toLowerCase().includes(q) || t.slug.toLowerCase().includes(q),
  )
})

function onSelectTool(key: string) {
  activeToolKey.value = key
  switch (key) {
    case 'home':
      activeCategoryId.value = 'all'
      break
    case 'collect':
      activeCategoryId.value = 'all'
      break
    case 'tags':
      showTagsModal.value = true
      fetchTags()
      return
    case 'search':
      // Focus the TopNav search input
      const input = document.querySelector<HTMLInputElement>('.search-input')
      input?.focus()
      break
    case 'highlights':
      $toast.info('高亮即将上线')
      break
    case 'archive':
      $toast.info('归档即将上线')
      break
  }
}

function onSelectTag(_tagId: number) {
  // Future: filter cards by tag
  $toast.info('标签筛选即将上线')
}

// ── Category CRUD ──

// Modal state
const catFormVisible = ref(false)
const catFormMode = ref<CategoryFormMode>('create')
const catFormNode = ref<CategoryNode | null>(null)
const catFormParent = ref<CategoryNode | null>(null)

function openCatForm(mode: CategoryFormMode, node: CategoryNode | null, parent?: CategoryNode | null) {
  catFormMode.value = mode
  catFormNode.value = node
  catFormParent.value = parent ?? null
  catFormVisible.value = true
}

async function onSelectCategory(id: string) {
  if (activeCategoryId.value === id) {
    activeCategoryId.value = 'all'
  } else {
    activeCategoryId.value = id
  }
  activeToolKey.value = 'home'
  if (drawerOpen.value) drawerOpen.value = false
}

function onAddCategory(parentId: string | null) {
  const parent = parentId ? findNode(categories.value, parentId) : null
  openCatForm('create', null, parent)
}

function findNode(nodes: CategoryNode[], id: string): CategoryNode | null {
  for (const n of nodes) {
    if (n.id === id) return n
    if (n.children) {
      const r = findNode(n.children, id)
      if (r) return r
    }
  }
  return null
}

function onRenameCategory(id: string) {
  if (id === 'all' || id === '__uncategorized__') return
  const node = findNode(categories.value, id)
  if (!node) return
  openCatForm('rename', node)
}

function onDeleteCategory(id: string) {
  if (id === 'all' || id === '__uncategorized__') return
  const node = findNode(categories.value, id)
  if (!node) return
  openCatForm('delete', node)
}

function onMoveCategory(id: string) {
  if (id === 'all' || id === '__uncategorized__') return
  const node = findNode(categories.value, id)
  if (!node) return
  openCatForm('move', node)
}

function onChangeCover(id: string) {
  if (id === 'all' || id === '__uncategorized__') return
  const node = findNode(categories.value, id)
  if (!node) return
  openCatForm('cover', node)
}

async function onCatFormSubmit(data: {
  mode: CategoryFormMode
  nodeId: string | null
  name?: string
  slug?: string
  image?: string
  targetParentId?: string | null
}) {
  try {
    switch (data.mode) {
      case 'create': {
        await request('/api/categories', {
          method: 'POST',
          body: {
            name: data.name!,
            slug: data.slug!,
            parentId: data.targetParentId || null,
          },
        })
        $toast.success(`已创建分类「${data.name}」`)
        break
      }
      case 'rename': {
        await request(`/api/categories/${data.nodeId}`, {
          method: 'PUT',
          body: {
            name: data.name!,
            ...(data.slug ? { slug: data.slug } : {}),
          },
        })
        $toast.success('已重命名')
        break
      }
      case 'cover': {
        await request(`/api/categories/${data.nodeId}`, {
          method: 'PUT',
          body: { image: data.image! },
        })
        $toast.success('封面已更新')
        break
      }
      case 'delete': {
        // Delete children first (bottom-up)
        const node = data.nodeId ? findNode(categories.value, data.nodeId) : null
        if (node?.children) {
          for (const child of node.children) {
            await request(`/api/categories/${child.id}`, { method: 'DELETE' })
          }
        }
        await request(`/api/categories/${data.nodeId}`, { method: 'DELETE' })
        if (activeCategoryId.value === data.nodeId) activeCategoryId.value = 'all'
        $toast.success('已删除分类')
        break
      }
      case 'move': {
        await request(`/api/categories/${data.nodeId}`, {
          method: 'PUT',
          body: { parentId: data.targetParentId || null },
        })
        $toast.success('已移动分类')
        break
      }
    }
    catFormVisible.value = false
    await fetchCategories()
  } catch (e: any) {
    $toast.error(e.data.statusMessage || e.message || e.toString() || '操作失败')
  }
}

// ── Card modals ──

const showDetailModal = ref(false)
const detailCard = ref<CardDetail | null>(null)
const showCardFormModal = ref(false)
const cardFormMode = ref<'create' | 'edit'>('create')
const cardFormEditData = ref<CardDetail | null>(null)

function onCardClick(card: CardItem) {
  detailCard.value = {
    id: card.id,
    type: card.type,
    image: card.image,
    images: card.images,
    title: card.title,
    description: card.description,
    tags: card.tags,
    aspectRatio: card.aspectRatio,
    categoryId: card.categoryId,
    createdAt: card.createdAt,
  }
  showDetailModal.value = true
}

function onCardEdit(card: CardDetail) {
  cardFormMode.value = 'edit'
  cardFormEditData.value = card
  showCardFormModal.value = true
  showDetailModal.value = false
}

async function onCardDelete(id: number) {
  showConfirm('确定删除这张卡片吗？', async () => {
    try {
      await request(`/api/cards/${id}`, { method: 'DELETE' })
      $toast.success('已删除卡片')
      showDetailModal.value = false
      // Refresh category counts
      fetchCategories()
      // Reload cards
      page.value = 1
      hasMore.value = true
      allItems.value = []
      columns.value = Array.from({ length: columnCount.value }, () => [])
      columnHeights.value = new Array(columnCount.value).fill(0)
      initLoading.value = true
      loadMore()
    } catch {
      $toast.error('删除失败')
    }
  })
}

function onNewCard() {
  cardFormMode.value = 'create'
  cardFormEditData.value = null
  showCardFormModal.value = true
}

function onCardSaved() {
  $toast.success(cardFormMode.value === 'create' ? '卡片已创建' : '卡片已更新')
  fetchCategories()
  resetAndReload()
}

// ── Drag & Drop to category ──

let dragCardId: number | null = null

function onCardDragStart(e: DragEvent, item: CardItem) {
  dragCardId = item.id
  if (e.dataTransfer) {
    e.dataTransfer.effectAllowed = 'move'
    e.dataTransfer.setData('application/x-card-id', String(item.id))
  }
}

async function onCardDropToCategory(categoryId: string | null, categoryName: string) {
  if (dragCardId === null) return
  try {
    await request(`/api/cards/${dragCardId}`, {
      method: 'PUT',
      body: { categoryId },
    })
    $toast.success(`已移至「${categoryName}」`)
    fetchCategories()
    resetAndReload()
  } catch {
    $toast.error('移动失败')
  } finally {
    dragCardId = null
  }
}

// ── Right panel waterfall ──

const sentinel = ref<HTMLElement | null>(null)
const mainRef = ref<HTMLElement | null>(null)
const allItems = ref<CardItem[]>([])
const page = ref(1)
const hasMore = ref(true)
const loading = ref(false)
const initLoading = ref(true)

const columnCount = ref(1)
const columns = ref<CardItem[][]>([[]])
const columnHeights = ref<number[]>([0])

function containerWidth(): number {
  return mainRef.value?.clientWidth ?? 800
}

function getColumnWidth(): number {
  const padding = 48
  const w = containerWidth() - padding
  const gap = 12
  return (w - gap * (columnCount.value - 1)) / columnCount.value
}

function estimateCardHeight(item: CardItem): number {
  const colWidth = getColumnWidth()
  if (item.type === 'text') {
    const textWidth = colWidth - 48
    const charsPerLine = Math.max(1, Math.floor(textWidth / 13.5))
    const charCount = (item.description || '').length
    const lines = Math.ceil(charCount / charsPerLine)
    return 109 + lines * 24
  }
  return colWidth / item.aspectRatio
}

function addToShortestColumn(item: CardItem) {
  let minIdx = 0
  for (let i = 1; i < columnCount.value; i++) {
    if (columnHeights.value[i]! < columnHeights.value[minIdx]!) minIdx = i
  }
  columns.value[minIdx]!.push(item)
  columnHeights.value[minIdx]! += estimateCardHeight(item)
}

function distributeAll() {
  const n = columnCount.value
  columns.value = Array.from({ length: n }, () => [])
  columnHeights.value = new Array(n).fill(0)
  for (const item of allItems.value) addToShortestColumn(item)
}

function decideColumnCount(): number {
  if (isMobile.value) return 1
  const w = containerWidth()
  if (w < 900) return 2
  if (w < 1200) return 3
  return 4
}

let resizeTimer: ReturnType<typeof setTimeout> | null = null
function onContainerResize() {
  if (resizeTimer) clearTimeout(resizeTimer)
  resizeTimer = setTimeout(() => {
    const n = decideColumnCount()
    if (n !== columnCount.value) {
      columnCount.value = n
      distributeAll()
    }
  }, 120)
}

function removeCardFromList(cardId: number) {
  allItems.value = allItems.value.filter((item) => item.id !== cardId)
  distributeAll()
}

async function handleToggleFavorite(cardId: number) {
  const wasFavorited = isFavorited(cardId)
  const result = await toggleFavorite(cardId)
  // When unfavoriting on the collect page, remove the card from the list immediately
  if (wasFavorited && !result && activeToolKey.value === 'collect') {
    removeCardFromList(cardId)
  }
}

function onDetailFavoriteToggled(cardId: number, favorited: boolean) {
  // When unfavoriting from the detail modal on the collect page, remove the card
  if (!favorited && activeToolKey.value === 'collect') {
    removeCardFromList(cardId)
  }
}

async function loadMore() {
  if (loading.value || !hasMore.value) return
  loading.value = true
  try {
    const query: Record<string, string | number> = {
      page: page.value,
      pageSize: 12,
    }
    if (activeCategoryId.value !== 'all') {
      query.categoryId = activeCategoryId.value
    }
    // favorites mode
    if (activeToolKey.value === 'collect') {
      query.favorited = 1
    }
    // '__uncategorized__' will be passed as-is to filter null categories
    const q = (route.query.q as string)?.trim()
    if (q) query.q = q
    const raw = await $fetch<{
      code: number
      data: {
        items: ServerCard[]
        hasMore: boolean
        total: number
      }
    }>('/api/cards', { query })

    const result = raw.data
    // Sync favorite status from server
    syncFromCards(result.items as Array<{ id: number; isFavorited?: boolean }>)
    const mapped = result.items.map(mapCard)
    for (const item of mapped) {
      allItems.value.push(item)
      addToShortestColumn(item)
    }
    hasMore.value = result.hasMore
    page.value++
  } catch {
    $toast.error('加载失败，请稍后重试')
  } finally {
    loading.value = false
    initLoading.value = false
  }
}

// Reset when category switches
watch(activeCategoryId, () => {
  resetAndReload()
})

// Reset when search query changes
watch(() => route.query.q, () => {
  resetAndReload()
})

// Reset when tool changes (e.g. collect <-> home)
watch(activeToolKey, () => {
  resetAndReload()
})

function resetAndReload() {
  page.value = 1
  hasMore.value = true
  allItems.value = []
  columns.value = Array.from({ length: columnCount.value }, () => [])
  columnHeights.value = new Array(columnCount.value).fill(0)
  initLoading.value = true
  loadMore()
}

watch(isMobile, () => {
  const n = decideColumnCount()
  if (n !== columnCount.value) {
    columnCount.value = n
    distributeAll()
  }
})

let observer: IntersectionObserver | null = null
let resizeObserver: ResizeObserver | null = null

watch(sentinel, (el) => {
  observer?.disconnect()
  if (!el) return
  observer = new IntersectionObserver(
    ([entry]) => {
      if (entry!.isIntersecting) loadMore()
    },
    { rootMargin: '320px' },
  )
  observer.observe(el)
})

onMounted(() => {
  if (mainRef.value) {
    columnCount.value = decideColumnCount()
    distributeAll()
    resizeObserver = new ResizeObserver(onContainerResize)
    resizeObserver.observe(mainRef.value)
  }
  fetchCategories()
  resetAndReload()
})

onUnmounted(() => {
  observer?.disconnect()
  resizeObserver?.disconnect()
  if (resizeTimer) clearTimeout(resizeTimer)
})
</script>

<template>
  <div class="home-layout">
    <IndexLeftSidebar
      v-if="sidebarMode === 'rail'"
      class="home-sidebar"
      :mode="sidebarMode"
      :categories="categories"
      :active-tool-key="activeToolKey"
      :active-category-id="activeCategoryId"
      @select-tool="onSelectTool"
      @select-category="onSelectCategory"
      @add-category="onAddCategory"
      @rename-category="onRenameCategory"
      @delete-category="onDeleteCategory"
      @move-category="onMoveCategory"
      @change-cover="onChangeCover"
      @drop-card="onCardDropToCategory"
    />

    <IndexLeftSidebar
      v-else
      :mode="sidebarMode"
      :drawer-open="drawerOpen"
      :categories="categories"
      :active-tool-key="activeToolKey"
      :active-category-id="activeCategoryId"
      @update:drawer-open="(v: boolean) => drawerOpen = v"
      @select-tool="onSelectTool"
      @select-category="onSelectCategory"
      @add-category="onAddCategory"
      @rename-category="onRenameCategory"
      @delete-category="onDeleteCategory"
      @move-category="onMoveCategory"
      @change-cover="onChangeCover"
      @drop-card="onCardDropToCategory"
    />

    <main ref="mainRef" class="home-main">
      <!-- Mobile drawer trigger -->
      <div v-if="isMobile" class="mobile-bar">
        <button
          type="button"
          class="drawer-trigger"
          aria-label="打开分类抽屉"
          @click="drawerOpen = true"
        >
          <Icon name="lucide:menu" />
          <span>分类</span>
        </button>
        <span class="category-chip">{{ activeToolKey === 'collect' ? '我的收藏' : activeCategoryName }}</span>
      </div>

      <!-- Category heading -->
      <h2 v-if="!isMobile" class="category-heading">{{ activeToolKey === 'collect' ? '我的收藏' : activeCategoryName }}</h2>

      <div class="masonry">
        <div
          v-for="(col, ci) in columns"
          :key="ci"
          class="masonry-col"
        >
          <template v-for="(item, ri) in col" :key="item.id">
            <div
              class="card-reveal"
              :style="{ '--enter-delay': `${ci * 80 + ri * 60}ms` }"
              draggable="true"
              @click="onCardClick(item)"
              @dragstart="onCardDragStart($event, item)"
            >
              <!-- Hover actions -->
              <div class="card-actions">
                <button
                  type="button"
                  class="card-action-btn"
                  :class="{ 'card-action-faved': isFavorited(item.id) }"
                  :title="isFavorited(item.id) ? '取消收藏' : '收藏'"
                  @click.stop="handleToggleFavorite(item.id)"
                >
                  <Icon :name="isFavorited(item.id) ? 'ph:star-fill' : 'ph:star'" />
                </button>
                <button
                  type="button"
                  class="card-action-btn"
                  title="编辑"
                  @click.stop="onCardEdit({ id: item.id, type: item.type, title: item.title, description: item.description, image: item.image, images: item.images, tags: item.tags, aspectRatio: item.aspectRatio, categoryId: item.categoryId, createdAt: item.createdAt })"
                >
                  <Icon name="lucide:pencil" />
                </button>
                <button
                  type="button"
                  class="card-action-btn card-action-delete"
                  title="删除"
                  @click.stop="onCardDelete(item.id)"
                >
                  <Icon name="lucide:trash-2" />
                </button>
              </div>

              <component
                :is="cardTypeRegistry[item.type].component"
                v-bind="cardTypeRegistry[item.type].mapProps(item)"
              />
            </div>
          </template>
        </div>
      </div>

      <!-- Empty state -->
      <div
        v-if="!initLoading && allItems.length === 0"
        class="empty-state"
      >
        <Icon name="lucide:inbox" class="empty-icon" />
        <h3>还没有内容</h3>
        <p v-if="activeCategoryId !== 'all'">当前分类下暂无卡片</p>
        <p v-else-if="route.query.q">没有找到匹配「{{ route.query.q }}」的卡片</p>
        <p v-else>点击右下角 + 按钮创建第一张卡片</p>
      </div>

      <div ref="sentinel" class="sentinel">
        <template v-if="initLoading || loading">
          <div class="loading-bars">
            <span v-for="i in 3" :key="i" class="bar" :style="{ animationDelay: `${(i - 1) * 0.15}s` }" />
          </div>
        </template>
        <span v-else-if="hasMore && allItems.length > 0" class="sentinel-text">继续滚动查看更多</span>
        <span v-else-if="allItems.length > 0" class="sentinel-text end">—— 已经到底了 ——</span>
      </div>

      <!-- Floating add button -->
      <button type="button" class="fab-add" aria-label="新增卡片" @click="onNewCard">
        <Icon name="lucide:plus" />
      </button>
    </main>

    <!-- Tags Modal -->
    <Teleport to="body">
      <Transition name="modal-fade">
        <div v-if="showTagsModal" class="modal-overlay" @click.self="showTagsModal = false">
          <div class="modal-panel">
            <div class="modal-header">
              <h2 class="modal-title">标签</h2>
              <button type="button" class="modal-close" @click="showTagsModal = false">
                <Icon name="lucide:x" />
              </button>
            </div>
            <div class="modal-search">
              <Icon name="lucide:search" class="modal-search-icon" />
              <input
                v-model="tagSearch"
                type="text"
                class="modal-search-input"
                placeholder="搜索标签..."
              />
            </div>
            <div class="modal-body">
              <template v-if="tagsLoading">
                <div class="modal-loading">加载中...</div>
              </template>
              <template v-else-if="filteredTagList.length === 0">
                <div class="modal-empty">
                  <Icon name="lucide:tag" class="modal-empty-icon" />
                  <span>{{ tagSearch ? '没有匹配的标签' : '还没有标签' }}</span>
                </div>
              </template>
              <template v-else>
                <button
                  v-for="tag in filteredTagList"
                  :key="tag.id"
                  type="button"
                  class="modal-tag-item"
                  @click="onSelectTag(tag.id)"
                >
                  <Icon name="lucide:hash" class="modal-tag-icon" />
                  <span class="modal-tag-name">{{ tag.name }}</span>
                  <span class="modal-tag-count">{{ tag.slug }}</span>
                </button>
              </template>
            </div>
          </div>
        </div>
      </Transition>
    </Teleport>

    <!-- Card Detail Modal -->
    <IndexCardDetailModal
      :visible="showDetailModal"
      :card="detailCard"
      :categories="categories"
      @update:visible="(v) => showDetailModal = v"
      @favorite-toggled="onDetailFavoriteToggled"
    />

    <!-- Card Form Modal -->
    <IndexCardFormModal
      :visible="showCardFormModal"
      :mode="cardFormMode"
      :card="cardFormEditData"
      :categories="categories"
      @update:visible="(v) => showCardFormModal = v"
      @saved="onCardSaved"
    />

    <!-- Category Form Modal -->
    <IndexCategoryFormModal
      :visible="catFormVisible"
      :mode="catFormMode"
      :node="catFormNode"
      :parent-node="catFormParent"
      :categories="categories"
      @update:visible="(v) => catFormVisible = v"
      @submit="onCatFormSubmit"
    />

    <!-- Confirm Dialog -->
    <BoDialog v-model:show="confirmShow" :mask-can-close="true">
      <div class="rounded-lg bg-white p-6 shadow-xl border border-[#e6dfd8] confirm-dialog">
        <h3 class="text-[16px] font-medium text-[#141413]">确认操作</h3>
        <p class="mt-3 text-[14px] text-[#3d3d3a]">{{ confirmMsg }}</p>
        <div class="mt-6 flex justify-end gap-2">
          <button class="px-4 py-2 bg-[#faf9f5] border border-[#e6dfd8] rounded-lg text-[14px] hover:bg-white transition-colors" @click="confirmShow = false">取消</button>
          <button class="px-4 py-2 bg-[#cc785c] text-white rounded-lg text-[14px] hover:bg-[#a9583e] transition-colors" @click="onConfirm">确定</button>
        </div>
      </div>
    </BoDialog>
  </div>
</template>

<style scoped>
.home-layout {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 280px 1fr;
  align-items: start;
  gap: 0;
}

.home-sidebar {
  position: sticky;
  top: 64px;
  height: calc(100vh - 64px);
  align-self: start;
}

.home-main {
  min-width: 0;
  padding: 16px 24px 48px;
  display: flex;
  flex-direction: column;
}

/* ── Category heading ── */

.category-heading {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 500;
  color: var(--color-ink);
  margin: 0 0 12px 0;
  letter-spacing: -0.3px;
}

/* ── Mobile bar ── */

.mobile-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--color-hairline);
}

/* ── Masonry ── */

.masonry {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}

.masonry-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.card-reveal {
  animation: card-enter 0.6s var(--ease-out-expo) both;
  animation-delay: var(--enter-delay, 0ms);
  cursor: pointer;
  position: relative;
}

.card-actions {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  gap: 4px;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.18s ease;
}

.card-reveal:hover .card-actions {
  opacity: 1;
}

.card-action-btn {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 5px;
  background: rgba(20, 20, 19, 0.45);
  backdrop-filter: blur(4px);
  color: rgba(255, 255, 255, 0.85);
  cursor: pointer;
  transition: background 0.12s ease;
}

.card-action-btn :deep(svg) {
  width: 12px;
  height: 12px;
}

.card-action-btn:hover {
  background: rgba(20, 20, 19, 0.7);
  color: #fff;
}

.card-action-delete:hover {
  background: rgba(198, 69, 69, 0.8);
}

.card-action-faved {
  color: var(--color-accent-amber);
}

@keyframes card-enter {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ── Sentinel ── */

.sentinel {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 40px 0 24px;
  min-height: 56px;
}

.sentinel-text {
  font-size: 13px;
  color: var(--color-muted-soft);
}

.sentinel-text.end {
  color: var(--color-hairline);
}

.loading-bars {
  display: flex;
  gap: 5px;
  align-items: center;
}

.bar {
  width: 4px;
  height: 18px;
  background: var(--color-primary);
  border-radius: 2px;
  opacity: 0.5;
  animation: pulse 0.9s ease-in-out infinite;
}

/* ── Empty state ── */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 64px 24px;
  text-align: center;
}

.empty-icon {
  width: 48px;
  height: 48px;
  color: var(--color-hairline);
  margin-bottom: 4px;
}

.empty-state h3 {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 400;
  color: var(--color-muted);
  margin: 0;
}

.empty-state p {
  font-size: 13px;
  color: var(--color-muted-soft);
  margin: 0;
  max-width: 300px;
}

/* ── Floating add button ── */

.fab-add {
  position: fixed;
  bottom: 32px;
  right: 32px;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--color-primary);
  border: none;
  color: var(--color-on-primary, #fff);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 16px rgba(204, 120, 92, 0.35);
  transition: transform 0.2s var(--ease-out-expo), box-shadow 0.2s ease;
  z-index: 40;
}

.fab-add :deep(svg) {
  width: 22px;
  height: 22px;
}

.fab-add:hover {
  transform: scale(1.08);
  box-shadow: 0 6px 24px rgba(204, 120, 92, 0.45);
}

@keyframes pulse {
  0%, 100% { opacity: 0.3; transform: scaleY(0.6); }
  50% { opacity: 0.8; transform: scaleY(1); }
}

/* ── Responsive (DESIGN.md breakpoints) ── */

@media (max-width: 767.98px) {
  .home-layout {
    grid-template-columns: 1fr;
  }

  .home-sidebar {
    display: none;
  }

  .home-main {
    padding: 8px 12px 24px;
  }
}

@media (min-width: 768px) and (max-width: 1023.98px) {
  .home-layout {
    grid-template-columns: 240px 1fr;
  }

  .home-main {
    padding: 20px 16px 32px;
  }
}

.drawer-trigger {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  padding: 0 16px;
  border-radius: var(--rounded-md, 8px);
  background: transparent;
  border: 1px solid var(--color-hairline);
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 500;
  color: var(--color-ink);
  cursor: pointer;
  min-width: 44px;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.drawer-trigger:hover {
  border-color: var(--color-ink);
}

.drawer-trigger :deep(svg) {
  width: 18px;
  height: 18px;
}

.category-chip {
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 0 14px;
  border-radius: 9999px;
  background: var(--color-surface-card);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
}

/* ── Tags Modal ── */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(4px);
}

.modal-panel {
  width: 90vw;
  max-width: 420px;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--color-surface-default, #fff);
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
  overflow: hidden;
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px 12px;
}

.modal-title {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 500;
  color: var(--color-ink);
  margin: 0;
}

.modal-close {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-muted);
  transition: all 0.12s ease;
}

.modal-close :deep(svg) {
  width: 18px;
  height: 18px;
}

.modal-close:hover {
  background: var(--color-surface-soft);
  color: var(--color-ink);
}

.modal-search {
  position: relative;
  margin: 0 20px 8px;
}

.modal-search-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: var(--color-muted-soft);
}

.modal-search-input {
  width: 100%;
  height: 40px;
  padding: 0 12px 0 36px;
  border-radius: 10px;
  border: 1px solid var(--color-hairline);
  background: var(--color-surface-soft);
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--color-ink);
  outline: none;
  transition: border-color 0.15s ease;
}

.modal-search-input:focus {
  border-color: var(--color-primary);
}

.modal-search-input::placeholder {
  color: var(--color-muted-soft);
}

.modal-body {
  flex: 1;
  overflow-y: auto;
  padding: 8px 12px 16px;
}

.modal-loading,
.modal-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 32px 16px;
  color: var(--color-muted-soft);
  font-size: 13px;
}

.modal-empty-icon {
  width: 28px;
  height: 28px;
  opacity: 0.4;
}

.modal-tag-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
  text-align: left;
  transition: background 0.12s ease;
}

.modal-tag-item:hover {
  background: var(--color-surface-soft);
}

.modal-tag-icon {
  width: 16px;
  height: 16px;
  color: var(--color-muted);
  flex-shrink: 0;
}

.modal-tag-name {
  flex: 1;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-ink);
}

.modal-tag-count {
  font-size: 11px;
  color: var(--color-muted-soft);
  font-family: var(--font-mono, monospace);
}

/* ── Modal transition ── */

.modal-fade-enter-active,
.modal-fade-leave-active {
  transition: opacity 0.2s ease;
}

.modal-fade-enter-active .modal-panel,
.modal-fade-leave-active .modal-panel {
  transition: transform 0.2s var(--ease-out-expo);
}

.modal-fade-enter-from,
.modal-fade-leave-to {
  opacity: 0;
}

.modal-fade-enter-from .modal-panel {
  transform: scale(0.95) translateY(8px);
}

.modal-fade-leave-to .modal-panel {
  transform: scale(0.95) translateY(8px);
}
</style>
