<script lang="ts" setup>
import { useHead } from 'nuxt/app'
import { hasProductionUrl, previewSource, productionUrl } from 'nuxtseo-layer-devtools/composables/state'
import { withHttps } from 'ufo'
import { computed, reactive, ref, watch } from 'vue'
import { useOgImage } from '../../lib/og-image/og-image'
import { RendererSelectDialogPromise } from '../../lib/og-image/renderer-select'
import { isConnectionFailed, isFallbackMode } from '../../lib/og-image/rpc'

const RE_SINGLE_QUOTE = /'/g
const RE_VUE_FILENAME = /[^/]+\.vue$/
const RE_HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i

const {
  globalDebug,
  isDebugLoading,
  isCustomOgImage,
  isValidDebugError,
  hasDefinedOgImage,
  fetchError,
  aspectRatio,
  imageFormat,
  socialPreview,
  imageColorMode,
  src,
  whatsappInlineSrc,
  socialPreviewTitle,
  socialPreviewDescription,
  socialSiteUrl,
  slackSocialPreviewSiteName,
  activeComponentName,
  activeComponent,
  activeComponentRelativePath,
  isOgImageTemplate,
  renderer,
  isComponentCompatibleWithRenderer,
  sidePanelOpen,
  isPageScreenshot,
  currentPageFile,
  allImageKeys,
  description,
  hasMadeChanges,
  options,
  propEditor,
  ogImageKey,
  toggleSocialPreview: _toggleSocialPreview,
  generateLoadTime,
  openImage,
  openCurrentPageFile,
  openCurrentComponent,
  componentNames,
  ejectComponent,
  createComponent,
  addExistingComponent,
  resetProps,
  updateProps,
  refreshSources,
} = useOgImage()

const EXCLUDED_PROPS = new Set(['colorMode'])
const editableProps = computed(() =>
  Object.keys(propEditor.value).filter(k => !EXCLUDED_PROPS.has(k)),
)

function editProp(key: string, value: unknown) {
  propEditor.value = { ...propEditor.value, [key]: value }
  updateProps({ [key]: value })
}

function inferPropType(value: unknown): 'color' | 'number' | 'json' | 'string' {
  if (typeof value === 'number')
    return 'number'
  if (typeof value === 'string' && RE_HEX_COLOR.test(value))
    return 'color'
  if (typeof value === 'string')
    return 'string'
  return 'json'
}

const rendererIcons: Record<string, string> = {
  satori: 'logos:vercel-icon',
  browser: 'logos:chrome',
}

const activeFormatLabel = computed(() => {
  const ext = imageFormat.value === 'jpeg' ? 'JPG' : imageFormat.value?.toUpperCase() || 'PNG'
  const name = renderer.value === 'browser' ? 'Browser' : renderer.value === 'takumi' ? 'Takumi' : 'Satori'
  return `${name} - ${ext}`
})

const socialItems = [
  { label: 'Raw', icon: 'carbon:image', value: '' },
  // Optical size adjustments: thin logos scale up, dense filled shapes scale down
  { label: 'Twitter / X', icon: 'simple-icons:x', value: 'twitter', iconScale: 0.8 },
  { label: 'Facebook', icon: 'simple-icons:facebook', value: 'facebook', iconScale: 0.92 },
  { label: 'LinkedIn', icon: 'simple-icons:linkedin', value: 'linkedin', iconScale: 0.92 },
  { label: 'Discord', icon: 'simple-icons:discord', value: 'discord', iconScale: 0.95 },
  { label: 'Slack', icon: 'simple-icons:slack', value: 'slack' },
  { label: 'WhatsApp', icon: 'simple-icons:whatsapp', value: 'whatsapp', iconScale: 0.92 },
  { label: 'Bluesky', icon: 'simple-icons:bluesky', value: 'bluesky', iconScale: 0.92 },
]

// Size variants per social platform — different display contexts show the card at different widths
interface SizeVariant { label: string, width: number }
const socialSizeVariants: Record<string, SizeVariant[]> = {
  twitter: [
    { label: 'Feed', width: 504 },
    { label: 'Detail', width: 600 },
    { label: 'Mobile', width: 335 },
  ],
  facebook: [
    { label: 'Feed', width: 500 },
    { label: 'Mobile', width: 320 },
  ],
  linkedin: [
    { label: 'Feed', width: 552 },
    { label: 'Mobile', width: 335 },
  ],
  discord: [
    { label: 'Embed', width: 432 },
    { label: 'Mobile', width: 300 },
  ],
  slack: [
    { label: 'Desktop', width: 500 },
    { label: 'Compact', width: 360 },
  ],
  whatsapp: [
    { label: 'Default', width: 600 },
    { label: 'Squared', width: 600 },
  ],
  bluesky: [
    { label: 'Post', width: 513 },
    { label: 'With link', width: 487 },
    { label: 'Mobile', width: 335 },
  ],
}

const activeSizeVariant = ref<Record<string, number>>({})

const currentSizeVariants = computed(() => socialSizeVariants[socialPreview.value] || [])
const currentSizeWidth = computed(() => {
  const variants = currentSizeVariants.value
  if (!variants.length)
    return undefined
  const idx = activeSizeVariant.value[socialPreview.value] ?? 0
  return variants[idx]?.width
})

// Raw preview draggable resize
const rawResizeWidth = ref<number | null>(null)
const isRawDragging = ref(false)
const rawPreviewRef = ref<HTMLElement | null>(null)

const isRawMode = computed(() => !socialPreview.value)

const previewMaxWidth = computed(() => {
  if (isRawMode.value && rawResizeWidth.value)
    return `${rawResizeWidth.value}px`
  if (currentSizeWidth.value)
    return `${currentSizeWidth.value}px`
  return undefined
})

const whatsappSquared = computed(() => {
  const idx = activeSizeVariant.value.whatsapp ?? 0
  return socialPreview.value === 'whatsapp' && idx === 1
})

