<template>
  <div v-if="commune">
    <!-- ── 1. Header ── -->
    <div class="page-header">
      <NuxtLink to="/" style="color: var(--color-text-muted); font-size: 0.875rem;">&larr; Toutes les communes</NuxtLink>
      <h1>{{ commune.name }}</h1>
      <p style="color: var(--color-text-muted);">{{ commune.description }}</p>
    </div>

    <!-- Loading -->
    <div v-if="loading" class="card" style="text-align: center; padding: 3rem;">
      <div class="spinner" style="margin: 0 auto;"></div>
      <p style="margin-top: 1rem; color: var(--color-text-muted);">Chargement...</p>
    </div>

    <template v-else-if="curveData">

      <!-- ═══════════════════════════════════════════════════════
           COURBE INTERACTIVE + SIDEBAR AUTH/VOTE
           ═══════════════════════════════════════════════════════ -->
      <div class="main-layout">
        <!-- ── LEFT: interactive chart ── -->
        <div class="main-layout-chart">
          <div class="chart-card">
            <div class="chart-header">
              <div>
                <h3 class="chart-title">Tarification progressive — Prix au m<sup>3</sup></h3>
                <p class="chart-subtitle">
                  Deplacez les poignees pour ajuster la courbe. Le prix au palier s'ajuste pour equilibrer les recettes.
                  La barre verte indique le prix median — deplacez-la pour changer les recettes cibles.
                </p>
                <p v-if="differentiatedTariff" class="chart-subtitle" style="margin-top: 0.15rem; font-style: italic;">
                  Hors abonnement. L'abonnement differe selon le statut : {{ abop }} € (RP/PRO) / {{ abos }} € (RS).
                </p>
              </div>
              <div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
                <label class="toggle-label">
                  <input type="checkbox" v-model="showOutliers" />
                  Courbes des votes
                </label>
                <label class="toggle-label">
                  <input type="checkbox" v-model="showHouseholds" />
                  Voir les foyers
                </label>
                <span v-if="curveData.has_votes" class="badge badge-green">
                  {{ curveData.vote_count }} vote(s)
                </span>
                <span v-else class="badge badge-amber">Courbe par defaut</span>
              </div>
            </div>

            <!-- Zoom controls + abonnement -->
            <div class="zoom-bar">
              <button class="btn btn-secondary btn-xs" @click="zoomPreset('tier1')" title="Zoom population">Population</button>
              <button class="btn btn-secondary btn-xs" @click="zoomPreset('tier2')" title="Zoom cas exceptionnels">Cas exceptionnels</button>
              <button class="btn btn-secondary btn-xs" @click="zoomIn">+</button>
              <button class="btn btn-secondary btn-xs" @click="zoomOut">&minus;</button>
              <button class="btn btn-secondary btn-xs" @click="zoomReset">Reset</button>
              <span class="zoom-info">
                {{ zoomVolMin.toFixed(0) }}–{{ zoomVolMax.toFixed(0) }} m³
              </span>
              <span class="zoom-separator"></span>
              <label class="abo-input-label">
                Abonnement
                <input type="number" v-model.number="citizenAbo" min="0" step="10" class="abo-input" /> €
              </label>
              <span class="zoom-separator"></span>
              <span class="zoom-info" style="font-weight: 500;" :style="{ color: t.textCount }">
                Recettes : {{ Math.round(recettes).toLocaleString() }} €
              </span>
            </div>

        <!-- SVG: full-width, X axis inverted (high vol left, 0 right) -->
        <div class="chart-container chart-main">
          <svg
            ref="svgRef"
            :viewBox="`0 0 ${W} ${H}`"
            preserveAspectRatio="xMidYMid meet"
            @mousemove="onMouseMove"
            @mouseup="onMouseUp"
            @mouseleave="onMouseUp"
            @touchmove.prevent="onTouchMove"
            @touchend="onMouseUp"
            @wheel.prevent="onWheel"
          >
            <!-- Clip path for plot area -->
            <defs>
              <clipPath id="plot-clip">
                <rect :x="margin.left" :y="margin.top" :width="plotW" :height="plotH" />
              </clipPath>
            </defs>

            <!-- Background -->
            <rect :x="margin.left" :y="margin.top" :width="plotW" :height="plotH"
              :fill="t.plotBg" rx="4" />

            <!-- Grid -->
            <g>
              <line v-for="v in gridVols" :key="'gv'+v"
                :x1="cx(v)" :y1="margin.top" :x2="cx(v)" :y2="margin.top + plotH"
                :stroke="t.grid" stroke-width="0.5" />
              <line v-for="p in gridPrices" :key="'gp'+p"
                :x1="margin.left" :y1="cy(p)" :x2="margin.left + plotW" :y2="cy(p)"
                :stroke="t.grid" stroke-width="0.5" />
              <!-- Volume labels (bottom) -->
              <text v-for="v in gridVols" :key="'lv'+v"
                :x="cx(v)" :y="margin.top + plotH + 16" text-anchor="middle"
                class="axis-label">
                {{ v }} m³
              </text>
              <!-- Household counts per 30m³ bracket below axis -->
              <g v-if="distBuckets30.length">
                <text v-for="bk in visibleBuckets30Main" :key="'hc'+bk.low"
                  :x="(cx(bk.low) + cx(bk.high)) / 2"
                  :y="margin.top + plotH + 32"
                  text-anchor="middle" font-size="9.5" :fill="t.textMuted" font-weight="500">
                  {{ bk.count }}
                </text>
                <text :x="margin.left + plotW + 6" :y="margin.top + plotH + 32"
                  text-anchor="start" font-size="8.5" :fill="t.textMuted">foyers/30m³</text>
              </g>
              <!-- Price labels (RIGHT side, since Y axis is on right at vol=0) -->
              <text v-for="p in gridPrices" :key="'lp'+p"
                :x="margin.left + plotW + 6" :y="cy(p) + 4" text-anchor="start"
                class="axis-label">
                {{ p.toFixed(p < 1 ? 1 : 0) }}€
              </text>
              <!-- Axis titles -->
              <text :x="W / 2" :y="margin.top + plotH + 48" text-anchor="middle" class="axis-title">
                Consommation (m³) → reduire
              </text>
              <text :x="W - 4" :y="margin.top - 6" text-anchor="end" class="axis-title">
                €/m³
              </text>
            </g>

            <!-- Clipped plot content -->
            <g clip-path="url(#plot-clip)">
              <!-- Outlier vote curves (faisceau) -->
              <g v-if="showOutliers && outlierVotes.length">
                <template v-for="(vote, i) in outlierVotes" :key="'ov'+i">
                  <path :d="outlierPath(vote, 1)" fill="none" stroke="#3b82f6" stroke-width="2" opacity="0.45" />
                  <path :d="outlierPath(vote, 2)" fill="none" stroke="#f87171" stroke-width="2" opacity="0.45" />
                </template>
              </g>

              <!-- Tangent lines (control arms) -->
              <line :x1="cx(cp.p1.x)" :y1="cy(cp.p1.y)" :x2="cx(cp.p2.x)" :y2="cy(cp.p2.y)"
                stroke="#93c5fd" stroke-width="1" stroke-dasharray="6 3" opacity="0.6" />
              <line :x1="cx(cp.p3.x)" :y1="cy(cp.p3.y)" :x2="cx(cp.p4.x)" :y2="cy(cp.p4.y)"
                stroke="#93c5fd" stroke-width="1" stroke-dasharray="6 3" opacity="0.6" />
              <line :x1="cx(cp.p4.x)" :y1="cy(cp.p4.y)" :x2="cx(cp.p5.x)" :y2="cy(cp.p5.y)"
                stroke="#fca5a5" stroke-width="1" stroke-dasharray="6 3" opacity="0.6" />
              <line :x1="cx(cp.p6.x)" :y1="cy(cp.p6.y)" :x2="cx(cp.p7.x)" :y2="cy(cp.p7.y)"
                stroke="#fca5a5" stroke-width="1" stroke-dasharray="6 3" opacity="0.6" />

              <!-- Prix moyen avec abonnement (abo/v + marginal) — dashed green -->
              <polyline v-if="citizenAbo > 0" :points="avgPriceWithAboPath" fill="none"
                stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-dasharray="6 4" opacity="0.8" />

              <!-- Bezier curve: population (blue gradient, thick) -->
              <path :d="tier1Path" fill="none"
                :stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#2563eb'"
                :stroke-width="showHouseholds ? 2 : 3.5" stroke-linecap="round" />
              <!-- Bezier curve: cas exceptionnels (orange) -->
              <path :d="tier2Path" fill="none"
                :stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#ea580c'"
                :stroke-width="showHouseholds ? 2 : 2.5" stroke-linecap="round" />

              <!-- Household dots on curves -->
              <g v-if="showHouseholds && householdVolumes.length">
                <circle v-for="(hh, i) in householdDots" :key="'hd'+i"
                  :cx="cx(hh.volume)" :cy="cy(hh.price)"
                  r="3.5"
                  :fill="hh.volume <= bp.vinf ? '#2563eb' : '#ea580c'"
                  :opacity="0.65"
                  :stroke="t.plotBg" stroke-width="0.8"
                />
              </g>

              <!-- Inflection reference lines -->
              <line :x1="cx(bp.vinf)" :y1="margin.top + plotH" :x2="cx(bp.vinf)" :y2="cy(localP0)"
                stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 4" />
              <line :x1="cx(bp.vinf)" :y1="cy(localP0)" :x2="margin.left + plotW" :y2="cy(localP0)"
                stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 4" />

              <!-- p0 label (pill style) -->
              <rect :x="cx(bp.vinf) + 8" :y="cy(localP0) - 18" width="180" height="20" rx="10" :fill="t.pillBg" :stroke="t.pillBorder" />
              <text :x="cx(bp.vinf) + 98" :y="cy(localP0) - 5" text-anchor="middle" font-size="10.5" :fill="t.pillText" font-weight="600">
                Prix au palier : {{ localP0.toFixed(2) }} €/m³
              </text>

              <!-- Median marginal price bar (draggable) -->
              <line :x1="margin.left" :y1="cy(medianPrice)" :x2="margin.left + plotW" :y2="cy(medianPrice)"
                stroke="#059669" stroke-width="1.5" stroke-dasharray="8 4" opacity="0.7" />
              <rect :x="margin.left + plotW - 178" :y="cy(medianPrice) - 16" width="170" height="18" rx="9"
                fill="#059669" fill-opacity="0.12" stroke="#059669" stroke-width="0.5" />
              <text :x="margin.left + plotW - 93" :y="cy(medianPrice) - 4" text-anchor="middle"
                font-size="10" fill="#059669" font-weight="600">
                Prix median : {{ medianPrice.toFixed(2) }} €/m³
              </text>
            </g>

            <!-- Median bar drag handle (outside clip so always visible) -->
            <circle
              :cx="margin.left + 14" :cy="cy(medianPrice)"
              :r="dragging === 'medianBar' ? 10 : 7"
              fill="#059669" :stroke="t.plotBg" :stroke-width="dragging === 'medianBar' ? 3 : 2"
              class="drag-handle"
              @mousedown.prevent="startDrag('medianBar')"
              @touchstart.prevent="startDrag('medianBar')"
            />
            <text :x="margin.left + 28" :y="cy(medianPrice) + 4"
              font-size="9" fill="#059669" font-weight="600" opacity="0.7">
              ↕
            </text>

            <!-- Draggable control points -->
            <circle v-for="(pt, key) in dragPoints" :key="key"
              :cx="cx(pt.x)" :cy="cy(pt.y)"
              :r="dragging === key ? 10 : 7"
              :fill="ptColors[key]" :stroke="t.plotBg" :stroke-width="dragging === key ? 3 : 2"
              class="drag-handle"
              @mousedown.prevent="startDrag(key)"
              @touchstart.prevent="startDrag(key)"
            />
            <text v-for="(pt, key) in dragPoints" :key="'l-'+key"
              :x="cx(pt.x) + 12" :y="cy(pt.y) - 12"
              font-size="10" :fill="ptColors[key]" font-weight="600" opacity="0.8">
              {{ ptLabels[key] }}
            </text>

            <!-- Legend box (top-right) -->
            <g :transform="`translate(${margin.left + 8}, ${margin.top + plotH - 90})`">
              <rect x="0" y="0" width="220" :height="citizenAbo > 0 ? 80 : 62" rx="6" :fill="t.legendBg" fill-opacity="0.92" :stroke="t.legendBorder" />
              <line x1="10" y1="14" x2="28" y2="14" stroke="#2563eb" stroke-width="3" stroke-linecap="round" />
              <text x="34" y="18" font-size="11" :fill="t.text">Consommations foyers</text>
              <line x1="10" y1="32" x2="28" y2="32" stroke="#ea580c" stroke-width="2.5" stroke-linecap="round" />
              <text x="34" y="36" font-size="11" :fill="t.text">Consommations exceptionnelles</text>
              <line x1="10" y1="50" x2="28" y2="50" stroke="#059669" stroke-width="2" stroke-dasharray="6 3" stroke-linecap="round" />
              <text x="34" y="54" font-size="11" :fill="t.text">
                Prix median ({{ medianPrice.toFixed(2) }}€/m³)
              </text>
              <g v-if="citizenAbo > 0">
                <line x1="10" y1="68" x2="28" y2="68" stroke="#059669" stroke-width="2" stroke-dasharray="3 2" stroke-linecap="round" />
                <text x="34" y="72" font-size="11" :fill="t.text">Prix moyen avec abo.</text>
              </g>
            </g>
          </svg>
          </div>
        </div>
      </div> <!-- end main-layout-chart -->

        <!-- ── RIGHT SIDEBAR: Auth + Vote ── -->
        <div class="main-layout-sidebar">
          <!-- Auth: Code electeur -->
          <div class="card sidebar-card">
            <div v-if="isCitizenAuth" class="alert alert-success" style="margin-bottom: 0;">
              Authentifie. Ajustez la courbe et votez.
            </div>
            <template v-else>
              <div class="sidebar-auth-block">
                <h4>Code electeur</h4>
                <p class="sidebar-muted">par carte electorale</p>
                <div v-if="authError" class="alert alert-error" style="margin-bottom: 0.5rem; font-size: 0.8rem;">{{ authError }}</div>
                <form @submit.prevent="authenticate" style="display: flex; gap: 0.4rem;">
                  <input v-model="authCode" type="text" maxlength="8" placeholder="Code electeur"
                    class="form-input" style="flex: 1; text-transform: uppercase; font-family: monospace; letter-spacing: 0.12em; font-size: 0.85rem;" />
                  <button type="submit" class="btn btn-primary" :disabled="authLoading" style="padding: 0.35rem 0.75rem;">OK</button>
                </form>
                <p v-if="isDev" :style="{ marginTop: '0.4rem', padding: '0.3rem 0.5rem', background: t.devBg, border: '1px solid ' + t.devBorder, borderRadius: '5px', fontSize: '0.7rem', color: isDark ? '#fcd34d' : 'inherit' }">
                  <strong>Dev:</strong> DEVTEST2 (RS 60m³) · DEVTEST3 (RP 120m³) · DEVTEST4 (PRO 350m³)
                </p>
              </div>
              <div class="sidebar-auth-block" style="opacity: 0.5; border-top: 1px solid var(--color-border); padding-top: 0.75rem; margin-top: 0.75rem;">
                <h4>Toile fiduciaire decentralisee</h4>
                <p class="sidebar-muted">Authentification avec identite numerique Duniter</p>
                <button class="btn btn-secondary" disabled style="font-size: 0.78rem; padding: 0.3rem 0.6rem;">Bientot disponible</button>
              </div>
            </template>
          </div>

          <!-- Vote -->
          <div class="card sidebar-card">
            <h4 style="margin-bottom: 0.25rem;">Mon vote</h4>
            <p style="font-size: 0.78rem; color: var(--color-text-muted); margin-bottom: 0.6rem;">
              Ajustez la courbe, puis sauvegardez.
            </p>
            <button class="btn btn-primary" @click="submitVote" style="width: 100%; font-size: 0.9rem;"
              :disabled="!isCitizenAuth || submitting || isVoteClosed">
              <span v-if="submitting" class="spinner" style="width: 0.9rem; height: 0.9rem;"></span>
              Sauvegarder mon vote
            </button>
            <p v-if="lastVoteDate" style="font-size: 0.72rem; color: var(--color-text-muted); margin-top: 0.4rem; text-align: center;">
              Dernier vote enregistre le {{ lastVoteDate }}
            </p>
            <p v-if="!isCitizenAuth" style="font-size: 0.72rem; color: var(--color-text-muted); margin-top: 0.4rem; text-align: center;">
              Authentifiez-vous pour voter.
            </p>
            <p v-else-if="isVoteClosed" style="font-size: 0.72rem; color: #dc2626; margin-top: 0.4rem; text-align: center;">
              Le vote est clos.
            </p>
            <div v-if="voteSuccess" class="alert alert-success" style="margin-top: 0.5rem; font-size: 0.8rem;">
              Vote enregistre !
            </div>
          </div>

          <!-- Countdown (if applicable) -->
          <div v-if="commune.vote_deadline" class="card sidebar-card">
            <h4 style="margin-bottom: 0.25rem;">Echeance</h4>
            <div v-if="isVoteClosed" style="font-size: 0.9rem; color: #dc2626; font-weight: 600;">
              Vote clos.
            </div>
            <div v-else class="countdown-compact">
              <span><strong>{{ countdown.days }}</strong>j</span>
              <span><strong>{{ countdown.hours }}</strong>h</span>
              <span><strong>{{ countdown.minutes }}</strong>m</span>
              <span><strong>{{ countdown.seconds }}</strong>s</span>
            </div>
          </div>
        </div>
      </div> <!-- end main-layout -->

      <!-- ═══════════════════════════════════════════════════════
           SITUATION DE DEPART
           ═══════════════════════════════════════════════════════ -->
      <h2 class="section-title">Situation de depart</h2>

      <!-- Histogramme des consommations -->
      <div v-if="distBuckets.length" class="chart-card" style="margin-bottom: 1.5rem;">
        <div class="chart-header">
          <div>
            <h3 class="chart-title">Repartition des consommations</h3>
            <p class="chart-subtitle">Nombre de foyers par tranche de consommation (m³/an)</p>
          </div>
        </div>
        <div class="chart-container">
          <svg :viewBox="`0 0 ${histW} ${histH}`" preserveAspectRatio="xMidYMid meet">
            <rect :x="histMargin.left" :y="histMargin.top" :width="histPlotW" :height="histPlotH"
              :fill="t.plotBg" rx="4" />

            <!-- Y grid lines -->
            <g>
              <template v-for="y in histGridY" :key="'hgy'+y">
                <line :x1="histMargin.left" :y1="histCy(y)" :x2="histMargin.left + histPlotW" :y2="histCy(y)"
                  :stroke="t.grid" stroke-width="0.5" />
                <text :x="histMargin.left - 6" :y="histCy(y) + 4" text-anchor="end" class="axis-label-sm">{{ y }}</text>
              </template>
            </g>

            <!-- Bars -->
            <g v-for="(bk, i) in distBuckets" :key="'hb'+i">
              <rect
                :x="histCx(bk.low) + 1"
                :y="histCy(bk.count)"
                :width="Math.max(0, histCx(bk.high) - histCx(bk.low) - 2)"
                :height="Math.max(0, histCy(0) - histCy(bk.count))"
                :fill="bk.high <= bp.vinf ? '#2563eb' : '#ea580c'"
                :opacity="bk.high <= bp.vinf ? 0.7 : 0.7"
                rx="2"
              />
              <!-- Count label above bar -->
              <text v-if="bk.count > 0"
                :x="(histCx(bk.low) + histCx(bk.high)) / 2"
                :y="histCy(bk.count) - 4"
                text-anchor="middle" font-size="10" :fill="t.text" font-weight="600">
                {{ bk.count }}
              </text>
            </g>

            <!-- Vinf dashed line -->
            <line :x1="histCx(bp.vinf)" :y1="histMargin.top" :x2="histCx(bp.vinf)" :y2="histMargin.top + histPlotH"
              stroke="#8b5cf6" stroke-width="1.5" stroke-dasharray="5 3" />
            <text :x="histCx(bp.vinf)" :y="histMargin.top - 4" text-anchor="middle" font-size="10" fill="#8b5cf6" font-weight="600">
              Palier : {{ bp.vinf.toFixed(0) }} m³
            </text>

            <!-- X axis labels -->
            <g>
              <template v-for="(bk, i) in distBuckets" :key="'hx'+i">
                <text v-if="i % 2 === 0 || distBuckets.length <= 12"
                  :x="histCx(bk.low)" :y="histMargin.top + histPlotH + 16" text-anchor="middle" class="axis-label-sm">
                  {{ bk.low }}
                </text>
              </template>
            </g>

            <!-- Legend (top-right) -->
            <g :transform="`translate(${histMargin.left + histPlotW - 222}, ${histMargin.top + 6})`">
              <rect x="0" y="0" width="210" height="44" rx="6" :fill="t.legendBg" fill-opacity="0.9" :stroke="t.legendBorder" />
              <rect x="10" y="8" width="14" height="10" rx="2" fill="#2563eb" opacity="0.7" />
              <text x="30" y="17" font-size="10" :fill="t.text">Consommations foyers</text>
              <rect x="10" y="26" width="14" height="10" rx="2" fill="#ea580c" opacity="0.7" />
              <text x="30" y="35" font-size="10" :fill="t.text">Consommations exceptionnelles</text>
            </g>

            <!-- Axis titles -->
            <text :x="histMargin.left + histPlotW / 2" :y="histMargin.top + histPlotH + 36" text-anchor="middle" class="axis-title">
              Consommation (m³)
            </text>
            <text :x="histMargin.left - 28" :y="histMargin.top + histPlotH / 2" text-anchor="middle"
              class="axis-title" transform-origin="center" :transform="`rotate(-90, ${histMargin.left - 28}, ${histMargin.top + histPlotH / 2})`">
              Foyers
            </text>
          </svg>
        </div>
      </div>

      <!-- ═══════════════════════════════════════════════════════
           INDICATEURS CLES
           ═══════════════════════════════════════════════════════ -->
      <div v-if="params" class="key-metrics-banner" :style="{ marginBottom: '1.5rem', background: t.metricBg }">
        <div class="key-metric key-metric-input">
          <span class="key-metric-value">{{ params.recettes.toLocaleString() }} €</span>
          <span class="key-metric-label">Recettes cibles</span>
          <span class="key-metric-tag">contrainte</span>
        </div>
        <div class="key-metric key-metric-input">
          <span class="key-metric-value">{{ bp.vinf.toFixed(0) }} m³</span>
          <span class="key-metric-label">Palier exceptionnel</span>
          <span class="key-metric-tag">contrainte</span>
        </div>
        <div class="key-metric key-metric-result">
          <span class="key-metric-value">{{ localP0.toFixed(2) }} €/m³</span>
          <span class="key-metric-label">Prix au palier</span>
          <span class="key-metric-tag key-metric-tag-calc">calcule</span>
        </div>
        <div class="key-metric key-metric-result">
          <span class="key-metric-value">{{ medianPrice.toFixed(2) }} €/m³</span>
          <span class="key-metric-label">Prix median</span>
          <span class="key-metric-tag key-metric-tag-calc">calcule</span>
        </div>
        <div class="key-metric key-metric-highlight">
          <span class="key-metric-value">{{ citizenPriceM3 }}</span>
          <span class="key-metric-label">Mon prix au m³</span>
        </div>
      </div>

      <!-- ═══════════════════════════════════════════════════════
           PARAMETRES COURBE + TABLE IMPACTS
           ═══════════════════════════════════════════════════════ -->
      <div class="params-impacts-layout" style="margin-bottom: 1.5rem;">
        <div class="card">
          <h4 style="margin-bottom: 0.5rem;">Parametres de la courbe</h4>
          <div class="param-grid">
            <div class="param-row">
              <span class="param-label">v<sub>inf</sub></span>
              <span class="param-val">{{ bp.vinf.toFixed(0) }} m³</span>
            </div>
            <div class="param-row">
              <span class="param-label">a</span>
              <span class="param-val">{{ bp.a.toFixed(3) }}</span>
            </div>
            <div class="param-row">
              <span class="param-label">b</span>
              <span class="param-val">{{ bp.b.toFixed(3) }}</span>
            </div>
            <div class="param-row">
              <span class="param-label">c</span>
              <span class="param-val">{{ bp.c.toFixed(3) }}</span>
            </div>
            <div class="param-row">
              <span class="param-label">d</span>
              <span class="param-val">{{ bp.d.toFixed(3) }}</span>
            </div>
            <div class="param-row">
              <span class="param-label">e</span>
              <span class="param-val">{{ bp.e.toFixed(3) }}</span>
            </div>
          </div>
          <div style="border-top: 1px solid var(--color-border); padding-top: 0.75rem; margin-top: 0.5rem;">
            <p style="font-size: 0.82rem; color: var(--color-text-muted); font-weight: 600; margin-bottom: 0.35rem;">Votre vote donne :</p>
            <div class="vote-result-row">
              <span>un prix au palier de</span>
              <strong>{{ localP0.toFixed(2) }} €/m³</strong>
            </div>
            <div class="vote-result-row">
              <span>un prix median de</span>
              <strong>{{ medianPrice.toFixed(2) }} €/m³</strong>
            </div>
          </div>
        </div>

        <div class="card">
          <h4 style="margin-bottom: 0.5rem;">Impact par volume</h4>
          <table class="table table-sm">
            <thead>
              <tr>
                <th>Vol.</th>
                <th>Actuel</th>
                <th>{{ differentiatedTariff ? 'RP/PRO' : 'Nouveau' }}</th>
                <th v-if="differentiatedTariff">RS</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="imp in impacts" :key="imp.volume">
                <td>{{ imp.volume }} m³</td>
                <td>{{ imp.old_price.toFixed(0) }} €</td>
                <td :class="imp.new_price_rp > imp.old_price ? 'text-up' : 'text-down'">
                  {{ imp.new_price_rp.toFixed(0) }} €
                </td>
                <td v-if="differentiatedTariff" :class="imp.new_price_rs > imp.old_price ? 'text-up' : 'text-down'">
                  {{ imp.new_price_rs.toFixed(0) }} €
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      <!-- ═══════════════════════════════════════════════════════
           7. TARIFICATION ACTUELLE (baseline lineaire) — Axe X inverse
           ═══════════════════════════════════════════════════════ -->
      <div class="chart-card" style="margin-bottom: 1.5rem;">
        <h3 class="chart-title">Tarification actuelle (modele lineaire)</h3>
        <p class="chart-subtitle" style="margin-bottom: 1rem;">
          Situation tarifaire en vigueur : prix uniforme de {{ curveData.p0_linear?.toFixed(2) }} €/m³ + abonnement {{ abop }} €<template v-if="differentiatedTariff"> (RP/PRO) / {{ abos }} € (RS)</template>.
        </p>

        <div class="baseline-charts">
          <!-- Left: Facture totale -->
          <div class="chart-container">
            <h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: var(--color-text-muted);">Facture totale (€)</h4>
            <svg :viewBox="`0 0 ${W2} ${H2}`" preserveAspectRatio="xMidYMid meet">
              <rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" :fill="t.plotBg" rx="3" />
              <g>
                <line v-for="v in gridVols2" :key="'bg1v'+v"
                  :x1="cx2(v)" :y1="margin2.top" :x2="cx2(v)" :y2="margin2.top + plotH2"
                  :stroke="t.grid" stroke-width="0.5" />
                <line v-for="b in gridBills" :key="'bg1b'+b"
                  :x1="margin2.left" :y1="cy2bill(b)" :x2="margin2.left + plotW2" :y2="cy2bill(b)"
                  :stroke="t.grid" stroke-width="0.5" />
                <text v-for="v in gridVols2" :key="'bg1lv'+v"
                  :x="cx2(v)" :y="margin2.top + plotH2 + 14" text-anchor="middle" class="axis-label-sm">{{ v }}</text>
                <text v-for="b in gridBills" :key="'bg1lb'+b"
                  :x="margin2.left + plotW2 + 4" :y="cy2bill(b) + 3" text-anchor="start" class="axis-label-sm">{{ b }}€</text>
                <!-- Household counts per 30m³ -->
                <g v-if="distBuckets30.length">
                  <text v-for="bk in visibleBuckets30Baseline" :key="'b1hc'+bk.low"
                    :x="(cx2(bk.low) + cx2(bk.high)) / 2"
                    :y="margin2.top + plotH2 + 28"
                    text-anchor="middle" font-size="9" :fill="t.textMuted" font-weight="500">
                    {{ bk.count }}
                  </text>
                  <text :x="margin2.left + plotW2 + 4" :y="margin2.top + plotH2 + 28"
                    text-anchor="start" font-size="8" :fill="t.textMuted">foy.</text>
                </g>
              </g>
              <defs>
                <clipPath id="baseline-clip-1">
                  <rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" />
                </clipPath>
              </defs>
              <g clip-path="url(#baseline-clip-1)">
                <polyline :points="baselineBillRP" fill="none"
                  :stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#2563eb'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" />
                <polyline v-if="differentiatedTariff" :points="baselineBillRS" fill="none"
                  :stroke="showHouseholds ? (isDark ? '#334155' : '#e2e8f0') : '#f59e0b'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" stroke-dasharray="6 3" />
                <!-- Household dots: bill = abo + p0_linear * volume -->
                <g v-if="showHouseholds && householdVolumes.length">
                  <circle v-for="(hh, i) in householdDotsBill" :key="'hdb'+i"
                    :cx="cx2(hh.volume)" :cy="cy2bill(hh.bill)"
                    r="2.5" fill="#2563eb" opacity="0.5"
                    :stroke="t.plotBg" stroke-width="0.5"
                  />
                </g>
              </g>
              <text :x="W2/2" :y="margin2.top + plotH2 + 42" text-anchor="middle" class="axis-label-sm">m³ → reduire</text>
              <!-- Legend when differentiated (top-right) -->
              <g v-if="differentiatedTariff" :transform="`translate(${margin2.left + plotW2 - 92}, ${margin2.top + 4})`">
                <rect x="0" y="0" width="80" height="32" rx="4" :fill="t.legendBg" fill-opacity="0.9" :stroke="t.legendBorder" />
                <line x1="6" y1="11" x2="18" y2="11" stroke="#2563eb" stroke-width="2" />
                <text x="22" y="14" font-size="8" :fill="t.text">RP/PRO</text>
                <line x1="6" y1="24" x2="18" y2="24" stroke="#f59e0b" stroke-width="2" stroke-dasharray="4 2" />
                <text x="22" y="27" font-size="8" :fill="t.text">RS</text>
              </g>
            </svg>
          </div>

          <!-- Right: Prix au m3 -->
          <div class="chart-container">
            <h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: var(--color-text-muted);">Prix au m³ (€)</h4>
            <svg :viewBox="`0 0 ${W2} ${H2}`" preserveAspectRatio="xMidYMid meet">
              <rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" :fill="t.plotBg" rx="3" />
              <defs>
                <clipPath id="baseline-clip-2">
                  <rect :x="margin2.left" :y="margin2.top" :width="plotW2" :height="plotH2" />
                </clipPath>
              </defs>
              <g>
                <line v-for="v in gridVols2" :key="'bg2v'+v"
                  :x1="cx2(v)" :y1="margin2.top" :x2="cx2(v)" :y2="margin2.top + plotH2"
                  :stroke="t.grid" stroke-width="0.5" />
                <line v-for="p in gridPrices2" :key="'bg2p'+p"
                  :x1="margin2.left" :y1="cy2price(p)" :x2="margin2.left + plotW2" :y2="cy2price(p)"
                  :stroke="t.grid" stroke-width="0.5" />
                <text v-for="v in gridVols2" :key="'bg2lv'+v"
                  :x="cx2(v)" :y="margin2.top + plotH2 + 14" text-anchor="middle" class="axis-label-sm">{{ v }}</text>
                <text v-for="p in gridPrices2" :key="'bg2lp'+p"
                  :x="margin2.left + plotW2 + 4" :y="cy2price(p) + 3" text-anchor="start" class="axis-label-sm">{{ p }}€</text>
                <!-- Household counts per 30m³ -->
                <g v-if="distBuckets30.length">
                  <text v-for="bk in visibleBuckets30Baseline" :key="'b2hc'+bk.low"
                    :x="(cx2(bk.low) + cx2(bk.high)) / 2"
                    :y="margin2.top + plotH2 + 28"
                    text-anchor="middle" font-size="9" :fill="t.textMuted" font-weight="500">
                    {{ bk.count }}
                  </text>
                  <text :x="margin2.left + plotW2 + 4" :y="margin2.top + plotH2 + 28"
                    text-anchor="start" font-size="8" :fill="t.textMuted">foy.</text>
                </g>
              </g>
              <g clip-path="url(#baseline-clip-2)">
                <polyline :points="baselinePriceRP" fill="none"
                  :stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : '#2563eb'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" />
                <polyline v-if="differentiatedTariff" :points="baselinePriceRS" fill="none"
                  :stroke="showHouseholds ? (isDark ? '#334155' : '#e2e8f0') : '#f59e0b'" :stroke-width="showHouseholds ? 1.5 : 2" stroke-linecap="round" stroke-dasharray="6 3" />
                <!-- Household dots: price = abo/v + p0_linear -->
                <g v-if="showHouseholds && householdVolumes.length">
                  <circle v-for="(hh, i) in householdDotsBaselinePrice" :key="'hdp'+i"
                    :cx="cx2(hh.volume)" :cy="cy2price(hh.price)"
                    r="2.5" fill="#2563eb" opacity="0.5"
                    :stroke="t.plotBg" stroke-width="0.5"
                  />
                </g>
                <line :x1="cx2(baselineVolMax)" :y1="cy2price(curveData.p0_linear)" :x2="cx2(0)" :y2="cy2price(curveData.p0_linear)"
                  stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 4" />
              </g>
              <text :x="cx2(0) + 5" :y="cy2price(curveData.p0_linear) - 6" text-anchor="start"
                font-size="10" :fill="t.textCount" font-weight="500">
                Prix uniforme : {{ curveData.p0_linear?.toFixed(2) }}€/m³
              </text>
              <text :x="W2/2" :y="margin2.top + plotH2 + 42" text-anchor="middle" class="axis-label-sm">m³ → reduire</text>
              <!-- Legend when differentiated (top-right) -->
              <g v-if="differentiatedTariff" :transform="`translate(${margin2.left + plotW2 - 92}, ${margin2.top + 4})`">
                <rect x="0" y="0" width="80" height="32" rx="4" :fill="t.legendBg" fill-opacity="0.9" :stroke="t.legendBorder" />
                <line x1="6" y1="11" x2="18" y2="11" stroke="#2563eb" stroke-width="2" />
                <text x="22" y="14" font-size="8" :fill="t.text">RP/PRO</text>
                <line x1="6" y1="24" x2="18" y2="24" stroke="#f59e0b" stroke-width="2" stroke-dasharray="4 2" />
                <text x="22" y="27" font-size="8" :fill="t.text">RS</text>
              </g>
            </svg>
          </div>
        </div>

        <!-- Full-width: Prix marginal au m³ + distribution foyers -->
        <div style="margin-top: 1rem;">
          <h4 style="text-align: center; font-size: 0.85rem; margin-bottom: 0.25rem; color: var(--color-text-muted);">
            Prix au m³ — situation actuelle
          </h4>
          <div class="chart-container">
            <svg :viewBox="`0 0 ${Wmarg} ${Hmarg}`" preserveAspectRatio="xMidYMid meet">
              <!-- Background -->
              <rect :x="margMargin.left" :y="margMargin.top" :width="margPlotW" :height="margPlotH"
                :fill="t.plotBg" rx="4" />

              <defs>
                <clipPath id="marg-clip">
                  <rect :x="margMargin.left" :y="margMargin.top" :width="margPlotW" :height="margPlotH" />
                </clipPath>
                <linearGradient id="bar-grad" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0%" :stop-color="isDark ? '#60a5fa' : '#2563eb'" stop-opacity="0.35" />
                  <stop offset="100%" :stop-color="isDark ? '#60a5fa' : '#2563eb'" stop-opacity="0.12" />
                </linearGradient>
              </defs>

              <!-- Grid -->
              <g>
                <line v-for="v in margGridVols" :key="'mgv'+v"
                  :x1="margCx(v)" :y1="margMargin.top" :x2="margCx(v)" :y2="margMargin.top + margPlotH"
                  :stroke="t.grid" stroke-width="0.5" />
                <line v-for="p in margGridPrices" :key="'mgp'+p"
                  :x1="margMargin.left" :y1="margCyPrice(p)" :x2="margMargin.left + margPlotW" :y2="margCyPrice(p)"
                  :stroke="t.grid" stroke-width="0.5" />
                <!-- Volume labels -->
                <text v-for="v in margGridVols" :key="'mglv'+v"
                  :x="margCx(v)" :y="margMargin.top + margPlotH + 18" text-anchor="middle" class="axis-label-sm">{{ v }}m³</text>
                <!-- Price labels (right) -->
                <text v-for="p in margGridPrices" :key="'mglp'+p"
                  :x="margMargin.left + margPlotW + 4" :y="margCyPrice(p) + 3" text-anchor="start" class="axis-label-sm">
                  {{ p.toFixed(p < 1 ? 1 : 0) }}€
                </text>
                <!-- Household count labels (left axis) -->
                <text v-for="c in margGridCounts" :key="'mgc'+c"
                  :x="margMargin.left - 4" :y="margCyCount(c) + 3" text-anchor="end"
                  font-size="9.5" :fill="t.textMuted" font-weight="500">{{ c }}</text>
              </g>

              <!-- Household histogram bars (behind the curve) -->
              <g clip-path="url(#marg-clip)">
                <g v-for="(bk, i) in distBuckets30" :key="'mb'+i">
                  <rect v-if="bk.count > 0"
                    :x="Math.min(margCx(bk.low), margCx(bk.high))"
                    :y="margCyCount(bk.count)"
                    :width="Math.abs(margCx(bk.high) - margCx(bk.low))"
                    :height="Math.max(0, margCyCount(0) - margCyCount(bk.count))"
                    fill="url(#bar-grad)" rx="1"
                  />
                  <text v-if="bk.count > 0"
                    :x="(margCx(bk.low) + margCx(bk.high)) / 2"
                    :y="margCyCount(bk.count) - 3"
                    text-anchor="middle" font-size="9.5" :fill="t.textCount" font-weight="600">
                    {{ bk.count }}
                  </text>
                </g>

                <!-- Price line (marginal) -->
                <polyline :points="margPriceLine" fill="none"
                  :stroke="showHouseholds ? (isDark ? '#475569' : '#cbd5e1') : (isDark ? '#93c5fd' : '#1e40af')"
                  :stroke-width="showHouseholds ? 1.5 : 2.5" stroke-linecap="round" />

                <!-- Household dots on marginal price chart -->
                <g v-if="showHouseholds && householdVolumes.length">
                  <circle v-for="(hh, i) in householdDotsMarg" :key="'hm'+i"
                    :cx="margCx(hh.volume)" :cy="margCyPrice(hh.price)"
                    r="3"
                    :fill="isDark ? '#93c5fd' : '#1e40af'" opacity="0.5"
                    :stroke="t.plotBg" stroke-width="0.6"
                  />
                </g>

                <!-- p0 horizontal dashed line -->
                <line :x1="margMargin.left" :y1="margCyPrice(curveData.p0_linear)" :x2="margMargin.left + margPlotW" :y2="margCyPrice(curveData.p0_linear)"
                  stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 3" />
              </g>

              <!-- p0 label -->
              <text :x="margMargin.left + margPlotW + 4" :y="margCyPrice(curveData.p0_linear) - 5" text-anchor="start"
                font-size="9" :fill="t.textCount" font-weight="600">
                {{ curveData.p0_linear?.toFixed(2) }}€/m³
              </text>

              <!-- Axis titles -->
              <text :x="Wmarg / 2" :y="margMargin.top + margPlotH + 40" text-anchor="middle" class="axis-title">
                Consommation (m³) → reduire
              </text>
              <!-- Y-axis title left: Foyers -->
              <text :x="8" :y="margMargin.top + margPlotH / 2" text-anchor="middle"
                class="axis-title" :transform="`rotate(-90, 8, ${margMargin.top + margPlotH / 2})`">
                Nombre de foyers
              </text>
              <!-- Y-axis title right: €/m³ -->
              <text :x="Wmarg - 4" :y="margMargin.top - 6" text-anchor="end" class="axis-title">
                €/m³
              </text>

              <!-- Legend -->
              <g :transform="`translate(${margMargin.left + 8}, ${margMargin.top + 6})`">
                <rect x="0" y="0" width="230" height="44" rx="6" :fill="t.legendBg" fill-opacity="0.92" :stroke="t.legendBorder" />
                <line x1="10" y1="14" x2="28" y2="14" :stroke="isDark ? '#93c5fd' : '#1e40af'" stroke-width="2.5" stroke-linecap="round" />
                <text x="34" y="18" font-size="10" :fill="t.text" font-weight="500">Prix au m³ avec abonnement (€/m³)</text>
                <rect x="10" y="25" width="14" height="10" rx="2" :fill="isDark ? '#60a5fa' : '#2563eb'" fill-opacity="0.25" />
                <text x="34" y="34" font-size="10" :fill="t.text" font-weight="500">Foyers par tranche de 30m³</text>
              </g>
            </svg>
          </div>
        </div>
      </div>

      <!-- ═══════════════════════════════════════════════════════
           CONTENU CMS
           ═══════════════════════════════════════════════════════ -->
      <div v-if="contentPages.length" style="margin-bottom: 1.5rem;">
        <div v-for="page in contentPages" :key="page.slug" class="card" style="margin-bottom: 1rem;">
          <h3 style="margin-bottom: 0.5rem;">{{ page.title }}</h3>
          <div class="cms-body" v-html="renderMarkdown(page.body_markdown)"></div>
        </div>
      </div>

    </template>
  </div>

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

  <div v-else style="text-align: center; padding: 3rem;">
    <div class="spinner" style="margin: 0 auto;"></div>
  </div>
