<script setup lang="ts">
/**
 * Visualisation Donut — SVG custom, version maquette « Capture d'écran 2026-05-28 205310 ».
 *
 * Anneaux concentriques sobres (pas de couleurs vives sur les secteurs) :
 *   - Anneau extérieur gris  = "Plafond écologique" (limites planétaires)
 *   - Espace sûr cream       = zone où l'on veut que vivent les projets
 *   - Anneau intérieur gris  = "Plancher social" (besoins fondamentaux)
 *
 * Positionnement des projets :
 *   - Angle : par catégorie (6 secteurs de 60°), jitter angulaire
 *   - Rayon : déterminé par la DIMENSION
 *       social      -> bande intérieure (proche du plancher)
 *       ecologique  -> bande extérieure (proche du plafond)
 *       transversal -> milieu de la zone sûre
 *     puis modulé par le PROGRESS (plus progressé -> plus éloigné du centre,
 *     dans les limites de la bande de la dimension)
 *   - Projets archivés rendus en POINTS CREUX (stroke only).
 */

import { ref, computed, onMounted } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { api, ApiError } from '@/api/client'
import type { Category, Dimension, Paginated, ProjectListRow, Wrapped } from '@/types/api'
import LoadingState from '@/components/LoadingState.vue'
import ErrorState from '@/components/ErrorState.vue'