function onRawHandlePointerDown(e: PointerEvent) {
  e.preventDefault()
  const el = rawPreviewRef.value
  if (!el)
    return
  isRawDragging.value = true
  const startX = e.clientX
  const startWidth = el.getBoundingClientRect().width

  function onMove(ev: PointerEvent) {
    // Handle is centered, so distance from center = half the width change
    const dx = ev.clientX - startX
    rawResizeWidth.value = Math.max(200, Math.round(startWidth + dx * 2))
  }
  function onUp() {
    isRawDragging.value = false
    document.removeEventListener('pointermove', onMove)
    document.removeEventListener('pointerup', onUp)
  }
  document.addEventListener('pointermove', onMove)
  document.addEventListener('pointerup', onUp)
}

const protoTab = ref('meta-tags')
const protoTabs = computed(() => {
  const tabs = [
    { label: 'Meta Tags', value: 'meta-tags' },
    { label: 'OG Image Props', value: 'og-props' },
    { label: 'Fonts', value: 'fonts' },
  ]
  if (!socialPreview.value)
    return tabs.filter(t => t.value !== 'meta-tags')
  return tabs
})

watch(socialPreview, (val) => {
  if (!val && protoTab.value === 'meta-tags')
    protoTab.value = 'og-props'
})

const isTwitterMode = computed(() => socialPreview.value === 'twitter')
const metaLabelPrefix = computed(() => isTwitterMode.value ? 'Twitter' : 'OG')

interface FontFileEntry {
  key: string
  family: string
  weight: number
  style: string
  src: string
  label: string
  loaded: boolean
  subsetCount: number
  subsets: string[]
}

const fontFiles = computed<FontFileEntry[]>(() => {
  const available = globalDebug.value?.availableFonts || []
  const resolved = globalDebug.value?.resolvedFonts || []
  if (!available.length && !resolved.length)
    return []
  const source = available.length ? available : resolved
  // Build set of resolved keys for loaded check
  const resolvedKeys = new Set(resolved.map((f: any) => `${f.family}-${f.weight}-${f.style}`))
  // Count subsets per family+weight+style and collect subset names
  const subsetMap = new Map<string, string[]>()
  for (const f of source) {
    const key = `${f.family}-${f.weight}-${f.style}`
    const subs = subsetMap.get(key) || []
    if (f.subset)
      subs.push(f.subset)
    subsetMap.set(key, subs)
  }
  // Dedupe by family+weight+style (multiple unicode-range subsets)
  const seen = new Set<string>()
  const entries: FontFileEntry[] = []
  for (const f of source) {
    const key = `${f.family}-${f.weight}-${f.style}`
    if (seen.has(key))
      continue
    seen.add(key)
    const subs = subsetMap.get(key) || []
    entries.push({
      key,
      family: f.family,
      weight: f.weight,
      style: f.style,
      src: f.src,
      label: `${f.family} ${f.weight}${f.style === 'italic' ? 'i' : ''}`,
      loaded: resolvedKeys.has(key),
      subsetCount: Math.max(subs.length, 1),
      subsets: subs,
    })
  }
  // Sort: by family, then weight, then style
  entries.sort((a, b) => a.family.localeCompare(b.family) || a.weight - b.weight || a.style.localeCompare(b.style))
  return entries
})

/** Total byte size of all resolved font files per family */
const fontFamilySizes = computed(() => {
  const resolved = globalDebug.value?.resolvedFonts || []
  const sizes = new Map<string, number>()
  for (const f of resolved) {
    if (f.size)
      sizes.set(f.family, (sizes.get(f.family) || 0) + f.size)
  }
  return sizes
})

function formatBytes(bytes: number): string {
  if (bytes < 1024)
    return `${bytes} B`
  const kb = bytes / 1024
  return kb < 1000 ? `${kb.toFixed(1)} kB` : `${(kb / 1024).toFixed(1)} MB`
}

const fontFamilyNames = computed(() => [...new Set(fontFiles.value.map(f => f.family))])
const resolvedFamilyNames = computed(() => [...new Set(fontFiles.value.filter(f => f.loaded).map(f => f.family))])

const detectedFontRequirements = computed(() => globalDebug.value?.fontRequirements)

const unresolvedFamilies = computed(() => {
  const detected = detectedFontRequirements.value?.families
  if (!detected?.length)
    return []
  const available = new Set(fontFamilyNames.value)
  return detected.filter((f: string) => !available.has(f))
})

const fontFaceCss = computed(() => {
  const rules: string[] = []
  const seen = new Set<string>()
  for (const f of fontFiles.value) {
    if (seen.has(f.family))
      continue
    seen.add(f.family)
    rules.push(`@font-face { font-family: 'ogp-${f.family}'; src: url('${f.src}'); font-display: swap; }`)
  }
  return rules.join('\n')
})

useHead({ style: computed(() => fontFaceCss.value ? [fontFaceCss.value] : []) })

const fontOverride = ref('')

function applyFontOverride(family: string) {
  const next = fontOverride.value === family ? '' : family
  fontOverride.value = next
  hasMadeChanges.value = true
  updateProps({ fontFamily: next || undefined })
}

const metaOverrides = reactive({
  ogTitle: '',
  twitterTitle: '',
  siteName: '',
  description: '',
})

const effectiveTitle = computed(() => {
  if (socialPreview.value === 'twitter' && metaOverrides.twitterTitle)
    return metaOverrides.twitterTitle
  return metaOverrides.ogTitle || socialPreviewTitle.value
})

const effectiveDescription = computed(() => {
  return metaOverrides.description || socialPreviewDescription.value
})

const effectiveSiteName = computed(() => {
  return metaOverrides.siteName || slackSocialPreviewSiteName.value
})

const effectiveSiteUrl = computed(() => {
  return metaOverrides.siteName || socialSiteUrl.value
})

function updateMetaField(field: keyof typeof metaOverrides, value: string) {
  metaOverrides[field] = value
  hasMadeChanges.value = true
}

const hasMetaOverrides = computed(() => Object.values(metaOverrides).some(Boolean))