</template>

<script setup lang="ts">
import {
  computeP0, computeImpacts, computeIntegrals, generateCurve,
  paramsToControlPoints,
  type HouseholdData, type ImpactRow, type ControlPoints,
} from '~/utils/bezier-math'

const route = useRoute()
const authStore = useAuthStore()
const api = useApi()
const isDark = useState('theme-dark', () => false)

// Reactive SVG theme colors (light/dark aware)
const t = computed(() => isDark.value ? {
  plotBg: '#1e293b', grid: '#334155', legendBg: '#1e293b', legendBorder: '#475569',
  text: '#cbd5e1', textDark: '#f1f5f9', textMuted: '#94a3b8', textCount: '#94a3b8',
  pillBg: '#1e293b', pillBorder: '#475569', pillText: '#f1f5f9',
  gradStart: 'rgba(96,165,250,0.3)', gradEnd: 'rgba(96,165,250,0.08)',
  metricBg: 'linear-gradient(135deg, #1e293b, #162032)',
  highlightBg: 'rgba(52,211,153,0.1)',
  devBg: '#451a03', devBorder: '#92400e',
} : {
  plotBg: '#f8fafc', grid: '#e2e8f0', legendBg: 'white', legendBorder: '#e2e8f0',
  text: '#334155', textDark: '#1e293b', textMuted: '#64748b', textCount: '#475569',
  pillBg: 'white', pillBorder: '#e2e8f0', pillText: '#1e293b',
  gradStart: 'rgba(37,99,235,0.35)', gradEnd: 'rgba(37,99,235,0.12)',
  metricBg: 'linear-gradient(135deg, #eff6ff, #f0fdf4)',
  highlightBg: 'rgba(5,150,105,0.06)',
  devBg: '#fef3c7', devBorder: '#f59e0b',
})