const router = useRouter()
const categories = ref<Category[]>([])
const projects = ref<ProjectListRow[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const hoveredProject = ref<ProjectListRow | null>(null)

async function load() {
  loading.value = true
  error.value = null
  try {
    // /api/projects sans filtre renvoie déjà tous les non soft-deletés
    // (active + completed + archived) — utile pour les points creux des archives.
    const [cats, projs] = await Promise.all([
      api.get<Wrapped<Category>>('/api/categories'),
      api.get<Paginated<ProjectListRow>>('/api/projects', { query: { per_page: 100 } }),
    ])
    categories.value = [...cats.data].sort((a, b) => a.sort_order - b.sort_order)
    projects.value = projs.data
  } catch (e) {
    error.value = e instanceof ApiError ? (e.payload.message ?? e.code) : 'Erreur réseau'
  } finally {
    loading.value = false
  }
}
onMounted(load)

// --- Géométrie (en unités du viewBox) ---
//
// Les anneaux du plancher social et leurs labels ont été agrandis pour rester
// lisibles : circonférence ≈ 2π × R, donc plus R est grand, plus chaque label
// d'arc dispose de place. Le centre et le ring extérieur ne bougent pas, c'est
// uniquement la zone intérieure qui grossit, en mangeant un peu sur l'espace sûr.
const R_PLANET_LABEL  = 252  // étiquettes des limites planétaires (extérieur)
const R_CAT_LABEL     = 212  // noms de catégories EN ARC, sur l'anneau gris du plafond (intermédiaire)
const R_CEILING_OUTER = 225
const R_CEILING_INNER = 200
const R_FLOOR_OUTER   = 125
const R_FLOOR_INNER   = 102
const R_SOCIAL_LABEL  = 88   // arc de label = 2π·88/12 ≈ 46u
const R_CENTER        = 55

/**
 * Format : chaque entrée est un tableau de string. Un tableau à 1 élément →
 * libellé sur une ligne. À 2 éléments → ligne 1 (primaire) + ligne 2 (suite).
 * On garde un type uniforme pour simplifier le helper qui les rend.
 */
type Label = readonly string[]

/** Les 9 limites planétaires de Kate Raworth, placées autour de l'anneau extérieur. */
const PLANETARY_BOUNDARIES: readonly Label[] = [
  ['Changement climatique'],
  ['Acidification des océans'],
  ['Pollution chimique'],
  ['Cycles azote & phosphore'],
  ['Usage des sols'],
  ['Eau douce'],
  ['Biodiversité'],
  ["Pollution de l'air"],
  ["Couche d'ozone"],
]

/** Les 12 dimensions du plancher social de Raworth. Beaucoup sont sur 2 lignes
 *  car leur largeur dépasse l'arc disponible à R_SOCIAL_LABEL. */
const SOCIAL_FOUNDATIONS: readonly Label[] = [
  ['Alimentation'],
  ['Santé'],
  ['Éducation'],
  ['Revenu',  'travail'],
  ['Eau'],
  ['Énergie'],
  ['Liens',   'sociaux'],
  ['Logement'],
  ['Égalité', 'de genre'],
  ['Équité',  'sociale'],
  ['Voix',    'politique'],
  ['Paix',    'justice'],
]

interface ArcLine {
  pathId: string
  d: string
  text: string
}

/**
 * Génère pour chaque libellé un (ou plusieurs) petit arc(s) de cercle.
 * Le texte épouse alors la courbure de l'anneau via <textPath>.
 *
 * Lisibilité haut/bas : on inverse le sens du path (CW vs CCW) selon la moitié
 * du cercle. Sans ça, les labels du bas seraient écrits à l'envers.
 *
 *  - Moitié haute : path va de θ-δ à θ+δ, sweep=1 (CW visuellement)
 *    → direction tangente vers la droite au sommet, "above the path" = OUTSIDE
 *  - Moitié basse : path va de θ+δ à θ-δ, sweep=0 (CCW visuellement)
 *    → direction tangente vers la droite au bas (du POV du lecteur fixe en face)
 *
 * Multi-lignes : chaque ligne a son propre arc à un rayon décalé. Pour qu'une
 * ligne donnée apparaisse toujours "au-dessus" visuellement (que ce soit en
 * haut ou en bas du donut), on inverse aussi le décalage radial selon la moitié :
 *
 *  - En haut, ligne 0 = R + offset (loin du centre = haut de l'écran)
 *  - En bas,  ligne 0 = R - offset (proche du centre = haut de l'écran)
 *
 * @param labels       Liste des libellés (chacun = tableau de lignes)
 * @param radius       Rayon central de l'arc
 * @param idPrefix     Préfixe pour les `id` des paths (unique en DOM)
 * @param halfSpanDeg  Demi-largeur angulaire de chaque label
 * @param lineSpacing  Espacement radial entre 2 lignes (en unités viewBox)
 */
function arcLabelPaths(
  labels: readonly Label[],
  radius: number,
  idPrefix: string,
  halfSpanDeg: number,
  lineSpacing = 9,
): Array<{ key: number; lines: ArcLine[] }> {
  const n = labels.length
  const stepDeg = 360 / n
  return labels.map((entry, i) => {
    const centerDeg = i * stepDeg + stepDeg / 2
    const startDeg = centerDeg - halfSpanDeg
    const endDeg = centerDeg + halfSpanDeg

    const normalized = ((centerDeg % 360) + 360) % 360
    const isTopHalf = normalized <= 90 || normalized >= 270

    const fromDeg = isTopHalf ? startDeg : endDeg
    const toDeg   = isTopHalf ? endDeg   : startDeg
    const sweep   = isTopHalf ? 1 : 0

    const nLines = entry.length
    const halfSpan = (nLines - 1) / 2

    const lines: ArcLine[] = entry.map((text, lineIdx) => {
      // offsetIdx : +halfSpan pour la ligne du haut visuel, -halfSpan pour la ligne du bas
      const offsetIdx = isTopHalf
        ? halfSpan - lineIdx       // top : ligne 0 au plus éloigné du centre
        : lineIdx - halfSpan       // bot : ligne 0 au plus proche du centre
      const r = radius + offsetIdx * lineSpacing

      const [x1, y1] = pt(fromDeg, r)
      const [x2, y2] = pt(toDeg, r)
      return {
        pathId: `${idPrefix}-${i}-${lineIdx}`,
        d: `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r.toFixed(2)} ${r.toFixed(2)} 0 0 ${sweep} ${x2.toFixed(2)} ${y2.toFixed(2)}`,
        text,
      }
    })

    return { key: i, lines }
  })
}

/** deg → rad avec offset -90° (0° en haut). */
function rad(deg: number): number {
  return (deg - 90) * Math.PI / 180
}
function pt(angleDeg: number, r: number): [number, number] {
  const a = rad(angleDeg)
  return [Math.cos(a) * r, Math.sin(a) * r]
}

/** Hash 32-bit basique pour stabiliser le jitter par projet. */
function strHash(s: string): number {
  let h = 0
  for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0
  return Math.abs(h)
}

/** Construit le path SVG d'un anneau-secteur (donut wedge) entre 2 rayons. */
function wedgePath(startDeg: number, endDeg: number, rInner: number, rOuter: number): string {
  const [x1, y1] = pt(startDeg, rOuter)
  const [x2, y2] = pt(endDeg, rOuter)
  const [x3, y3] = pt(endDeg, rInner)
  const [x4, y4] = pt(startDeg, rInner)
  const largeArc = endDeg - startDeg > 180 ? 1 : 0
  return [
    `M ${x1.toFixed(2)} ${y1.toFixed(2)}`,
    `A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`,
    `L ${x3.toFixed(2)} ${y3.toFixed(2)}`,
    `A ${rInner} ${rInner} 0 ${largeArc} 0 ${x4.toFixed(2)} ${y4.toFixed(2)}`,
    'Z',
  ].join(' ')
}

/** Secteurs colorés (tint subtil) pour chaque catégorie, dans la zone sûre. */
const thematicWedges = computed(() => {
  const n = categories.value.length || 6
  const step = 360 / n
  return categories.value.map((cat, i) => ({
    cat,
    path: wedgePath(i * step, (i + 1) * step, R_FLOOR_OUTER, R_CEILING_INNER),
    key: cat.id,
  }))
})

/** Lignes radiales de séparation des secteurs (entre plancher et plafond). */
const wedgeDividers = computed(() => {
  const n = categories.value.length || 6
  const step = 360 / n
  return Array.from({ length: n }, (_, i) => {
    const angle = i * step
    const [x1, y1] = pt(angle, R_FLOOR_OUTER)
    const [x2, y2] = pt(angle, R_CEILING_INNER)
    return { x1, y1, x2, y2, key: i }
  })
})

/** Arcs des noms de catégories, tout à l'extérieur du donut, dans la couleur de la catégorie.
 *  6 catégories × 60° d'arc chacune ; halfSpan=28° laisse une petite marge avant la suivante. */
const categoryArcs = computed(() =>
  arcLabelPaths(categories.value.map(c => [c.name] as Label), R_CAT_LABEL, 'cat', 28),
)
const categoryArcLines = computed(() => categoryArcs.value.flatMap(c => c.lines))

/** Paths d'arc pour les 9 limites planétaires (anneau extérieur). */
const planetaryArcs = computed(() => arcLabelPaths(PLANETARY_BOUNDARIES, R_PLANET_LABEL, 'pb', 18))

/** Paths d'arc pour les 12 dimensions du plancher social (anneau intérieur). */
const socialArcs = computed(() => arcLabelPaths(SOCIAL_FOUNDATIONS, R_SOCIAL_LABEL, 'sf', 14))

/** Aplatissement (label × ligne) pour rendre proprement en flat-list dans le SVG. */
const planetaryArcLines = computed(() => planetaryArcs.value.flatMap(l => l.lines))
const socialArcLines    = computed(() => socialArcs.value.flatMap(l => l.lines))

interface DotView {
  project: ProjectListRow
  x: number
  y: number
  isHollow: boolean
  color: string
}

const dots = computed<DotView[]>(() => {
  if (categories.value.length === 0) return []
  // index catégorie par slug -> position du secteur (0..5)
  const catIndex = new Map<string, number>()
  categories.value.forEach((c, i) => catIndex.set(c.slug, i))
  const n = categories.value.length
  const step = 360 / n

  // Bands selon la dimension (zone sûre = R_FLOOR_OUTER..R_CEILING_INNER, soit 75u de large).
  // Recalibrées pour le nouveau dimensionnement (floor agrandi).
  const BANDS: Record<Dimension, [number, number]> = {
    social:      [R_FLOOR_OUTER + 5,  R_FLOOR_OUTER + 23],     // 130..148  (proche du plancher)
    transversal: [R_FLOOR_OUTER + 26, R_CEILING_INNER - 26],   // 151..174  (au milieu)
    ecologique:  [R_CEILING_INNER - 23, R_CEILING_INNER - 5],  // 177..195  (proche du plafond)
  }

  // Groupage par catégorie pour répartir angulairement.
  const byCat = new Map<string, ProjectListRow[]>()
  for (const p of projects.value) {
    const list = byCat.get(p.category.slug) ?? []
    list.push(p)
    byCat.set(p.category.slug, list)
  }

  const out: DotView[] = []
  for (const [slug, list] of byCat.entries()) {
    const i = catIndex.get(slug) ?? 0
    const wedgeStart = i * step + 3
    const wedgeEnd   = (i + 1) * step - 3
    const span = wedgeEnd - wedgeStart

    list.forEach((p, idx) => {
      // Angle : répartition + jitter stable
      const t = (idx + 1) / (list.length + 1)
      const angleJitter = ((strHash(p.slug + 'a') % 100) / 100 - 0.5) * Math.min(6, span / Math.max(2, list.length))
      const angle = wedgeStart + span * t + angleJitter

      // Rayon : bande de dimension + progress
      const [rMin, rMax] = BANDS[p.category.dimension] ?? BANDS.transversal
      const progressR = rMin + (rMax - rMin) * (p.progress_percent / 100)
      const rJitter = ((strHash(p.slug + 'r') % 100) / 100 - 0.5) * 4
      const r = Math.max(rMin, Math.min(rMax, progressR + rJitter))

      const [x, y] = pt(angle, r)
      out.push({
        project: p,
        x, y,
        isHollow: p.status === 'archived',
        color: p.category.color_hex,
      })
    })
  }
  return out
})

// Légende — catégories regroupées par dimension avec leur count.
const projectsByCategorySlug = computed(() => {
  const m: Record<string, number> = {}
  for (const p of projects.value) {
    m[p.category.slug] = (m[p.category.slug] ?? 0) + 1
  }
  return m
})

const DIMENSION_ORDER: Dimension[] = ['social', 'ecologique', 'transversal']
const DIMENSION_META: Record<Dimension, { label: string; color: string }> = {
  social:      { label: 'Plancher social',    color: '#F97316' },
  ecologique:  { label: 'Plafond écologique', color: '#22C55E' },
  transversal: { label: 'Transversal',        color: '#6B7280' },
}

const categoriesByDimension = computed(() => {
  const map: Record<Dimension, Category[]> = { social: [], ecologique: [], transversal: [] }
  for (const c of categories.value) map[c.dimension].push(c)
  return map
})

function gotoProject(p: ProjectListRow) {
  router.push({ name: 'project-detail', params: { slug: p.slug } })
}
</script>

<template>
  <section class="px-10 py-8 max-w-7xl">
    <header class="text-center max-w-3xl mx-auto">
      <h1 class="text-3xl font-semibold text-donut-text">Le Donut des Projets Citoyens</h1>
      <p class="text-donut-muted mt-2">
        Inspiré du modèle de l'économie du Donut de Kate Raworth. L'Infolab des projets citoyens
        explore les initiatives qui poussent une vie marseillaise dans l'espace sûr et juste : entre
        plancher social et plafond écologique.
      </p>
    </header>

    <LoadingState v-if="loading" class="mt-12 justify-center" />
    <ErrorState v-else-if="error" :message="error" :retry="load" class="mt-12" />

    <div v-else class="mt-10 grid grid-cols-1 lg:grid-cols-5 gap-10 items-start">
      <!-- Donut SVG (3/5 sur grands écrans) -->
      <div class="lg:col-span-3 flex flex-col items-center">
        <svg viewBox="-280 -280 560 560" class="w-full max-w-[640px] aspect-square">
          <defs>
            <!-- Anneaux gris légèrement striés -->
            <pattern id="ring-stripes" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
              <rect width="6" height="6" fill="#ECECE7" />
              <line x1="0" y1="0" x2="0" y2="6" stroke="#D1D5DB" stroke-width="0.9" />
            </pattern>
            <!-- Arcs invisibles pour textPath : 1 par catégorie (extérieur), 1 par limite
                 planétaire, 1 à 2 par dimension sociale selon la longueur. -->
            <path
              v-for="ln in categoryArcLines" :key="ln.pathId"
              :id="ln.pathId" :d="ln.d" fill="none"
            />
            <path
              v-for="ln in planetaryArcLines" :key="ln.pathId"
              :id="ln.pathId" :d="ln.d" fill="none"
            />
            <path
              v-for="ln in socialArcLines" :key="ln.pathId"
              :id="ln.pathId" :d="ln.d" fill="none"
            />
          </defs>

          <!-- 1) ZONE D'OVERSHOOT (au-delà du plafond) -->
          <!--    fond cream clair pour soutenir les labels limites planétaires -->
          <circle r="270" fill="#F5F2EA" />

          <!-- 2) Étiquettes des 9 LIMITES PLANÉTAIRES, le long de leur arc -->
          <g>
            <text
              v-for="ln in planetaryArcLines" :key="`pb-text-${ln.pathId}`"
              font-size="8" fill="#16A34A" font-weight="600"
            >
              <textPath :href="`#${ln.pathId}`" startOffset="50%" text-anchor="middle">
                {{ ln.text }}
              </textPath>
            </text>
          </g>

          <!-- 3) Anneau extérieur PLAFOND ÉCOLOGIQUE (gris strié) -->
          <circle :r="R_CEILING_OUTER" fill="url(#ring-stripes)" />
          <circle :r="R_CEILING_INNER" fill="#FAFAF6" />

          <!-- 4) Secteurs thématiques colorés (tint subtil, sur la zone sûre) -->
          <g>
            <path
              v-for="w in thematicWedges" :key="w.key"
              :d="w.path"
              :fill="w.cat.color_hex + '1A'"
              :stroke="w.cat.color_hex + '33'"
              stroke-width="0.5"
            />
          </g>

          <!-- 5) Anneau intérieur PLANCHER SOCIAL (gris strié) -->
          <circle :r="R_FLOOR_OUTER" fill="url(#ring-stripes)" />
          <circle :r="R_FLOOR_INNER" fill="#FAFAF6" />

          <!-- 6) Séparateurs radiaux entre secteurs -->
          <g stroke="#FFFFFF" stroke-width="1.5">
            <line
              v-for="d in wedgeDividers" :key="d.key"
              :x1="d.x1" :y1="d.y1" :x2="d.x2" :y2="d.y2"
            />
          </g>

          <!-- 7) Noms des CATÉGORIES en arc, SUR L'ANNEAU GRIS DU PLAFOND (entre wedges et
                  limites planétaires), en couleur catégorie pour contraster avec le pattern. -->
          <g>
            <text
              v-for="arc in categoryArcs" :key="`cat-text-${arc.key}`"
              font-size="10"
              font-weight="700"
              :fill="categories[arc.key]?.color_hex ?? '#111827'"
              style="letter-spacing: 0.5px; paint-order: stroke; stroke: #FAFAF6; stroke-width: 2.5; stroke-linejoin: round"
            >
              <textPath :href="`#${arc.lines[0].pathId}`" startOffset="50%" text-anchor="middle">
                {{ arc.lines[0].text }}
              </textPath>
            </text>
          </g>

          <!-- 8) Étiquettes des 12 DIMENSIONS DU PLANCHER SOCIAL, le long de leur arc -->
          <!--    Libellés longs (« Revenu / travail », « Égalité / de genre », …) sur 2 lignes
                  radiales : chaque ligne a son propre arc, lineSpacing=9u du helper. -->
          <g>
            <text
              v-for="ln in socialArcLines" :key="`sf-text-${ln.pathId}`"
              font-size="8" fill="#EA580C" font-weight="600"
            >
              <textPath :href="`#${ln.pathId}`" startOffset="50%" text-anchor="middle">
                {{ ln.text }}
              </textPath>
            </text>
          </g>

          <!-- 9) Points-projets : la classe `dot-circle` corrige le transform-origin -->
          <g>
            <circle
              v-for="d in dots" :key="d.project.id"
              :cx="d.x" :cy="d.y" r="6"
              :fill="d.isHollow ? '#FAFAF6' : d.color"
              :stroke="d.color"
              :stroke-width="d.isHollow ? 2 : 1.5"
              class="dot-circle"
              :class="hoveredProject?.id === d.project.id ? 'is-hover' : ''"
              @mouseenter="hoveredProject = d.project"
              @mouseleave="hoveredProject = null"
              @click="gotoProject(d.project)"
            >
              <title>{{ d.project.title }} — {{ d.project.progress_percent }}%</title>
            </circle>
          </g>

          <!-- 10) Centre -->
          <circle :r="R_CENTER" fill="#FFFFFF" stroke="#E5E7EB" />
          <text x="0" y="-4" text-anchor="middle" font-size="13" fill="#111827" font-weight="700"
                style="letter-spacing:2px">LE DONUT</text>
          <text x="0" y="12" text-anchor="middle" font-size="9" fill="#6B7280">
            {{ projects.length }} projet{{ projects.length > 1 ? 's' : '' }}
          </text>
        </svg>

        <!-- Tooltip projet survolé -->
        <div class="mt-2 h-12 flex items-center justify-center">
          <transition name="fade">
            <RouterLink
              v-if="hoveredProject"
              :to="{ name: 'project-detail', params: { slug: hoveredProject.slug } }"
              class="px-3 py-1.5 rounded-full bg-white border border-donut-border shadow-sm text-sm flex items-center gap-2 hover:shadow-md transition-shadow"
            >
              <span
                class="w-2 h-2 rounded-full"
                :style="{ background: hoveredProject.category.color_hex }"
              />
              <span class="font-medium">{{ hoveredProject.title }}</span>
              <span class="text-donut-muted text-xs">
                — {{ hoveredProject.progress_percent }}%
                <template v-if="hoveredProject.status === 'archived'"> · archivé</template>
                <template v-else-if="hoveredProject.status === 'completed'"> · réalisé</template>
              </span>
            </RouterLink>
            <div v-else class="text-xs text-donut-muted">
              Survolez un point pour voir le projet · cliquez pour ouvrir
            </div>
          </transition>
        </div>
      </div>

      <!-- Légende -->
      <aside class="lg:col-span-2 space-y-6">
        <div class="rounded-xl border border-donut-border bg-donut-surface p-5">
          <h2 class="text-base font-semibold text-donut-text">Comment lire ce donut ?</h2>
          <ul class="mt-3 space-y-2.5 text-sm text-donut-muted leading-relaxed">
            <li>L'<strong class="text-donut-text">anneau intérieur</strong> représente le
              <span class="font-medium" style="color:#F97316">plancher social</span>, qui définit les besoins fondamentaux.</li>
            <li>L'<strong class="text-donut-text">anneau extérieur</strong> représente le
              <span class="font-medium" style="color:#22C55E">plafond écologique</span>, qui définit les limites planétaires.</li>
            <li>L'espace entre les deux est l'<strong class="text-donut-text">espace sûr et juste</strong>.</li>
            <li>Les <strong class="text-donut-text">projets sociaux</strong> sont positionnés vers l'intérieur.</li>
            <li>Les <strong class="text-donut-text">projets écologiques</strong> sont positionnés vers l'extérieur.</li>
            <li>Plus un point est <strong class="text-donut-text">éloigné du centre</strong>, plus le projet est progressé.</li>
            <li>Les <strong class="text-donut-text">points creux</strong> sont les projets historiques.</li>
          </ul>
        </div>

        <div
          v-for="dim in DIMENSION_ORDER"
          :key="dim"
          class="rounded-xl border border-donut-border bg-donut-surface p-5"
        >
          <div class="flex items-center gap-2 mb-3">
            <span class="w-2.5 h-2.5 rounded-full" :style="{ background: DIMENSION_META[dim].color }" />
            <h3 class="text-xs font-semibold uppercase tracking-wider" :style="{ color: DIMENSION_META[dim].color }">
              {{ DIMENSION_META[dim].label }}
            </h3>
          </div>
          <ul v-if="categoriesByDimension[dim].length" class="space-y-2">
            <li
              v-for="c in categoriesByDimension[dim]"
              :key="c.id"
            >
              <RouterLink
                :to="{ name: 'projects', query: { category: c.slug } }"
                class="flex items-center justify-between gap-2 text-sm group"
              >
                <span class="flex items-center gap-2 min-w-0">
                  <span class="w-2.5 h-2.5 rounded-full shrink-0" :style="{ background: c.color_hex }" />
                  <span class="text-donut-text truncate group-hover:underline">{{ c.name }}</span>
                </span>
                <span class="text-donut-muted tabular-nums shrink-0">
                  {{ projectsByCategorySlug[c.slug] ?? 0 }}
                </span>
              </RouterLink>
            </li>
          </ul>
          <p v-else class="text-xs text-donut-muted">Aucune catégorie dans cette dimension.</p>
        </div>
      </aside>
    </div>
  </section>
</template>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.15s ease;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}

/* Fix anti-jitter sur les points-projets.
 *
 * Sans ces 2 propriétés, `transform: scale()` en SVG prend pour origine le (0,0)
 * du viewBox -> le cercle scalé est déplacé vers le centre, la souris n'est plus
 * dessus, mouseleave -> retour à la position d'origine -> mouseenter -> re-déplacé.
 * Boucle infinie de jitter.
 *
 * `transform-box: fill-box` change le référentiel à la bounding box de la forme,
 * et `transform-origin: center` met l'origine au centre de cette box.
 * Le scale se fait alors "sur place".
 */
.dot-circle {
  cursor: pointer;
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 0.15s ease;
}
.dot-circle:hover,
.dot-circle.is-hover {
  transform: scale(1.6);
}
</style>