const seoMetaSnippet = computed(() => {
  const entries: string[] = []
  if (metaOverrides.ogTitle)
    entries.push(`  ogTitle: '${metaOverrides.ogTitle.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
  if (metaOverrides.description)
    entries.push(`  ogDescription: '${metaOverrides.description.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
  if (metaOverrides.siteName)
    entries.push(`  ogSiteName: '${metaOverrides.siteName.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
  if (metaOverrides.twitterTitle)
    entries.push(`  twitterTitle: '${metaOverrides.twitterTitle.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
  if (!entries.length)
    return ''
  return `useSeoMeta({\n${entries.join(',\n')}\n})`
})

function resetAll() {
  resetProps(true)
  fontOverride.value = ''
  Object.assign(metaOverrides, { ogTitle: '', twitterTitle: '', siteName: '', description: '' })
}

const productionHostname = computed(() => {
  try {
    return new URL(productionUrl.value).hostname
  }
  catch {
    return productionUrl.value
  }
})
</script>

<template>
  <div class="preview-container card animate-fade-up">
    <!-- Demo mode when devtools connection fails -->
    <div v-if="isConnectionFailed" class="h-full flex flex-col">
      <DevtoolsAlert variant="warning">
        Could not connect to devtools. Showing demo preview.
      </DevtoolsAlert>
      <div class="flex-1 flex items-center justify-center p-6 sm:p-8">
        <div class="w-full max-w-2xl">
          <TwitterCardRenderer title="My Page Title" :aspect-ratio="1200 / 630">
            <template #domain>
              <span>From example.com</span>
            </template>
            <div class="w-full h-full bg-gradient-to-br from-emerald-500 to-sky-500 flex items-center justify-center">
              <div class="text-white text-center p-6">
                <div class="text-2xl sm:text-4xl font-bold mb-2">
                  OG Image Preview
                </div>
                <div class="text-sm sm:text-lg opacity-80">
                  Connect devtools to see your actual image
                </div>
              </div>
            </div>
          </TwitterCardRenderer>
          <p class="text-center text-sm text-[var(--color-text-muted)] mt-4">
            Open this page in Nuxt DevTools to preview your OG images.
          </p>
        </div>
      </div>
    </div>

    <!-- Custom OG Image (prebuilt URL) -->
    <div v-else-if="isCustomOgImage" class="h-full flex flex-col">
      <DevtoolsToolbar variant="minimal">
        <span class="text-[var(--color-text-subtle)]">Prebuilt:</span>
        <code class="text-[var(--color-text-muted)]">{{ options?.url }}</code>
      </DevtoolsToolbar>
      <div class="flex-1 flex items-center justify-center p-6 sm:p-8">
        <ImageLoader
          :src="src"
          :aspect-ratio="aspectRatio"
          @load="generateLoadTime"
          @refresh="refreshSources"
        />
      </div>
    </div>

    <!-- Loading state -->
    <div v-else-if="isDebugLoading" class="h-full flex items-center justify-center p-6 sm:p-8">
      <DevtoolsEmptyState icon="carbon:image-search" title="Loading OG Image&#8230;" class="animate-pulse" />
    </div>

    <!-- Server error (e.g. font loading failure) -->
    <div v-else-if="fetchError" class="h-full flex items-center justify-center p-6 sm:p-8">
      <DevtoolsEmptyState
        icon="carbon:warning"
        title="OG Image Error"
        :description="fetchError.message"
        variant="error"
        class="animate-scale-in"
      >
        <details v-if="fetchError.stack?.length" class="text-left w-full">
          <summary class="text-xs text-[var(--color-text-subtle)] cursor-pointer hover:text-[var(--color-text-muted)]">
            Stack trace
          </summary>
          <pre class="mt-2 text-xs text-[var(--color-text-subtle)] overflow-auto max-h-48 p-3 rounded-lg bg-[var(--color-surface-sunken)] border border-[var(--color-border-subtle)]">{{ fetchError.stack.join('\n') }}</pre>
        </details>
      </DevtoolsEmptyState>
    </div>

    <!-- Missing defineOgImage error -->
    <div v-else-if="isValidDebugError || !hasDefinedOgImage" class="h-full flex items-center justify-center p-6 sm:p-8">
      <DevtoolsEmptyState
        icon="carbon:image-search"
        title="No OG Image Defined"
        class="animate-scale-in"
      >
        <template #description>
          This page has no <code class="inline-code">defineOgImage()</code> in
          <UButton variant="link" class="cursor-pointer" @click="openCurrentPageFile">
            {{ currentPageFile }}
          </UButton>
        </template>

        <!-- Existing components: let user pick one -->
        <div v-if="componentNames.length" class="mb-4 flex flex-col items-center gap-3">
          <div class="flex flex-wrap justify-center gap-2">
            <UButton
              v-for="c in componentNames"
              :key="c.pascalName"
              color="primary"
              variant="soft"
              size="sm"
              @click="addExistingComponent(c.pascalName)"
            >
              {{ c.pascalName }}
            </UButton>
          </div>
          <span class="text-xs text-[var(--color-text-subtle)]">or</span>
          <UButton variant="link" size="xs" class="text-[var(--color-text-subtle)]" @click="createComponent()">
            Create new component
          </UButton>
        </div>

        <!-- No components: create is the primary CTA -->
        <div v-else class="mb-4">
          <UButton color="primary" size="md" @click="createComponent()">
            <UIcon name="carbon:add" class="w-4 h-4" />
            Create OG Image Component
          </UButton>
        </div>

        <div v-if="globalDebug?.runtimeConfig?.hasNuxtContent" class="text-sm text-[var(--color-text-subtle)]">
          Using Nuxt Content? See the <a href="https://nuxtseo.com/docs/integrations/content" target="_blank" class="text-[var(--seo-green)] hover:underline">integration guide</a>
        </div>
        <a v-else href="https://nuxtseo.com/og-image/getting-started/getting-familar-with-nuxt-og-image" target="_blank" class="text-sm text-[var(--seo-green)] hover:underline inline-flex items-center gap-1">
          Learn more
          <UIcon name="carbon:arrow-right" class="w-3 h-3" />
        </a>
      </DevtoolsEmptyState>
    </div>

    <!-- Main preview UI -->
    <div v-else class="h-full flex flex-col">
      <!-- Fallback mode banner -->
      <DevtoolsAlert v-if="isFallbackMode" variant="info">
        Fallback mode: Connected to localhost:3000
      </DevtoolsAlert>

      <!-- Production preview banner -->
      <DevtoolsAlert v-if="previewSource === 'production' && hasProductionUrl" variant="production">
        Previewing from <strong>{{ productionUrl }}</strong>
        <template #action>
          <UButton variant="link" size="xs" @click="previewSource = 'local'">
            Switch to local
          </UButton>
        </template>
      </DevtoolsAlert>

      <!-- Renderer incompatibility banner -->
      <DevtoolsAlert v-if="!isComponentCompatibleWithRenderer && activeComponent" variant="warning">
        Component <code class="inline-code">{{ activeComponentName }}</code> uses {{ activeComponent.renderer }} renderer.
        <NuxtLink to="/templates" class="text-[var(--seo-green)] hover:underline ml-1">
          Select a {{ renderer }} template
        </NuxtLink>
      </DevtoolsAlert>

      <!-- Top toolbar -->
      <DevtoolsToolbar>
        <!-- Left: Renderer + Format controls -->
        <div class="flex items-center gap-2 sm:gap-3 flex-wrap">
          <UButton color="neutral" variant="ghost" size="xs" aria-label="Change renderer and format" @click="RendererSelectDialogPromise.start()">
            <img v-if="renderer === 'takumi'" src="https://takumi.kane.tw/logo.svg" class="w-3.5 h-3.5" width="14" height="14" alt="">
            <UIcon v-else :name="rendererIcons[renderer] || 'logos:vercel-icon'" class="w-3.5 h-3.5" />
            <span class="hidden sm:inline">{{ activeFormatLabel }}</span>
            <UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-60" />
          </UButton>
        </div>

        <!-- Center: Component info -->
        <div v-if="!isPageScreenshot" class="component-info hidden md:flex">
          <UButton
            variant="link"
            class="component-path-link cursor-pointer"
            :disabled="isOgImageTemplate"
            @click="openCurrentComponent"
          >
            <span v-if="activeComponentRelativePath"><span class="text-[var(--color-text-muted)] hidden min-[850px]:inline">{{ activeComponentRelativePath.replace(RE_VUE_FILENAME, '') }}</span><span class="text-[var(--color-text)]">{{ activeComponentRelativePath.match(RE_VUE_FILENAME)?.[0] || activeComponentName }}</span></span>
            <span v-else class="text-[var(--color-text)]">{{ activeComponentName }}</span>
          </UButton>
          <UButton
            v-if="isOgImageTemplate"
            variant="link"
            size="xs"
            class="cursor-pointer text-[var(--color-text-subtle)]"
            @click="ejectComponent(activeComponentName)"
          >
            Eject
          </UButton>
        </div>
        <div v-else class="component-info hidden md:flex">
          <span class="text-[var(--color-text-subtle)]">Page Screenshot</span>
        </div>

        <!-- Right: Props toggle -->
        <div class="flex items-center gap-2">
          <!-- Production URL indicator -->
          <UTooltip v-if="previewSource === 'production' && hasProductionUrl" :text="productionUrl" :delay-duration="200">
            <span class="devtools-production-badge">
              <span class="devtools-production-dot" />
              <span class="hidden sm:inline text-xs">{{ productionHostname }}</span>
            </span>
          </UTooltip>

          <UButton
            v-if="!isPageScreenshot"
            :variant="sidePanelOpen ? 'soft' : 'ghost'"
            :color="sidePanelOpen ? 'primary' : 'neutral'"
            size="xs"
            icon="carbon:settings-adjust"
            class="props-toggle"
            @click="sidePanelOpen = !sidePanelOpen"
          >
            <span class="hidden sm:inline">Debug</span>
          </UButton>
        </div>
      </DevtoolsToolbar>

      <!-- Social preview tabs -->
      <div class="px-3 sm:px-4 border-b border-[var(--color-border)] bg-[var(--color-surface-elevated)]">
        <UTabs
          v-model="socialPreview"
          :items="socialItems"
          :content="false"
          size="xs"
          variant="link"
          color="neutral"
        >
          <template #leading="{ item, ui }">
            <UIcon
              :name="item.icon"
              :class="ui.leadingIcon"
              :style="item.iconScale ? { transform: `scale(${item.iconScale})` } : undefined"
            />
          </template>
        </UTabs>
      </div>

      <!-- Size variant toggles + image key selector -->
      <div v-if="currentSizeVariants.length || allImageKeys.length > 1" class="size-variant-bar">
        <template v-if="currentSizeVariants.length">
          <span class="size-variant-label">Width</span>
          <div class="size-variant-group">
            <button
              v-for="(variant, idx) in currentSizeVariants"
              :key="variant.label"
              class="size-variant-btn"
              :class="{ active: (activeSizeVariant[socialPreview] ?? 0) === idx }"
              @click="activeSizeVariant[socialPreview] = idx"
            >
              <span class="size-variant-name">{{ variant.label }}</span>
              <span class="size-variant-px">{{ variant.width }}px</span>
            </button>
          </div>
        </template>

        <template v-if="allImageKeys.length > 1">
          <span class="size-variant-label">Image</span>
          <div class="size-variant-group">
            <button
              v-for="key in allImageKeys"
              :key="key"
              class="size-variant-btn"
              :class="{ active: (ogImageKey || 'og') === key }"
              @click="ogImageKey = key"
            >
              <span class="size-variant-name">{{ key }}</span>
            </button>
          </div>
        </template>
      </div>

      <!-- Preview area -->
      <div class="preview-area panel-grids" :class="{ 'preview-area--panel-open': sidePanelOpen && !isPageScreenshot }">
        <div ref="rawPreviewRef" class="preview-content" :class="{ 'preview-content--no-transition': isRawDragging }" :style="previewMaxWidth ? { maxWidth: previewMaxWidth } : undefined">
          <!-- Twitter/X preview -->
          <TwitterCardRenderer v-if="socialPreview === 'twitter'" :title="effectiveTitle" :aspect-ratio="aspectRatio">
            <template #domain>
              <a target="_blank" :href="withHttps(socialSiteUrl)">From {{ effectiveSiteUrl }}</a>
            </template>
            <ImageLoader
              v-if="imageFormat !== 'html'"
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @click="openImage"
              @refresh="refreshSources"
            />
            <IFrameLoader
              v-else
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
          </TwitterCardRenderer>

          <!-- Facebook preview -->
          <FacebookCardRenderer v-else-if="socialPreview === 'facebook'">
            <template #siteName>
              {{ effectiveSiteUrl }}
            </template>
            <template #title>
              {{ effectiveTitle }}
            </template>
            <template #description>
              {{ effectiveDescription }}
            </template>
            <ImageLoader
              v-if="imageFormat !== 'html'"
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
            <IFrameLoader
              v-else
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
          </FacebookCardRenderer>

          <!-- LinkedIn preview -->
          <LinkedInCardRenderer v-else-if="socialPreview === 'linkedin'">
            <template #siteName>
              {{ effectiveSiteUrl }}
            </template>
            <template #title>
              {{ effectiveTitle }}
            </template>
            <ImageLoader
              v-if="imageFormat !== 'html'"
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
            <IFrameLoader
              v-else
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
          </LinkedInCardRenderer>

          <!-- Discord preview -->
          <DiscordCardRenderer v-else-if="socialPreview === 'discord'">
            <template #siteName>
              {{ effectiveSiteName }}
            </template>
            <template #title>
              {{ effectiveTitle }}
            </template>
            <template #description>
              {{ effectiveDescription }}
            </template>
            <ImageLoader
              v-if="imageFormat !== 'html'"
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
            <IFrameLoader
              v-else
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
          </DiscordCardRenderer>

          <!-- Slack preview -->
          <SlackCardRenderer v-else-if="socialPreview === 'slack'">
            <template #favIcon>
              <img :src="`https://www.google.com/s2/favicons?domain=${encodeURIComponent(socialSiteUrl)}&sz=30`" width="30" height="30" alt="">
            </template>
            <template #siteName>
              {{ effectiveSiteName }}
            </template>
            <template #title>
              {{ effectiveTitle }}
            </template>
            <template #description>
              {{ effectiveDescription }}
            </template>
            <ImageLoader
              v-if="imageFormat !== 'html'"
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
            <IFrameLoader
              v-else
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
          </SlackCardRenderer>

          <!-- WhatsApp preview -->
          <WhatsAppRenderer v-else-if="socialPreview === 'whatsapp'" :squared="whatsappSquared">
            <template #siteName>
              {{ effectiveSiteName }}
            </template>
            <template #title>
              {{ effectiveTitle }}
            </template>
            <template #description>
              {{ effectiveDescription }}
            </template>
            <template #url>
              {{ effectiveSiteUrl }}
            </template>
            <template v-if="whatsappInlineSrc !== src" #inlineImage>
              <img
                :src="whatsappInlineSrc"
                alt=""
              >
            </template>
            <img
              v-if="imageFormat !== 'html'"
              :src="src"
              alt=""
              @load="generateLoadTime({ timeTaken: '0', sizeKb: '' })"
            >
          </WhatsAppRenderer>

          <!-- Bluesky preview -->
          <BlueskyCardRenderer v-else-if="socialPreview === 'bluesky'">
            <template #siteName>
              {{ effectiveSiteUrl }}
            </template>
            <template #title>
              {{ effectiveTitle }}
            </template>
            <template #description>
              {{ effectiveDescription }}
            </template>
            <ImageLoader
              v-if="imageFormat !== 'html'"
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
            <IFrameLoader
              v-else
              :src="src"
              :aspect-ratio="aspectRatio"
              @load="generateLoadTime"
              @refresh="refreshSources"
            />
          </BlueskyCardRenderer>

          <!-- Raw preview with resize handle -->
          <div v-else class="raw-preview-wrapper">
            <div class="raw-preview" :class="{ 'raw-preview--dragging': isRawDragging }">
              <ImageLoader
                v-if="imageFormat !== 'html'"
                :src="src"
                :aspect-ratio="aspectRatio"
                @load="generateLoadTime"
                @refresh="refreshSources"
              />
              <IFrameLoader
                v-else
                :src="src"
                :aspect-ratio="aspectRatio"
                @load="generateLoadTime"
                @refresh="refreshSources"
              />
            </div>
            <!-- Drag handle — double-click resets -->
            <div
              class="raw-resize-handle"
              @pointerdown="onRawHandlePointerDown"
              @dblclick="rawResizeWidth = null"
            >
              <div class="raw-resize-grip" />
            </div>
            <!-- Width readout -->
            <div v-if="rawResizeWidth" class="raw-resize-readout">
              {{ rawResizeWidth }}px
            </div>
          </div>

          <!-- Status line -->
          <div v-if="description" class="status-line">
            {{ description }}
          </div>
        </div>
      </div>

      <!-- Floating Props Panel -->
      <Transition
        enter-active-class="transition duration-200 ease-out"
        enter-from-class="opacity-0 translate-x-4"
        enter-to-class="opacity-100 translate-x-0"
        leave-active-class="transition duration-150 ease-in"
        leave-from-class="opacity-100 translate-x-0"
        leave-to-class="opacity-0 translate-x-4"
      >
        <DevtoolsPanel v-if="sidePanelOpen && !isPageScreenshot" title="Debug" class="props-panel" @close="sidePanelOpen = false">
          <template #actions>
            <UButton
              v-if="hasMadeChanges"
              @click="resetAll()"
            >
              Reset
            </UButton>
          </template>
          <div class="px-3 pt-2 border-b border-[var(--color-border)]">
            <UTabs
              v-model="protoTab"
              :items="protoTabs"
              :content="false"
              size="xs"
              variant="link"
              color="neutral"
            />
          </div>

          <!-- Meta Tags Tab -->
          <div v-if="protoTab === 'meta-tags'">
            <div class="props-field">
              <div class="props-field-label">
                <span>{{ metaLabelPrefix }} Title</span>
                <UTooltip :text="isTwitterMode ? 'The twitter:title meta tag. Controls the title shown on Twitter/X cards.' : 'The og:title meta tag. Controls the title shown when shared on social platforms.'" :delay-duration="0">
                  <UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
                </UTooltip>
              </div>
              <UInput
                :model-value="isTwitterMode ? (metaOverrides.twitterTitle || socialPreviewTitle) : (metaOverrides.ogTitle || socialPreviewTitle)"
                size="xs"
                :name="isTwitterMode ? 'twitter-title' : 'og-title'"
                autocomplete="off"
                :placeholder="isTwitterMode ? 'twitter:title…' : 'og:title…'"
                @update:model-value="updateMetaField(isTwitterMode ? 'twitterTitle' : 'ogTitle', $event as string)"
              />
            </div>

            <div class="props-field">
              <div class="props-field-label">
                <span>{{ metaLabelPrefix }} Description</span>
                <UTooltip :text="isTwitterMode ? 'The twitter:description meta tag. Controls the description shown on Twitter/X cards.' : 'The og:description meta tag. Controls the description shown when shared on social platforms.'" :delay-duration="0">
                  <UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
                </UTooltip>
              </div>
              <UInput
                :model-value="metaOverrides.description || socialPreviewDescription"
                size="xs"
                :name="isTwitterMode ? 'twitter-description' : 'og-description'"
                autocomplete="off"
                :placeholder="isTwitterMode ? 'twitter:description…' : 'og:description…'"
                @update:model-value="updateMetaField('description', $event as string)"
              />
            </div>

            <div class="props-field">
              <div class="props-field-label">
                <span>{{ socialPreview === 'discord' ? 'OG Site Name' : 'OG URL' }}</span>
                <UTooltip :text="socialPreview === 'discord' ? 'The og:site_name meta tag. Discord uses this for the provider name above the title.' : 'The og:url meta tag. The canonical URL shown in social card previews.'" :delay-duration="0">
                  <UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
                </UTooltip>
              </div>
              <UInput
                :model-value="metaOverrides.siteName || slackSocialPreviewSiteName"
                size="xs"
                :name="socialPreview === 'discord' ? 'og-site-name' : 'og-url'"
                autocomplete="off"
                :placeholder="socialPreview === 'discord' ? 'og:site_name…' : 'og:url…'"
                @update:model-value="updateMetaField('siteName', $event as string)"
              />
            </div>
          </div>

          <!-- OG Image Props Tab -->
          <div v-else-if="protoTab === 'og-props'">
            <div class="props-field">
              <div class="props-field-label">
                <span>Color Mode</span>
                <UTooltip text="Changes the color mode passed to the OG image renderer." :delay-duration="0">
                  <UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
                </UTooltip>
              </div>
              <UButton
                size="xs"
                color="neutral"
                variant="soft"
                :icon="imageColorMode === 'dark' ? 'carbon:moon' : 'carbon:sun'"
                @click="imageColorMode = imageColorMode === 'dark' ? 'light' : 'dark'"
              >
                {{ imageColorMode === 'dark' ? 'Dark' : 'Light' }}
              </UButton>
            </div>

            <div v-if="editableProps.length" class="prop-editor">
              <div v-for="key in editableProps" :key="key" class="prop-field">
                <label class="prop-label">{{ key }}</label>
                <input
                  v-if="inferPropType(propEditor[key]) === 'color'"
                  type="color"
                  :value="propEditor[key]"
                  class="prop-input prop-input-color"
                  @input="editProp(key, ($event.target as HTMLInputElement).value)"
                >
                <input
                  v-else-if="inferPropType(propEditor[key]) === 'number'"
                  type="number"
                  :value="propEditor[key]"
                  class="prop-input"
                  @input="editProp(key, Number(($event.target as HTMLInputElement).value))"
                >
                <textarea
                  v-else-if="inferPropType(propEditor[key]) === 'json'"
                  :value="JSON.stringify(propEditor[key], null, 2)"
                  class="prop-input prop-input-json"
                  rows="3"
                  @change="(() => {
                    try { editProp(key, JSON.parse(($event.target as HTMLTextAreaElement).value)) }
                    catch {
                      // Invalid JSON is expected while editing; keep the previous prop value.
                    }
                  })()"
                />
                <input
                  v-else
                  type="text"
                  :value="propEditor[key]"
                  class="prop-input"
                  @input="editProp(key, ($event.target as HTMLInputElement).value)"
                >
              </div>
            </div>
            <div v-else class="prop-editor-empty">
              No props defined
            </div>
          </div>

          <!-- Fonts Tab -->
          <div v-else-if="protoTab === 'fonts'" class="fonts-tab">
            <!-- Detected requirements — compact summary bar -->
            <div v-if="detectedFontRequirements" class="fonts-detected">
              <div class="fonts-detected-inner">
                <span class="fonts-detected-label">Detected</span>
                <span v-for="w in detectedFontRequirements.weights" :key="`w-${w}`" class="fonts-chip">{{ w }}</span>
                <span v-for="s in detectedFontRequirements.styles" :key="`s-${s}`" class="fonts-chip">{{ s }}</span>
                <template v-if="detectedFontRequirements.families?.length">
                  <span class="fonts-detected-sep" />
                  <span
                    v-for="f in detectedFontRequirements.families"
                    :key="f"
                    class="fonts-chip"
                    :class="unresolvedFamilies.includes(f) ? 'fonts-chip--error' : 'fonts-chip--family'"
                  >
                    <UIcon v-if="unresolvedFamilies.includes(f)" name="carbon:warning-filled" class="w-2.5 h-2.5 shrink-0" />
                    {{ f }}
                  </span>
                </template>
              </div>
            </div>

            <!-- Resolved fonts actually used for rendering -->
            <div v-if="resolvedFamilyNames.length" class="fonts-detected fonts-resolved">
              <div class="fonts-detected-inner">
                <span class="fonts-detected-label">Rendering</span>
                <span v-for="f in resolvedFamilyNames" :key="f" class="fonts-chip fonts-chip--family">{{ f }}</span>
              </div>
            </div>

            <!-- Font specimen list -->
            <div v-if="fontFiles.length" class="fonts-specimen">
              <template v-for="family in fontFamilyNames" :key="family">
                <button
                  class="fonts-family-row"
                  :class="{ active: fontOverride === family }"
                  @click="applyFontOverride(family)"
                >
                  <span class="fonts-family-name" :style="{ fontFamily: `'ogp-${family}', sans-serif` }">{{ family }}</span>
                  <span class="fonts-family-meta">
                    <span v-if="fontFamilySizes.get(family)" class="fonts-family-size">{{ formatBytes(fontFamilySizes.get(family)!) }}</span>
                    <UIcon v-if="fontOverride === family" name="carbon:checkmark-filled" class="fonts-family-check" />
                  </span>
                </button>
                <div class="fonts-variants">
                  <div
                    v-for="f in fontFiles.filter(ff => ff.family === family)"
                    :key="f.key"
                    class="fonts-variant"
                    :class="{ loaded: f.loaded }"
                  >
                    <span class="fonts-variant-dot" />
                    <span class="fonts-variant-label">{{ f.weight }}{{ f.style === 'italic' ? 'i' : '' }}</span>
                    <span v-if="f.subsetCount > 1" class="fonts-variant-count">&times;{{ f.subsetCount }}</span>
                    <template v-if="f.subsets.length">
                      <span v-for="s in f.subsets" :key="s" class="fonts-subset-chip">{{ s }}</span>
                    </template>
                  </div>
                </div>
              </template>
            </div>

            <div v-else class="fonts-empty">
              No fonts resolved. Install <code class="inline-code">@nuxt/fonts</code> to enable.
            </div>
          </div>

          <!-- useSeoMeta snippet for meta tag overrides -->
          <DevtoolsSnippet
            v-if="protoTab === 'meta-tags' && hasMetaOverrides"
            label="useSeoMeta"
            :code="seoMetaSnippet"
            lang="js"
          />

          <UAlert
            v-else-if="hasMadeChanges && protoTab !== 'meta-tags'"
            color="warning"
            variant="subtle"
            icon="carbon:warning"
            title="Unsaved changes"
            description="These changes are for preview only and won't persist."
            class="mx-3 my-2"
          />
        </DevtoolsPanel>
      </Transition>
    </div>
  </div>