const slug = route.params.slug as string
const commune = ref<any>(null)
const params = ref<any>(null)
const curveData = ref<any>(null)
const loading = ref(true)
const loadError = ref('')
const contentPages = ref<any[]>([])

// Bezier params (citizen-adjustable)
const bp = reactive({ vinf: 400, a: 0.5, b: 0.5, c: 0.5, d: 0.5, e: 0.5 })
const localP0 = ref(0)
const impacts = ref<any[]>([])
const households = ref<HouseholdData[]>([])

// Tariff fixed params
const vmax = ref(2100)
const pmax = ref(20)
const recettes = ref(75000)
const abop = ref(100)
const abos = ref(100)
const differentiatedTariff = ref(false)

// Auth
const authCode = ref('')
const authError = ref('')
const authLoading = ref(false)
const isCitizenAuth = computed(() => authStore.isCitizen && authStore.communeSlug === slug)
const submitting = ref(false)
const voteSuccess = ref(false)
const lastVoteDate = ref<string | null>(null)
const citizenVolume = ref<number | null>(null)
const isDev = import.meta.dev

// Citizen's own price per m³ (from their volume, looked up on the Bézier curve)
const citizenPriceM3 = computed(() => {
  if (citizenVolume.value === null) return '—'
  const vol = citizenVolume.value
  if (vol <= 0) return '0.00 €/m³'
  // Find price on current Bézier curve at citizen's volume
  const dot = householdDots.value.find(d => Math.abs(d.volume - vol) < 0.1)
  if (dot) return dot.price.toFixed(2) + ' €/m³'
  // Fallback: generate curve and interpolate
  const curve = generateCurve(bp.vinf, vmax.value, pmax.value, localP0.value, bp.a, bp.b, bp.c, bp.d, bp.e, 200)
  for (let j = 1; j < curve.curveVolumes.length; j++) {
    if (curve.curveVolumes[j - 1]! <= vol && curve.curveVolumes[j]! >= vol) {
      const dv = curve.curveVolumes[j]! - curve.curveVolumes[j - 1]!
      if (Math.abs(dv) < 1e-10) return curve.curvePricesM3[j]!.toFixed(2) + ' €/m³'
      const t = (vol - curve.curveVolumes[j - 1]!) / dv
      const price = curve.curvePricesM3[j - 1]! + t * (curve.curvePricesM3[j]! - curve.curvePricesM3[j - 1]!)
      return price.toFixed(2) + ' €/m³'
    }
  }
  return '—'
})

// ── Countdown ──
const countdown = reactive({ days: 0, hours: 0, minutes: 0, seconds: 0 })
const isVoteClosed = ref(false)
let countdownInterval: ReturnType<typeof setInterval> | null = null

function updateCountdown() {
  if (!commune.value?.vote_deadline) return
  const now = Date.now()
  const deadline = new Date(commune.value.vote_deadline).getTime()
  const diff = deadline - now
  if (diff <= 0) {
    isVoteClosed.value = true
    countdown.days = 0; countdown.hours = 0; countdown.minutes = 0; countdown.seconds = 0
    if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null }
    return
  }
  isVoteClosed.value = false
  countdown.days = Math.floor(diff / 86400000)
  countdown.hours = Math.floor((diff % 86400000) / 3600000)
  countdown.minutes = Math.floor((diff % 3600000) / 60000)
  countdown.seconds = Math.floor((diff % 60000) / 1000)
}

onUnmounted(() => { if (countdownInterval) clearInterval(countdownInterval) })

// ── Chart 1: Interactive Bezier with zoom — X AXIS INVERTED ──
const W = 700
const H = 400
const margin = { top: 24, right: 52, bottom: 56, left: 16 }
const plotW = W - margin.left - margin.right
const plotH = H - margin.top - margin.bottom