</template>

<style scoped>
.preview-container {
  height: calc(100vh - 100px);
  min-height: 500px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.preview-container:hover {
  border-color: var(--color-border);
  box-shadow: none;
}

@media (max-height: 600px) {
  .preview-container {
    height: 100vh;
    min-height: 0;
    border-radius: 0;
    border-left: 0;
    border-right: 0;
  }
}

/* Preview source toggle */
/* Component info */
.component-info {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
}

.component-path-link {
  font-size: 0.8125rem;
  font-family: var(--font-mono, ui-monospace, monospace);
}

/* Size variant bar */
.size-variant-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0.75rem;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-elevated);
}

@media (min-width: 640px) {
  .size-variant-bar {
    padding: 0.375rem 1rem;
  }
}

.size-variant-label {
  font-size: 0.6875rem;
  color: var(--color-text-subtle);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.size-variant-group {
  display: flex;
  gap: 1px;
  background: var(--color-border);
  border-radius: 6px;
  overflow: hidden;
}

.size-variant-btn {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem 0.5rem;
  font-size: 0.6875rem;
  line-height: 1;
  background: var(--color-surface);
  color: var(--color-text-muted);
  border: none;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
  white-space: nowrap;
}

.size-variant-btn:hover {
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.size-variant-btn.active {
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.size-variant-name {
  font-weight: 500;
}

.size-variant-px {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.625rem;
  opacity: 0.6;
}

.size-variant-btn.active .size-variant-px {
  opacity: 0.8;
}

/* Preview area */
.preview-area {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  overflow: auto;
  position: relative;
  transition: padding 200ms cubic-bezier(0.22, 1, 0.36, 1);
}

@media (min-width: 640px) {
  .preview-area {
    padding: 1.5rem;
  }

  .preview-area--panel-open {
    padding-right: 20rem;
  }
}

.preview-content {
  width: 100%;
  max-width: 42rem;
  margin: 0 auto;
  transition: max-width 200ms cubic-bezier(0.22, 1, 0.36, 1);
}

.preview-content--no-transition {
  transition: none;
}

@media (max-height: 600px) {
  .preview-area {
    padding: 0.5rem;
    overflow: hidden;
  }

  .preview-content {
    max-width: 100%;
    /* Scale cards to fit available height — toolbar ~40px, tabs ~36px, padding 16px */
    max-height: calc(100vh - 160px);
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* Constrain all card renderers to fit viewport height */
  .preview-content :deep(img) {
    max-height: calc(100vh - 280px);
    width: auto;
    object-fit: contain;
  }

  .preview-content :deep(.discord-image),
  .preview-content :deep(.facebook-image),
  .preview-content :deep(.linkedin-image),
  .preview-content :deep(.bluesky-image),
  .preview-content :deep(.slack-image) {
    max-height: calc(100vh - 280px);
  }
}

.raw-preview-wrapper {
  position: relative;
  width: 100%;
}

.raw-preview {
  width: 100%;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: 0 4px 24px oklch(0% 0 0 / 0.08);
}

.raw-preview--dragging {
  user-select: none;
}

.dark .raw-preview {
  box-shadow: 0 4px 24px oklch(0% 0 0 / 0.3);
}

/* Resize drag handle — right edge */
.raw-resize-handle {
  position: absolute;
  top: 0;
  right: -14px;
  width: 28px;
  height: 100%;
  cursor: col-resize;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}

.raw-resize-grip {
  width: 4px;
  height: 32px;
  border-radius: 2px;
  background: var(--color-border);
  transition: background 150ms ease, height 150ms ease;
}

.raw-resize-handle:hover .raw-resize-grip,
.raw-preview--dragging ~ .raw-resize-handle .raw-resize-grip {
  background: var(--color-text-subtle);
  height: 48px;
}

/* Width readout pill */
.raw-resize-readout {
  position: absolute;
  bottom: -24px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.625rem;
  color: var(--color-text-subtle);
  white-space: nowrap;
}

/* Status line */
.status-line {
  margin-top: 1rem;
  text-align: center;
  font-size: 0.75rem;
  color: var(--color-text-subtle);
}

@media (max-height: 600px) {
  .status-line {
    margin-top: 0.375rem;
    font-size: 0.6875rem;
  }
}

/* Props panel — positioning only, visuals from DevtoolsPanel */
.props-panel {
  position: absolute;
  right: 0.75rem;
  top: 7rem;
  bottom: 0.75rem;
  width: 18rem;
  z-index: 10;
}

@media (min-width: 640px) {
  .props-panel {
    right: 1rem;
    top: 8rem;
    bottom: 1rem;
  }
}

@media (max-height: 600px) {
  .props-panel {
    top: 5.5rem;
    bottom: 0.25rem;
    right: 0.25rem;
    border-radius: var(--radius-md);
  }
}

.props-field {
  padding: 0.375rem 0.75rem;
}

.props-field-label {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--color-text-muted);
  margin-bottom: 0.25rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* Fonts tab */
.fonts-tab {
  display: flex;
  flex-direction: column;
}

/* Detected requirements — compact summary bar */
.fonts-detected {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--color-border-subtle);
}

.fonts-detected-inner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.fonts-detected-label {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-subtle);
  margin-right: 0.125rem;
}

.fonts-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.1875rem;
  padding: 0.0625rem 0.3125rem;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  line-height: 1.4;
  border-radius: 3px;
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border-subtle);
  color: var(--color-text-muted);
}

.fonts-chip--family {
  color: var(--seo-green);
  border-color: oklch(65% 0.2 145 / 0.2);
  background: oklch(65% 0.2 145 / 0.06);
}

.dark .fonts-chip--family {
  background: oklch(65% 0.2 145 / 0.1);
}

.fonts-chip--error {
  color: oklch(55% 0.2 25);
  border-color: oklch(55% 0.2 25 / 0.25);
  background: oklch(55% 0.2 25 / 0.08);
}

.dark .fonts-chip--error {
  color: oklch(75% 0.15 25);
  background: oklch(55% 0.2 25 / 0.12);
}

.fonts-resolved {
  border-bottom: none;
  padding-top: 0;
}

.fonts-detected + .fonts-resolved {
  padding-top: 0;
}

.fonts-detected-sep {
  width: 1px;
  height: 0.75rem;
  background: var(--color-border);
  margin: 0 0.125rem;
  flex-shrink: 0;
}

/* Font specimen list */
.fonts-specimen {
  display: flex;
  flex-direction: column;
}

.fonts-family-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0.375rem 0.75rem;
  background: transparent;
  border: none;
  cursor: pointer;
  transition: background 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

.fonts-family-row:hover {
  background: var(--color-surface-sunken);
}

.fonts-family-row.active {
  background: oklch(65% 0.2 145 / 0.06);
}