const zoomVolMin = ref(0)
const zoomVolMax = ref(2100)
const zoomPriceMin = ref(0)
const zoomPriceMax = ref(20)

const showOutliers = ref(false)
const showHouseholds = ref(false)
const outlierVotes = ref<any[]>([])
const citizenAbo = ref(0)
const householdVolumes = ref<Array<{ volume_m3: number; status: string }>>([])

// ── Distribution histogram data ──
const distBuckets = ref<Array<{ low: number; high: number; count: number }>>([])

// Histogram chart dimensions
const histW = 700
const histH = 240
const histMargin = { top: 24, right: 16, bottom: 48, left: 42 }
const histPlotW = histW - histMargin.left - histMargin.right
const histPlotH = histH - histMargin.top - histMargin.bottom

function histCx(v: number): number {
  const last = distBuckets.value[distBuckets.value.length - 1]
  const maxVol = last ? last.high : 1
  return histMargin.left + (v / maxVol) * histPlotW
}
const histMaxCount = computed(() => {
  if (!distBuckets.value.length) return 10
  return Math.ceil(Math.max(...distBuckets.value.map(b => b.count)) * 1.15)
})
function histCy(c: number): number {
  return histMargin.top + histPlotH - (c / histMaxCount.value) * histPlotH
}
const histGridY = computed(() => {
  const step = Math.max(1, Math.ceil(histMaxCount.value / 5 / 10) * 10)
  const arr: number[] = []
  for (let y = step; y < histMaxCount.value; y += step) arr.push(y)
  return arr
})

// INVERTED X: high volumes on LEFT, 0 on RIGHT
function cx(v: number) {
  return margin.left + plotW - ((v - zoomVolMin.value) / (zoomVolMax.value - zoomVolMin.value)) * plotW
}
function cy(p: number) {
  return margin.top + plotH - ((p - zoomPriceMin.value) / (zoomPriceMax.value - zoomPriceMin.value)) * plotH
}
// Inverse mapping for drag: screen X → data volume (inverted)
function fromX(sx: number) {
  return zoomVolMin.value + ((margin.left + plotW - sx) / plotW) * (zoomVolMax.value - zoomVolMin.value)
}
function fromY(sy: number) {
  return zoomPriceMin.value + ((margin.top + plotH - sy) / plotH) * (zoomPriceMax.value - zoomPriceMin.value)
}

function zoomIn() {
  const dv = (zoomVolMax.value - zoomVolMin.value) * 0.15
  const dp = (zoomPriceMax.value - zoomPriceMin.value) * 0.15
  zoomVolMin.value += dv; zoomVolMax.value -= dv
  zoomPriceMin.value += dp; zoomPriceMax.value -= dp
}
function zoomOut() {
  const dv = (zoomVolMax.value - zoomVolMin.value) * 0.2
  const dp = (zoomPriceMax.value - zoomPriceMin.value) * 0.2
  zoomVolMin.value = Math.max(0, zoomVolMin.value - dv)
  zoomVolMax.value = Math.min(vmax.value, zoomVolMax.value + dv)
  zoomPriceMin.value = Math.max(0, zoomPriceMin.value - dp)
  zoomPriceMax.value = Math.min(pmax.value, zoomPriceMax.value + dp)
}
function zoomReset() {
  zoomVolMin.value = 0; zoomVolMax.value = vmax.value
  zoomPriceMin.value = 0; zoomPriceMax.value = pmax.value
}
function zoomPreset(tier: string) {
  if (tier === 'tier1') {
    zoomVolMin.value = 0
    zoomVolMax.value = Math.min(bp.vinf * 1.3, vmax.value)
    zoomPriceMin.value = 0
    zoomPriceMax.value = Math.min(Math.max(localP0.value * 1.5, 2), pmax.value)
  } else {
    zoomVolMin.value = Math.max(0, bp.vinf * 0.8)
    zoomVolMax.value = vmax.value
    zoomPriceMin.value = Math.max(0, localP0.value * 0.7)
    zoomPriceMax.value = pmax.value
  }
}
function onWheel(e: WheelEvent) { e.deltaY < 0 ? zoomIn() : zoomOut() }

const gridVols = computed(() => {
  const range = zoomVolMax.value - zoomVolMin.value
  const step = Math.max(10, Math.ceil(range / 7 / (range > 500 ? 100 : 10)) * (range > 500 ? 100 : 10))
  const arr: number[] = []
  const start = Math.ceil(zoomVolMin.value / step) * step
  for (let v = start; v < zoomVolMax.value; v += step) arr.push(v)
  return arr
})
const gridPrices = computed(() => {
  const range = zoomPriceMax.value - zoomPriceMin.value
  const step = range > 5 ? Math.ceil(range / 5) : range > 1 ? 1 : 0.5
  const arr: number[] = []
  const start = Math.ceil(zoomPriceMin.value / step) * step
  for (let p = start; p <= zoomPriceMax.value; p += step) arr.push(p)
  return arr
})

const cp = computed<ControlPoints>(() =>
  paramsToControlPoints(bp.vinf, vmax.value, pmax.value, localP0.value, bp.a, bp.b, bp.c, bp.d, bp.e)
)

const dragPoints = computed(() => ({
  p2: cp.value.p2, p3: cp.value.p3, p4: cp.value.p4, p5: cp.value.p5, p6: cp.value.p6,
}))

const ptColors: Record<string, string> = {
  p2: '#3b82f6', p3: '#3b82f6', p4: '#8b5cf6', p5: '#ea580c', p6: '#ea580c',
}
const ptLabels: Record<string, string> = {
  p2: 'a', p3: 'b', p4: 'vinf', p5: 'c', p6: 'd,e',
}

const tier1Path = computed(() => {
  const c = cp.value
  return `M ${cx(c.p1.x)} ${cy(c.p1.y)} C ${cx(c.p2.x)} ${cy(c.p2.y)}, ${cx(c.p3.x)} ${cy(c.p3.y)}, ${cx(c.p4.x)} ${cy(c.p4.y)}`
})
const tier2Path = computed(() => {
  const c = cp.value
  return `M ${cx(c.p4.x)} ${cy(c.p4.y)} C ${cx(c.p5.x)} ${cy(c.p5.y)}, ${cx(c.p6.x)} ${cy(c.p6.y)}, ${cx(c.p7.x)} ${cy(c.p7.y)}`
})

// Prix moyen avec abonnement : (abo + integral) / v
// Uses generateCurve to get the Bézier price curve, then adds abo/v
const avgPriceWithAboPath = computed(() => {
  if (citizenAbo.value <= 0) return ''
  const curve = generateCurve(bp.vinf, vmax.value, pmax.value, localP0.value, bp.a, bp.b, bp.c, bp.d, bp.e, 150)
  const pts: string[] = []
  for (let i = 1; i < curve.curveVolumes.length; i++) {
    const v = curve.curveVolumes[i]
    if (v < 30) continue
    // Approximate bill at volume v: we use trapezoidal integration of marginal price
    // Simpler: bill ≈ average_marginal_price * v, and average_marginal_price ≈ curvePrice at v
    // More accurate: use the integral formulas. But for display, we use:
    // prix_moyen = abo/v + prix_marginal(v) * correction
    // Actually, the correct formula for a Bézier tariff:
    // bill(v) = integral_0^v of marginal_price(u) du
    // prix_moyen(v) = abo/v + bill(v)/v
    // For simplicity and reactivity, we approximate bill(v) using trapezoidal sum
    let bill = 0
    for (let j = 1; j <= i; j++) {
      const dv = curve.curveVolumes[j] - curve.curveVolumes[j - 1]
      const avgP = (curve.curvePricesM3[j] + curve.curvePricesM3[j - 1]) / 2
      bill += avgP * dv
    }
    const avgPrice = citizenAbo.value / v + bill / v
    if (avgPrice >= 0 && avgPrice <= pmax.value * 2) {
      pts.push(`${cx(v)},${cy(avgPrice)}`)
    }
  }
  return pts.join(' ')
})

// Household dots: compute Bézier price at each household's volume
const householdDots = computed(() => {
  if (!householdVolumes.value.length) return []
  const curve = generateCurve(bp.vinf, vmax.value, pmax.value, localP0.value, bp.a, bp.b, bp.c, bp.d, bp.e, 300)
  const vols = curve.curveVolumes
  const prices = curve.curvePricesM3

  return householdVolumes.value.map(hh => {
    const v = hh.volume_m3
    // Find the price by linear interpolation in the generated curve
    let price = 0
    for (let j = 1; j < vols.length; j++) {
      if ((vols[j - 1]! <= v && vols[j]! >= v) || (vols[j]! <= v && vols[j - 1]! >= v)) {
        const dv = vols[j]! - vols[j - 1]!
        if (Math.abs(dv) < 1e-10) { price = prices[j]!; break }
        const t = (v - vols[j - 1]!) / dv
        price = prices[j - 1]! + t * (prices[j]! - prices[j - 1]!)
        break
      }
    }
    // If volume beyond curve range, use last price
    if (v >= vols[vols.length - 1]!) price = prices[prices.length - 1]!
    return { volume: v, price, status: hh.status }
  })
})

function outlierPath(vote: any, tier: number): string {
  const p0 = vote.computed_p0 || localP0.value
  const oc = paramsToControlPoints(vote.vinf, vmax.value, pmax.value, p0, vote.a, vote.b, vote.c, vote.d, vote.e)
  if (tier === 1) {
    return `M ${cx(oc.p1.x)} ${cy(oc.p1.y)} C ${cx(oc.p2.x)} ${cy(oc.p2.y)}, ${cx(oc.p3.x)} ${cy(oc.p3.y)}, ${cx(oc.p4.x)} ${cy(oc.p4.y)}`
  }
  return `M ${cx(oc.p4.x)} ${cy(oc.p4.y)} C ${cx(oc.p5.x)} ${cy(oc.p5.y)}, ${cx(oc.p6.x)} ${cy(oc.p6.y)}, ${cx(oc.p7.x)} ${cy(oc.p7.y)}`
}

// ── Drag handling ──
const svgRef = ref<SVGSVGElement | null>(null)
const dragging = ref<string | null>(null)

function getSvgPt(event: MouseEvent | Touch) {
  if (!svgRef.value) return { x: 0, y: 0 }
  const rect = svgRef.value.getBoundingClientRect()
  return {
    x: (event.clientX - rect.left) * (W / rect.width),
    y: (event.clientY - rect.top) * (H / rect.height),
  }
}
function startDrag(key: string) { dragging.value = key }
function onMouseMove(e: MouseEvent) { if (dragging.value) handleDrag(getSvgPt(e)) }
function onTouchMove(e: TouchEvent) { if (dragging.value && e.touches[0]) handleDrag(getSvgPt(e.touches[0])) }
function onMouseUp() {
  if (dragging.value) {
    dragging.value = null
    recompute()
  }
}

function handleDrag(pt: { x: number; y: number }) {
  const v = Math.max(0, Math.min(vmax.value, fromX(pt.x)))
  const p = Math.max(0, Math.min(pmax.value, fromY(pt.y)))

  switch (dragging.value) {
    case 'p2':
      bp.a = localP0.value > 0 ? Math.max(0, Math.min(1, p / localP0.value)) : 0.5
      break
    case 'p3':
      bp.b = bp.vinf > 0 ? Math.max(0, Math.min(1, v / bp.vinf)) : 0.5
      break
    case 'p4':
      bp.vinf = Math.max(1, Math.min(vmax.value - 1, v))
      break
    case 'p5': {
      const wmax = vmax.value - bp.vinf
      bp.c = wmax > 0 ? Math.max(0, Math.min(1, (v - bp.vinf) / wmax)) : 0.5
      break
    }
    case 'p6': {
      const wmax = vmax.value - bp.vinf
      const qmax = pmax.value - localP0.value
      bp.e = qmax > 0 ? Math.max(0, Math.min(1, (p - localP0.value) / qmax)) : 0.5
      if (wmax > 0 && Math.abs(1 - bp.c) > 1e-10) {
        const ratio = (v - bp.vinf) / wmax
        bp.d = Math.max(0, Math.min(1, (1 - ratio) / (1 - bp.c)))
      }
      break
    }
    case 'medianBar': {
      // Drag the median price bar → scale p0 and back-compute recettes
      const newPrice = Math.max(0.01, Math.min(pmax.value * 0.8, p))
      const oldMedian = medianPrice.value
      if (oldMedian > 0.001 && hhData.value.length > 0) {
        const scale = newPrice / oldMedian
        const newP0 = localP0.value * scale
        // Back-compute recettes from new p0
        let totalAbo = 0, totalAlpha = 0, totalBeta = 0
        for (const hh of hhData.value) {
          totalAbo += hh.status === 'RS' ? abos.value : abop.value
          const { alpha1, alpha2, beta2 } = computeIntegrals(
            Math.max(hh.volume_m3, 1e-5), bp.vinf, vmax.value, pmax.value,
            bp.a, bp.b, bp.c, bp.d, bp.e,
          )
          totalAlpha += alpha1 + alpha2
          totalBeta += beta2
        }
        recettes.value = Math.max(0, newP0 * totalAlpha + totalAbo + totalBeta)
        localP0.value = newP0
      }
      break
    }
  }
}

// ── Client-side household data for local computation ──
const hhData = computed<HouseholdData[]>(() =>
  householdVolumes.value.map(h => ({ volume_m3: h.volume_m3, status: h.status }))
)

// Median volume and median marginal price
const medianVolume = computed(() => {
  if (!householdVolumes.value.length) return 60
  const sorted = [...householdVolumes.value].map(h => h.volume_m3).sort((a, b) => a - b)
  const mid = Math.floor(sorted.length / 2)
  return sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!
})

const medianPrice = computed(() => {
  if (!householdDots.value.length) return localP0.value * 0.3
  const sorted = [...householdDots.value].map(d => d.price).sort((a, b) => a - b)
  const mid = Math.floor(sorted.length / 2)
  return sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!
})

// Recompute p0 and impacts client-side (instant, no server round-trip)
function recompute() {
  if (hhData.value.length > 0) {
    localP0.value = computeP0(
      hhData.value, recettes.value, abop.value, abos.value,
      bp.vinf, vmax.value, pmax.value,
      bp.a, bp.b, bp.c, bp.d, bp.e,
    )
    const result = computeImpacts(
      hhData.value, recettes.value, abop.value, abos.value,
      bp.vinf, vmax.value, pmax.value,
      bp.a, bp.b, bp.c, bp.d, bp.e,
    )
    impacts.value = result.impacts.map(imp => ({
      volume: imp.volume,
      old_price: imp.oldPrice,
      new_price_rp: imp.newPriceRP,
      new_price_rs: imp.newPriceRS,
    }))
  } else {
    // Fallback to server if no household data yet
    serverComputeFallback()
  }
}

let serverTimeout: ReturnType<typeof setTimeout> | null = null
function serverComputeFallback() {
  if (serverTimeout) clearTimeout(serverTimeout)
  serverTimeout = setTimeout(async () => {
    try {
      const result = await api.post<any>('/tariff/compute', {
        commune_slug: slug, vinf: bp.vinf,
        a: bp.a, b: bp.b, c: bp.c, d: bp.d, e: bp.e,
      })
      localP0.value = result.p0
      impacts.value = result.impacts.map((imp: any) => ({
        volume: imp.volume,
        old_price: imp.old_price,
        new_price_rp: imp.new_price_rp,
        new_price_rs: imp.new_price_rs,
      }))
    } catch {}
  }, 150)
}

// ── Chart 2: Baseline linear model — X AXIS INVERTED ──
const W2 = 340
const H2 = 230
const margin2 = { top: 10, right: 42, bottom: 52, left: 12 }
const plotW2 = W2 - margin2.left - margin2.right
const plotH2 = H2 - margin2.top - margin2.bottom

// Baseline chart range: tight focus on first tier for detail on small consumptions
const baselineVolMax = computed(() => {
  const target = Math.max(bp.vinf * 0.8, 300)
  return Math.min(Math.ceil(target / 50) * 50, vmax.value)
})

// INVERTED: high volumes left, 0 right
function cx2(v: number) { return margin2.left + plotW2 - (v / baselineVolMax.value) * plotW2 }

const maxBill = computed(() => {
  if (!curveData.value?.baseline_bills_rp?.length || !curveData.value?.baseline_volumes?.length) return 500
  // Only consider bills within the focused baseline range
  const vols = curveData.value.baseline_volumes
  const bills = curveData.value.baseline_bills_rp
  let mx = 0
  for (let i = 0; i < vols.length; i++) {
    if (vols[i] <= baselineVolMax.value && bills[i] > mx) mx = bills[i]
  }
  return Math.ceil((mx || 500) * 1.15 / 100) * 100
})