.dark .fonts-family-row.active {
  background: oklch(65% 0.2 145 / 0.08);
}

.fonts-family-name {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text);
  line-height: 1.3;
}

.fonts-family-row.active .fonts-family-name {
  color: var(--seo-green);
}

.fonts-family-meta {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  flex-shrink: 0;
}

.fonts-family-size {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  color: var(--color-text-subtle);
  letter-spacing: 0.01em;
}

.fonts-family-check {
  width: 0.875rem;
  height: 0.875rem;
  color: var(--seo-green);
  flex-shrink: 0;
}

.fonts-variants {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 0.625rem;
  padding: 0.125rem 0.75rem 0.5rem 1rem;
  border-bottom: 1px solid var(--color-border-subtle);
}

.fonts-variant {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
  flex-wrap: wrap;
}

.fonts-variant-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--color-border);
  align-self: center;
}

.fonts-variant.loaded .fonts-variant-dot {
  background: var(--seo-green);
}

.fonts-variant-label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  color: var(--color-text-subtle);
  line-height: 1;
}

.fonts-variant.loaded .fonts-variant-label {
  color: var(--color-text-muted);
}

.fonts-variant-count {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  color: var(--color-text-subtle);
  opacity: 0.7;
}

.fonts-subset-chip {
  font-family: var(--font-mono);
  font-size: 0.5rem;
  line-height: 1;
  padding: 0.0625rem 0.1875rem;
  border-radius: 2px;
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border-subtle);
  color: var(--color-text-subtle);
}

.fonts-variant.loaded .fonts-subset-chip {
  color: var(--color-text-muted);
}

.fonts-empty {
  padding: 1rem 0.75rem;
  font-size: 0.75rem;
  color: var(--color-text-subtle);
  text-align: center;
}

/* Prop editor */
.prop-editor {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  padding: 0.5rem 0.75rem;
}

.prop-field {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.prop-label {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--color-text-subtle);
  letter-spacing: 0.01em;
}

.prop-input {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  padding: 0.25rem 0.375rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border-subtle);
  background: var(--color-surface-sunken);
  color: var(--color-text);
  outline: none;
  transition: border-color 150ms;
}

.prop-input:focus {
  border-color: var(--seo-green);
}

.prop-input-color {
  width: 2.5rem;
  height: 1.5rem;
  padding: 0.125rem;
  cursor: pointer;
}

.prop-input-json {
  resize: vertical;
  min-height: 2.5rem;
  line-height: 1.4;
}

.prop-editor-empty {
  padding: 1rem 0.75rem;
  font-size: 0.75rem;
  color: var(--color-text-subtle);
  text-align: center;
}

/* Inline code */
.inline-code {
  padding: 0.125rem 0.375rem;
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border-subtle);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--seo-green);
}
</style>