function cy2bill(b: number) { return margin2.top + plotH2 - (b / maxBill.value) * plotH2 }
// Focused price range for baseline charts
const baselinePriceMax = computed(() => {
  if (!curveData.value?.baseline_price_m3_rp?.length || !curveData.value?.baseline_volumes?.length) return pmax.value
  const vols = curveData.value.baseline_volumes
  const prices = curveData.value.baseline_price_m3_rp
  let mx = 0
  for (let i = 0; i < vols.length; i++) {
    if (vols[i] <= baselineVolMax.value && vols[i] > 0.5 && prices[i] > mx) mx = prices[i]
  }
  return mx > 0 ? Math.ceil(mx * 1.3) : pmax.value
})
function cy2price(p: number) { return margin2.top + plotH2 - (p / baselinePriceMax.value) * plotH2 }

const gridVols2 = computed(() => {
  const mx = baselineVolMax.value
  const step = mx > 500 ? Math.ceil(mx / 5 / 100) * 100 : Math.ceil(mx / 5 / 50) * 50
  const arr: number[] = []
  for (let v = step; v < mx; v += step) arr.push(v)
  return arr
})
const gridBills = computed(() => {
  const step = Math.ceil(maxBill.value / 4 / 100) * 100
  const arr: number[] = []
  for (let b = step; b < maxBill.value; b += step) arr.push(b)
  return arr
})
const gridPrices2 = computed(() => {
  const mx = baselinePriceMax.value
  const step = mx > 5 ? Math.ceil(mx / 4) : mx > 2 ? 1 : 0.5
  const arr: number[] = []
  for (let p = step; p <= mx; p += step) arr.push(p)
  return arr
})

function toPolyline(vols: number[] | undefined, vals: number[] | undefined, cyFn: (v: number) => number): string {
  if (!vols?.length || !vals?.length) return ''
  const pts: string[] = []
  for (let i = 0; i < vols.length; i += 4) {
    pts.push(`${cx2(vols[i]!)},${cyFn(vals[i]!)}`)
  }
  const last = vols.length - 1
  if (last % 4 !== 0) pts.push(`${cx2(vols[last]!)},${cyFn(vals[last]!)}`)
  return pts.join(' ')
}

const baselineBillRP = computed(() => toPolyline(curveData.value?.baseline_volumes, curveData.value?.baseline_bills_rp, cy2bill))
const baselinePriceRP = computed(() => toPolyline(curveData.value?.baseline_volumes, curveData.value?.baseline_price_m3_rp, cy2price))
const baselineBillRS = computed(() => toPolyline(curveData.value?.baseline_volumes, curveData.value?.baseline_bills_rs, cy2bill))
const baselinePriceRS = computed(() => toPolyline(curveData.value?.baseline_volumes, curveData.value?.baseline_price_m3_rs, cy2price))

// ── Chart 3: Marginal price + household distribution (full-width, population focus, inverted X) ──
const Wmarg = 700
const Hmarg = 380
const margMargin = { top: 24, right: 52, bottom: 48, left: 42 }
const margPlotW = Wmarg - margMargin.left - margMargin.right
const margPlotH = Hmarg - margMargin.top - margMargin.bottom

// Focus on population: 0 to ~600 m³
const margVolMax = computed(() => {
  if (!distBuckets30.value.length) return 600
  // Find last bucket with significant count
  const lastSig = [...distBuckets30.value].reverse().find(b => b.count >= 2)
  return lastSig ? Math.min(Math.ceil((lastSig.high + 60) / 30) * 30, vmax.value) : 600
})
const margPriceMax = computed(() => {
  if (!curveData.value?.p0_linear) return 5
  // Cap to show detail around p0_linear; abo/v shoots up at tiny volumes so don't let it dominate
  // Target: see the price at the first 30m³ bucket comfortably (abo/30 + p0 ≈ 3.3 + 1.x ≈ 5)
  const p0lin = curveData.value.p0_linear
  const aboOverThirty = abop.value / 30
  return Math.ceil((aboOverThirty + p0lin) * 1.4)
})

// Inverted X: high volumes left, 0 right
function margCx(v: number): number {
  return margMargin.left + margPlotW - (v / margVolMax.value) * margPlotW
}
function margCyPrice(p: number): number {
  return margMargin.top + margPlotH - (p / margPriceMax.value) * margPlotH
}

// Distribution (30m³ buckets)
const distBuckets30 = ref<Array<{ low: number; high: number; count: number }>>([])

// Filtered buckets visible in each chart's current zoom/range
const visibleBuckets30Main = computed(() => {
  return distBuckets30.value.filter(b =>
    b.high > zoomVolMin.value && b.low < zoomVolMax.value && b.count > 0
  )
})
const visibleBuckets30Baseline = computed(() => {
  return distBuckets30.value.filter(b => b.low < baselineVolMax.value && b.count > 0)
})

const margMaxCount = computed(() => {
  if (!distBuckets30.value.length) return 10
  return Math.max(...distBuckets30.value.map(b => b.count)) * 1.2
})
function margCyCount(c: number): number {
  return margMargin.top + margPlotH - (c / margMaxCount.value) * margPlotH
}

const margGridVols = computed(() => {
  const step = margVolMax.value > 300 ? 60 : 30
  const arr: number[] = []
  for (let v = step; v < margVolMax.value; v += step) arr.push(v)
  return arr
})
const margGridPrices = computed(() => {
  const mx = margPriceMax.value
  const step = mx > 5 ? Math.ceil(mx / 4) : mx > 2 ? 1 : 0.5
  const arr: number[] = []
  for (let p = step; p < mx; p += step) arr.push(p)
  return arr
})
const margGridCounts = computed(() => {
  const mx = margMaxCount.value
  const step = Math.max(1, Math.ceil(mx / 4 / 10) * 10)
  const arr: number[] = []
  for (let c = step; c < mx; c += step) arr.push(c)
  return arr
})

const margPriceLine = computed(() => {
  const vols = curveData.value?.baseline_volumes
  const prices = curveData.value?.baseline_price_m3_rp
  if (!vols?.length || !prices?.length) return ''
  const pts: string[] = []
  for (let i = 0; i < vols.length; i += 3) {
    const v = vols[i]!
    if (v > margVolMax.value) break
    pts.push(`${margCx(v)},${margCyPrice(prices[i]!)}`)
  }
  return pts.join(' ')
})

// Prix moyen avec abonnement citoyen sur le graphique marginal (linear model)
const margAvgPriceWithAboLine = computed(() => {
  if (citizenAbo.value <= 0) return ''
  const vols = curveData.value?.baseline_volumes
  if (!vols?.length) return ''
  const p0lin = curveData.value?.p0_linear || 0
  const pts: string[] = []
  for (let i = 0; i < vols.length; i += 3) {
    const v = vols[i]!
    if (v < 1 || v > margVolMax.value) continue
    const avgP = citizenAbo.value / v + p0lin
    if (avgP <= margPriceMax.value * 1.5) {
      pts.push(`${margCx(v)},${margCyPrice(avgP)}`)
    }
  }
  return pts.join(' ')
})

// Household dots for baseline bill chart (bill = abo + p0_linear * volume)
const householdDotsBill = computed(() => {
  if (!householdVolumes.value.length || !curveData.value?.p0_linear) return []
  const p0lin = curveData.value.p0_linear
  return householdVolumes.value.map(hh => {
    const abo = hh.status === 'RS' ? abos.value : abop.value
    return { volume: hh.volume_m3, bill: abo + p0lin * hh.volume_m3 }
  })
})

// Household dots for baseline price chart (price = abo/v + p0_linear)
const householdDotsBaselinePrice = computed(() => {
  if (!householdVolumes.value.length || !curveData.value?.p0_linear) return []
  const p0lin = curveData.value.p0_linear
  return householdVolumes.value
    .filter(hh => hh.volume_m3 > 0)
    .map(hh => {
      const abo = hh.status === 'RS' ? abos.value : abop.value
      return { volume: hh.volume_m3, price: abo / hh.volume_m3 + p0lin }
    })
})

// Household dots for marginal price chart (linear model: price = abop/v + p0_linear)
const householdDotsMarg = computed(() => {
  if (!householdVolumes.value.length || !curveData.value?.p0_linear) return []
  const p0lin = curveData.value.p0_linear
  return householdVolumes.value
    .filter(hh => hh.volume_m3 > 0 && hh.volume_m3 <= margVolMax.value)
    .map(hh => {
      const v = hh.volume_m3
      const abo = hh.status === 'RS' ? abos.value : abop.value
      const price = abo / v + p0lin
      return { volume: v, price }
    })
})

// ── Auth & vote ──
async function authenticate() {
  authError.value = ''
  authLoading.value = true
  try {
    const data = await api.post<{ access_token: string; role: string; commune_slug: string; volume_m3?: number }>(
      '/auth/citizen/verify',
      { commune_slug: slug, auth_code: authCode.value.toUpperCase() },
    )
    authStore.setAuth(data.access_token, data.role, data.commune_slug)
    if (data.volume_m3 != null) citizenVolume.value = data.volume_m3
  } catch (e: any) {
    authError.value = e.message || 'Code invalide'
  } finally {
    authLoading.value = false
  }
}

async function submitVote() {
  submitting.value = true
  voteSuccess.value = false
  try {
    const voteResult = await api.post<any>(`/communes/${slug}/votes`, {
      vinf: bp.vinf, a: bp.a, b: bp.b, c: bp.c, d: bp.d, e: bp.e,
    })
    voteSuccess.value = true
    if (voteResult?.submitted_at) {
      lastVoteDate.value = new Date(voteResult.submitted_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' })
    } else {
      lastVoteDate.value = new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' })
    }
  } catch (e: any) {
    alert(e.message || 'Erreur lors de la soumission')
  } finally {
    submitting.value = false
  }
}

// ── Load data ──
onMounted(async () => {
  try {
    const [c, p, curve, pages] = await Promise.all([
      api.get<any>(`/communes/${slug}`),
      api.get<any>(`/communes/${slug}/params`),
      api.get<any>(`/communes/${slug}/votes/current`),
      api.get<any[]>(`/communes/${slug}/content`).catch(() => []),
    ])
    contentPages.value = pages
    commune.value = c
    params.value = p
    curveData.value = curve

    vmax.value = p.vmax
    pmax.value = p.pmax
    recettes.value = p.recettes
    abop.value = p.abop
    abos.value = p.abos
    differentiatedTariff.value = p.differentiated_tariff || false
    zoomVolMax.value = p.vmax
    zoomPriceMax.value = p.pmax

    // Use published curve as initial state for citizen editing,
    // but display the median as the result of votes
    if (curve.published) {
      bp.vinf = curve.published.vinf
      bp.a = curve.published.a; bp.b = curve.published.b
      bp.c = curve.published.c; bp.d = curve.published.d; bp.e = curve.published.e
    } else if (curve.median) {
      bp.vinf = curve.median.vinf
      bp.a = curve.median.a; bp.b = curve.median.b
      bp.c = curve.median.c; bp.d = curve.median.d; bp.e = curve.median.e
    }
    localP0.value = curve.p0
    impacts.value = curve.impacts || []

    const stats = await api.get<any>(`/communes/${slug}/households/stats`)
    const hh: HouseholdData[] = []
    const avgVol = stats.avg_volume || 90
    for (let i = 0; i < (stats.rs_count || 0); i++) hh.push({ volume_m3: avgVol, status: 'RS' })
    for (let i = 0; i < (stats.rp_count || 0); i++) hh.push({ volume_m3: avgVol, status: 'RP' })
    for (let i = 0; i < (stats.pro_count || 0); i++) hh.push({ volume_m3: avgVol, status: 'PRO' })
    households.value = hh

    try {
      outlierVotes.value = await api.get<any[]>(`/communes/${slug}/votes/current/overlay`)
    } catch {}

    try {
      const dist = await api.get<any>(`/communes/${slug}/households/distribution?bucket_size=50`)
      distBuckets.value = dist.buckets || []
    } catch {}

    try {
      const dist30 = await api.get<any>(`/communes/${slug}/households/distribution?bucket_size=30`)
      distBuckets30.value = dist30.buckets || []
    } catch {}

    try {
      householdVolumes.value = await api.get<any[]>(`/communes/${slug}/households/volumes`)
    } catch {}

    zoomPreset('tier1')

    if (c.vote_deadline) {
      updateCountdown()
      countdownInterval = setInterval(updateCountdown, 1000)
    }
  } catch (e: any) {
    loadError.value = e.message
  } finally {
    loading.value = false
  }
})

function renderMarkdown(md: string): string {
  if (!md) return ''
  return md
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/^### (.+)$/gm, '<h3>$1</h3>')
    .replace(/^## (.+)$/gm, '<h2>$1</h2>')
    .replace(/^# (.+)$/gm, '<h1>$1</h1>')
    .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
    .replace(/\*(.+?)\*/g, '<em>$1</em>')
    .replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')
    .replace(/^- (.+)$/gm, '<li>$1</li>')
    .replace(/\n\n/g, '</p><p>')
    .replace(/^(?!<[hulo])(.+)$/gm, '<p>$1</p>')
}
</script>

<style scoped>
/* ══════════════════════════════════════
   ADAPTIVE CSS — mobile-first + clamp()
   Breakpoints: 480 / 768 / 1024
   Chart density via --chart-scale (set by DisplaySettings)
   ══════════════════════════════════════ */

/* ── Chart density support ── */
.chart-container, .chart-main {
  transform: scale(var(--chart-scale, 1));
  transform-origin: top center;
}

/* ── Chart cards ── */
.chart-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: clamp(8px, 2vw, 12px);
  padding: clamp(0.75rem, 2vw, 1.5rem);
  box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.02);
}

.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

.chart-title {
  font-size: clamp(0.95rem, 2.5vw, 1.1rem);
  font-weight: 700;
  color: var(--color-text);
  margin-bottom: 0.15rem;
}

.chart-subtitle {
  font-size: clamp(0.72rem, 1.8vw, 0.82rem);
  color: var(--color-text-muted);
}

/* ── Zoom bar ── */
.zoom-bar {
  display: flex;
  gap: 0.3rem;
  align-items: center;
  margin-bottom: 0.75rem;
  flex-wrap: wrap;
}

.zoom-info {
  font-size: 0.72rem;
  color: var(--color-text-muted);
  margin-left: 0.25rem;
}

.btn-xs {
  padding: 0.2rem 0.6rem;
  font-size: 0.72rem;
  border-radius: 6px;
  font-weight: 500;
}

.toggle-label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: clamp(0.72rem, 1.8vw, 0.8rem);
  color: var(--color-text-muted);
  cursor: pointer;
}
.toggle-label input { cursor: pointer; }

.zoom-separator {
  width: 1px;
  height: 18px;
  background: var(--color-border);
  margin: 0 0.25rem;
}
@media (max-width: 480px) {
  .zoom-separator { display: none; }
  .zoom-bar { gap: 0.2rem; }
}

.abo-input-label {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.78rem;
  color: var(--color-text-muted);
  font-weight: 500;
}
.abo-input {
  width: 56px;
  padding: 0.15rem 0.35rem;
  border: 1px solid var(--color-border);
  border-radius: 5px;
  font-size: 0.78rem;
  text-align: right;
  font-family: monospace;
  background: var(--color-surface);
  color: var(--color-text);
}
.abo-input:focus {
  outline: none;
  border-color: #2563eb;
  box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
}

/* ── SVG axes ── */
.axis-label { font-size: 10.5px; fill: var(--svg-text); font-weight: 500; font-family: system-ui, -apple-system, sans-serif; }
.axis-label-sm { font-size: 10px; fill: var(--svg-text-light); font-weight: 500; font-family: system-ui, -apple-system, sans-serif; }
.axis-title { font-size: 11px; fill: var(--svg-text-light); font-weight: 600; font-family: system-ui, -apple-system, sans-serif; }

/* ── Chart containers ── */
.chart-container svg {
  width: 100%;
  height: auto;
  user-select: none;
}

.chart-main svg {
  border-radius: 8px;
}

.chart-container svg path {
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ── Drag handles ── */
.drag-handle {
  cursor: grab;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.15));
  transition: r 0.15s ease;
}
.drag-handle:hover { r: 10; }

/* ── Main layout: chart + sidebar ── */
.main-layout {
  display: grid;
  grid-template-columns: 1fr 260px;
  gap: clamp(0.75rem, 2vw, 1rem);
  align-items: start;
  margin-bottom: 1.5rem;
}
@media (max-width: 1024px) {
  .main-layout { grid-template-columns: 1fr 220px; }
}
@media (max-width: 768px) {
  .main-layout { grid-template-columns: 1fr; }
}
.main-layout-chart { min-width: 0; }
.main-layout-sidebar {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
@media (max-width: 768px) {
  .main-layout-sidebar {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.75rem;
  }
}
@media (max-width: 480px) {
  .main-layout-sidebar { grid-template-columns: 1fr; }
}
.sidebar-card {
  padding: clamp(0.65rem, 2vw, 0.85rem) !important;
  font-size: 0.85rem;
}
.sidebar-auth-block h4 {
  font-size: 0.9rem;
  font-weight: 700;
  margin-bottom: 0.15rem;
}
.sidebar-muted {
  font-size: 0.72rem;
  color: var(--color-text-muted);
  margin-bottom: 0.5rem;
}
.countdown-compact {
  display: flex;
  gap: 0.5rem;
  font-size: 1rem;
}
.countdown-compact span strong {
  font-weight: 700;
}

/* ── Section title ── */
.section-title {
  font-size: clamp(1rem, 2.5vw, 1.15rem);
  font-weight: 700;
  color: var(--color-text);
  margin: clamp(1rem, 3vw, 1.5rem) 0 clamp(0.75rem, 2vw, 1rem);
  padding-bottom: 0.4rem;
  border-bottom: 2px solid var(--color-border);
}

/* ── Params + impacts ── */
.params-impacts-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}
@media (max-width: 768px) { .params-impacts-layout { grid-template-columns: 1fr; } }

.param-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.25rem 1rem;
}
.param-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.15rem 0;
}
.param-label { font-size: 0.75rem; color: var(--color-text-muted); }
.param-val { font-family: monospace; font-weight: 600; font-size: 0.85rem; }

.table-sm { font-size: clamp(0.72rem, 1.8vw, 0.8rem); }
.table-sm th, .table-sm td { padding: 0.3rem 0.5rem; }

.text-up { color: #dc2626; font-weight: 600; }
.text-down { color: #059669; font-weight: 600; }

/* ── Baseline charts ── */
.baseline-charts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}
@media (max-width: 768px) { .baseline-charts { grid-template-columns: 1fr; } }

/* ── Key metrics banner ── */
.key-metrics-banner {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: clamp(0.5rem, 2vw, 1rem);
  border: 1px solid var(--color-border);
  border-radius: clamp(8px, 2vw, 12px);
  padding: clamp(0.75rem, 2vw, 1.25rem) clamp(0.75rem, 2vw, 1.5rem);
}
@media (max-width: 1024px) { .key-metrics-banner { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 600px) { .key-metrics-banner { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 380px) { .key-metrics-banner { grid-template-columns: 1fr; } }

.key-metric {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.2rem;
}
.key-metric-value {
  font-size: clamp(1.1rem, 3vw, 1.4rem);
  font-weight: 700;
  color: var(--color-text);
}
.key-metric-label {
  font-size: clamp(0.68rem, 1.6vw, 0.78rem);
  color: var(--color-text-muted);
  font-weight: 500;
}
.key-metric-tag {
  font-size: 0.62rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.1rem 0.5rem;
  border-radius: 20px;
  background: var(--color-bg);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
}
.key-metric-tag-calc {
  background: var(--color-primary);
  color: white;
  border-color: var(--color-primary);
  opacity: 0.85;
}
.key-metric-input {
  opacity: 0.85;
}
.key-metric-result {}
.key-metric-highlight {
  background: rgba(5, 150, 105, 0.06);
  border-radius: 8px;
  padding: 0.5rem;
}
.key-metric-highlight .key-metric-value {
  color: #059669;
}

/* ── Vote result rows ── */
.vote-result-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: clamp(0.72rem, 1.8vw, 0.8rem);
  padding: 0.2rem 0;
  color: var(--color-text-muted);
}
.vote-result-row strong {
  font-family: monospace;
  font-size: 0.88rem;
  color: var(--color-text);
}

/* ── Alerts ── */
.alert-success {
  background: #dcfce7;
  color: #166534;
  padding: 0.5rem 0.75rem;
  border-radius: 0.375rem;
  font-size: 0.85rem;
}

/* ── Page header adaptive ── */
.page-header h1 {
  font-size: clamp(1.25rem, 4vw, 1.75rem);
}

/* ── CMS ── */
.cms-body { line-height: 1.7; font-size: clamp(0.82rem, 2vw, 0.9rem); }
.cms-body :deep(h2) { font-size: clamp(1.05rem, 2.5vw, 1.2rem); margin: 0.75rem 0 0.5rem; }
.cms-body :deep(h3) { font-size: 1.05rem; margin: 0.5rem 0 0.25rem; }
.cms-body :deep(p) { margin: 0.5rem 0; }
.cms-body :deep(a) { color: var(--color-primary); }
.cms-body :deep(ul) { margin: 0.5rem 0; padding-left: 1.5rem; }
</style>
