<template>
  <q-dialog :model-value="modelValue" @update:model-value="$emit('update:modelValue', $event)" persistent>
    <q-card style="width:650px;max-width:95vw" class="column no-wrap">
      <q-card-section class="row items-center q-pb-sm" style="border-bottom:1px solid var(--ops-border, #e2e8f0)">
        <div class="col">
          <div class="text-subtitle1 text-weight-bold">
            <q-icon name="account_tree" class="q-mr-xs" />
            {{ issue?.name ? 'Projet' : 'Nouvelle soumission' }} &mdash; {{ STEP_LABELS[currentStep] }}
          </div>
          <div class="text-caption text-grey-6">
            <template v-if="issue?.name">{{ issue.name }} &middot; {{ issue.subject }}</template>
            <template v-else-if="customer?.name">{{ customer.customer_name || customer.name }}</template>
          </div>
          <!-- Adresse qualifiée (wizard soumission) : le prix dépend de l'adresse → visible pendant toute la soumission -->
          <div v-if="customer?.qualified_address" class="text-caption row items-center no-wrap q-mt-xs" style="gap:4px;color:#0f766e">
            <q-icon name="place" size="14px" />{{ customer.qualified_address.address }}
            <q-chip v-if="customer.qualified_address.zone != null" dense square color="teal-1" text-color="teal-9" size="sm" class="q-ml-xs">Zone {{ customer.qualified_address.zone }}</q-chip>
            <q-icon :name="customer.qualified_address.fiber ? 'wifi' : 'wifi_off'" size="13px" :color="customer.qualified_address.fiber ? 'positive' : 'grey-5'" />
          </div>
        </div>
        <q-btn flat round dense icon="close" @click="cancel" />
      </q-card-section>

      <div class="row q-px-md q-pt-sm" style="gap:4px">
        <div v-for="(label, i) in STEP_LABELS" :key="i"
          class="wizard-step-dot" :class="{ active: i === currentStep, done: i < currentStep, skipped: isQuickSale && i === 1 }"
          @click="handleStepDotClick(i)">
          <div class="wizard-step-num">{{ isQuickSale && i === 1 ? '&#x2013;' : (i < currentStep ? '&#x2713;' : i + 1) }}</div>
          <div class="wizard-step-label">{{ label }}</div>
        </div>
      </div>

      <q-card-section v-if="currentStep === 0" class="col q-pt-md" style="overflow-y:auto;min-height:300px">
        <div class="text-caption text-grey-6 q-mb-sm">Que voulez-vous vendre&nbsp;?</div>

        <div class="quick-sale-card" :class="{ selected: isQuickSale }" @click="selectQuickSale">
          <div class="quick-sale-icon"><q-icon name="sell" size="26px" color="white" /></div>
          <div class="col" style="min-width:0">
            <div class="quick-sale-title">Vente rapide &mdash; sans installation</div>
            <div class="quick-sale-desc">Ajout d'équipement (ex&nbsp;: routeur, amplificateur WiFi) ou d'un service mensuel sur une adresse existante. Aucune étape d'installation.</div>
          </div>
          <q-icon name="arrow_forward" size="20px" color="primary" />
        </div>

        <div class="row items-center q-mt-md q-mb-xs">
          <div class="text-caption text-grey-7 text-weight-medium">Ou choisir un modèle d'installation&nbsp;:</div>
          <q-space />
          <FlowQuickButton flat dense size="sm" icon="tune" label="Gérer les modèles"
            category="residential" applies-to="Service Contract"
            tooltip="Ouvrir l'éditeur de modèles (créer/modifier)" />
        </div>
        <div class="template-grid">
          <div v-for="tpl in templates" :key="tpl.id" class="template-card"
            :class="{ selected: selectedTemplate?.id === tpl.id }" @click="selectTemplate(tpl)">
            <q-icon :name="tpl.icon" size="28px" :color="selectedTemplate?.id === tpl.id ? 'primary' : 'grey-6'" />
            <div class="template-card-name">{{ tpl.name }}</div>
            <div class="template-card-desc">{{ tpl.description }}</div>
            <q-badge :label="tpl.steps.length + ' étapes'" color="grey-3" text-color="grey-8" class="q-mt-xs" />
          </div>
          <div class="template-card" :class="{ selected: selectedTemplate === null && customSteps.length }"
            @click="selectCustom">
            <q-icon name="add_circle_outline" size="28px" color="grey-6" />
            <div class="template-card-name">Projet sur mesure</div>
            <div class="template-card-desc">Créer les étapes manuellement</div>
          </div>
        </div>
      </q-card-section>

      <q-card-section v-if="currentStep === 1" class="col q-pt-md" style="overflow-y:auto;min-height:300px;max-height:60vh">
        <div v-if="isQuickSale" class="quick-sale-notice">
          <q-icon name="info" size="18px" color="primary" class="q-mr-sm" />
          <div class="col">
            <div class="text-weight-bold text-green-8" style="font-size:0.85rem">Mode Vente rapide &mdash; aucune étape requise</div>
            <div class="text-caption text-grey-7">Appuyez sur «&nbsp;Suivant&nbsp;» pour aller directement au catalogue.</div>
          </div>
        </div>
        <div v-else-if="lastMergeCount > 0" class="merge-notice">
          <q-icon name="auto_awesome" size="18px" color="green-7" class="q-mr-sm" />
          <div class="col">
            <div class="text-weight-bold text-green-8" style="font-size:0.85rem">
              {{ lastMergeCount }} étape{{ lastMergeCount > 1 ? 's' : '' }} fusionnée{{ lastMergeCount > 1 ? 's' : '' }} &mdash; un seul déplacement
            </div>
            <div class="text-caption text-grey-7">Les services sélectionnés partagent la même visite d'installation.</div>
          </div>
        </div>
        <div v-else class="text-caption text-grey-6 q-mb-sm">Modifiez les étapes, dates et assignations</div>
        <div class="steps-list">
          <div v-for="(step, i) in wizardSteps" :key="i" class="step-card">
            <div class="row items-center q-mb-xs">
              <div class="step-num">{{ i + 1 }}</div>
              <q-input v-model="step.subject" dense outlined class="col q-ml-sm" placeholder="Sujet de l'étape"
                :input-style="{ fontSize: '0.85rem', fontWeight: '600' }" />
              <q-btn v-if="wizardSteps.length > 1" flat round dense icon="delete" size="sm" color="negative" class="q-ml-xs"
                @click="removeStep(i)" />
            </div>
            <div class="row q-col-gutter-sm q-mb-xs">
              <div class="col-4">
                <q-select v-model="step.job_type" dense outlined emit-value map-options label="Type"
                  :options="JOB_TYPE_OPTIONS" :input-style="{ fontSize: '0.8rem' }" />
              </div>
              <div class="col-4">
                <q-select v-model="step.priority" dense outlined emit-value map-options label="Priorité"
                  :options="PRIORITY_OPTIONS" :input-style="{ fontSize: '0.8rem' }" />
              </div>
              <div class="col-4">
                <q-input v-model.number="step.duration_h" dense outlined type="number" label="Durée (h)"
                  :input-style="{ fontSize: '0.8rem' }" />
              </div>
            </div>
            <div class="row q-col-gutter-sm q-mb-xs">
              <div class="col-4">
                <q-select v-model="step.assigned_group" dense outlined emit-value map-options label="Groupe"
                  :options="groupOptions" clearable :input-style="{ fontSize: '0.8rem' }" />
              </div>
              <div class="col-4">
                <q-input v-model="step.scheduled_date" dense outlined type="date" label="Date"
                  :input-style="{ fontSize: '0.8rem' }" />
              </div>
              <div class="col-4">
                <q-select v-model="step.depends_on_step" dense outlined emit-value map-options label="Après"
                  :options="dependencyOptionsFor(i)" clearable :input-style="{ fontSize: '0.8rem' }" />
              </div>
            </div>
            <q-expansion-item dense dense-toggle label="Webhooks n8n" icon="webhook"
              header-class="text-caption text-grey-6" style="margin:-4px -4px 0">
              <div class="row q-col-gutter-sm q-pa-sm">
                <div class="col-6">
                  <q-input v-model="step.on_open_webhook" dense outlined label="À l'ouverture"
                    placeholder="https://n8n.../webhook/..." :input-style="{ fontSize: '0.75rem' }" />
                </div>
                <div class="col-6">
                  <q-input v-model="step.on_close_webhook" dense outlined label="À la fermeture"
                    placeholder="https://n8n.../webhook/..." :input-style="{ fontSize: '0.75rem' }" />
                </div>
              </div>
            </q-expansion-item>
            <div v-if="step.depends_on_step != null" class="step-dep-indicator">
              <q-icon name="subdirectory_arrow_right" size="14px" color="warning" />
              <span class="text-caption text-warning">Après: étape {{ step.depends_on_step + 1 }}</span>
            </div>
          </div>
        </div>
        <q-btn flat dense icon="add" label="Ajouter une étape" color="primary" no-caps class="q-mt-sm"
          @click="addStep" />
      </q-card-section>

      <q-card-section v-if="currentStep === 2" class="col q-pt-md cart-scroll-host" style="overflow-y:auto;min-height:300px;max-height:60vh">
        <div class="row q-mb-sm q-gutter-sm items-center">
          <q-btn-toggle v-model="orderMode" no-caps dense unelevated toggle-color="primary" color="grey-3" text-color="grey-8"
            :options="ORDER_MODE_OPTIONS" />
          <q-space />
          <q-btn unelevated icon="inventory_2" label="Catalogue" color="primary" no-caps size="sm" class="catalog-open-btn"
            @click="openCatalog()" />
        </div>

        <!-- Sticky cart pill — combined summary (count + totals + taxes).
             Sits at the top of the scrollable area so the running tally is
             always visible while the rep picks forfaits below. The full
             line-item editor lives on the Sommaire step (click → navigate). -->
        <div class="cart-pill cart-pill-sticky" :class="{ 'cart-pill-empty': !orderItems.length }"
          @click="goToSommaire">
          <div class="cart-pill-left">
            <q-icon name="shopping_cart" size="22px"
              :color="orderItems.length ? 'primary' : 'grey-5'" />
            <q-badge v-if="orderItems.length" floating color="warning" text-color="white"
              class="cart-pill-badge">{{ orderItems.length }}</q-badge>
          </div>
          <div class="cart-pill-body">
            <div class="cart-pill-title">
              <template v-if="orderItems.length">
                Panier — {{ orderItems.length }} item{{ orderItems.length > 1 ? 's' : '' }}
              </template>
              <template v-else>Panier vide</template>
            </div>
            <div class="cart-pill-sub">
              <template v-if="orderItems.length">
                <span class="text-primary">Voir le sommaire →</span>
                <span class="text-grey-6"> modifier, réordonner, supprimer</span>
              </template>
              <template v-else>
                <span class="text-grey-6">Choisissez un forfait Internet ci-dessous pour commencer</span>
              </template>
            </div>
          </div>
          <div v-if="orderItems.length" class="cart-pill-totals">
            <div v-if="recurringTotal > 0" class="cart-pill-total-line cart-pill-total-recurring">
              {{ recurringTotal.toFixed(2) }}$<span class="cart-pill-mo">/mois</span>
            </div>
            <div v-if="onetimeTotal > 0" class="cart-pill-total-line">
              {{ onetimeTotal.toFixed(2) }}$<span class="cart-pill-mo">unique</span>
            </div>
            <div class="cart-pill-tax">
              +{{ ((onetimeTotal + recurringTotal) * 0.14975).toFixed(2) }}$ tx
            </div>
          </div>
          <q-icon v-if="orderItems.length" name="arrow_forward" size="18px" color="primary" />
        </div>

        <!-- Égaler un concurrent (terrain) : total mensuel du concurrent → rabais promo récurrent (rab-x) sur l'Internet, 24 mois -->
        <div v-if="orderItems.length" class="cw-compet q-mt-sm q-pa-sm">
          <div class="row items-center no-wrap q-gutter-sm">
            <q-icon name="sell" color="deep-orange-6" size="20px" />
            <div class="text-caption text-weight-medium col">Égaler un concurrent<div class="text-caption text-grey-6" style="font-weight:400">Total mensuel du concurrent → rabais 24 mois sur l'Internet</div></div>
            <q-input dense outlined v-model.number="competitorPrice" type="number" step="0.01" min="0" suffix="$/mois" style="width:118px" placeholder="total" @keyup.enter="applyCompetitor" />
            <q-btn dense unelevated no-caps color="deep-orange-6" label="Égaler" :disable="!(competitorPrice > 0)" @click="applyCompetitor" />
          </div>
          <div v-if="competitorInfo" class="row items-center no-wrap q-mt-xs" :class="competitorInfo.ok ? 'text-positive' : 'text-grey-7'">
            <q-icon :name="competitorInfo.ok ? 'check_circle' : 'info'" size="15px" class="q-mr-xs" />
            <span class="text-caption col">{{ competitorInfo.msg }}</span>
            <q-btn v-if="competitorInfo.ok" flat dense no-caps size="sm" color="grey-7" label="Retirer" @click="removeCompetitor" />
          </div>
        </div>

        <!-- ──────────────────────────────────────────────────────────────
             ÉTAPE 1 — Internet (obligatoire, accordion list)
             Same accordion pattern as TV/Phone: clicking the header toggles
             the tier list, selecting a tier auto-closes the panel. The
             header summary becomes the anchor showing what's in the cart
             once a selection lands. ───────────────────────────────────── -->
        <section class="wizard-step-block wizard-step-block-required"
          :class="{ 'wizard-step-block-expanded': heroExpanded }">
          <header class="wizard-step-header wizard-step-header-clickable"
            @click="toggleHeroGroup('internet')">
            <div class="wizard-step-number">1</div>
            <div class="wizard-step-headline">
              <div class="wizard-step-title">Internet</div>
              <div class="wizard-step-summary">
                <template v-if="selectedHeroTier">
                  <q-icon name="check_circle" size="14px" color="green-6" class="q-mr-xs" />
                  <strong class="text-green-8">{{ selectedHeroTier.label }}</strong>
                  <span class="text-grey-7"> · {{ selectedHeroTier.price_effective.toFixed(2) }}$/mois</span>
                  <span v-if="internetContractMonths" class="text-grey-6"> · contrat {{ internetContractMonths }} mois</span>
                  <span v-if="selectedHeroTier.speed" class="text-grey-6"> · {{ selectedHeroTier.speed }}</span>
                </template>
                <template v-else>
                  <q-icon name="flash_on" size="14px" color="warning" class="q-mr-xs" />
                  <span class="text-warning">Obligatoire — à partir de 39.95$/mois · cliquez pour choisir un forfait</span>
                </template>
              </div>
            </div>
            <div class="wizard-step-action">
              <q-icon name="expand_more" size="20px" color="grey-6"
                class="preset-hero-chevron" :class="{ 'rotated': heroExpanded }" />
              <q-icon :name="selectedHeroTier ? 'check_circle' : 'radio_button_unchecked'"
                size="22px"
                :color="selectedHeroTier ? 'green-6' : 'amber-5'" />
            </div>
          </header>

          <!-- Internet tiers as a vertical list inside the expand panel.
               Selection closes the accordion (see onInternetTierClick). -->
          <div v-show="heroExpanded" class="tier-list">
            <div v-for="t in internetHeroTiers" :key="t.id"
              class="tier-row"
              :class="{ 'tier-row-active': selectedHeroTier && selectedHeroTier.code === t.code }"
              @click.stop="onInternetTierClick(t)">
              <div class="tier-row-icon">
                <q-icon :name="t.icon" size="24px"
                  :color="selectedHeroTier && selectedHeroTier.code === t.code ? 'green-6' : 'primary'" />
              </div>
              <div class="tier-row-body">
                <div class="tier-row-head">
                  <span class="tier-row-title">{{ t.label }}</span>
                  <q-badge v-if="t.badge" :color="t.id === 'tier_g1500' ? 'deep-orange-5' : 'blue-grey-5'"
                    text-color="white" class="tier-row-badge">
                    {{ t.badge }}
                  </q-badge>
                  <span class="tier-row-speed">· {{ t.speed }}</span>
                </div>
                <div class="tier-row-desc">{{ t.desc }}</div>
              </div>
              <div class="tier-row-price">
                <div class="tier-row-price-big">
                  {{ t.price_effective.toFixed(2) }}<span class="tier-row-price-unit">$/mois</span>
                </div>
                <div v-if="t.price_base > t.price_effective" class="tier-row-price-strike">
                  {{ t.price_base.toFixed(2) }}$
                </div>
              </div>
              <div class="tier-row-select">
                <q-icon :name="selectedHeroTier && selectedHeroTier.code === t.code ? 'check_circle' : 'radio_button_unchecked'"
                  size="22px"
                  :color="selectedHeroTier && selectedHeroTier.code === t.code ? 'green-6' : 'grey-4'" />
              </div>
            </div>
          </div>
        </section>

        <!-- ──────────────────────────────────────────────────────────────
             ÉTAPE 2 — Services additionnels (optionnels)
             Each sub-group is a click-to-expand accordion. The summary line
             reflects what's currently in the cart, so the rep sees a
             one-line recap above the detail cards. ──────────────────────── -->
        <section class="wizard-step-block q-mt-md">
          <header class="wizard-step-header">
            <div class="wizard-step-number wizard-step-number-optional">2</div>
            <div class="wizard-step-headline">
              <div class="wizard-step-title">Services additionnels</div>
              <div v-if="!step2Summary.length" class="wizard-step-summary">
                <span class="text-grey-6">Optionnels — Télé dès 25$/mois · Téléphonie +10$/mois · WiFi booster, etc. · rabais combo auto dès 2 services</span>
              </div>
            </div>
          </header>

          <!-- TV hero: anchor tile + click-expand panel. Gated behind Internet
               (can't provide TV without a same-address Internet subscription). -->
          <div class="preset-hero-group q-mt-sm"
            :class="{
              'preset-hero-group-expanded': tvExpanded,
              'preset-hero-group-disabled': !selectedHeroTier,
            }">
            <div class="preset-hero preset-hero-tv"
              :class="{ 'preset-hero-added': !!selectedTvTier }"
              @click="toggleHeroGroup('tv')">
              <div class="preset-hero-icon">
                <q-icon :name="tvAnchor.icon" size="28px"
                  :color="selectedTvTier ? 'green-6' : 'pink-6'" />
              </div>
              <div class="preset-hero-text">
                <div class="preset-hero-label">
                  Télé <span class="preset-hero-label-tier preset-hero-label-tv">{{ tvAnchor.label }}</span>
                  <q-badge v-if="tvAnchor.badge" color="pink-5" class="q-ml-xs" text-color="white">
                    {{ tvAnchor.badge }}
                  </q-badge>
                </div>
                <div class="preset-hero-desc">{{ tvAnchor.desc }}</div>
              </div>
              <div class="preset-hero-price">
                <div class="preset-hero-price-tag">à partir de</div>
                <div class="preset-hero-price-big preset-hero-price-tv">
                  {{ (selectedTvTier || tvHeroTiers[0]).price_effective.toFixed(2) }}<span class="preset-hero-price-unit">$/mois</span>
                </div>
              </div>
              <div class="preset-hero-action">
                <q-icon name="expand_more" size="18px" color="grey-6"
                  class="preset-hero-chevron" :class="{ 'rotated': tvExpanded }" />
                <q-icon :name="selectedTvTier ? 'check_circle' : 'add_circle'"
                  size="22px" :color="selectedTvTier ? 'green-6' : 'pink-4'" />
              </div>
            </div>

            <div v-show="tvExpanded" class="tier-expand tier-expand-tv">
              <div v-for="t in tvHeroTiers" :key="t.id" class="tier-card"
                :class="{
                  'tier-card-active': selectedTvTier && selectedTvTier.id === t.id,
                  'tier-card-preview': !t.ready,
                }"
                @click.stop="onTvTierClick(t)">
                <div class="tier-card-head">
                  <q-icon :name="t.icon" size="20px"
                    :color="selectedTvTier && selectedTvTier.id === t.id ? 'green-6' : 'pink-6'" />
                  <div class="tier-card-title">{{ t.label }}</div>
                  <q-badge v-if="t.badge" color="pink-5" text-color="white" class="tier-card-badge">
                    {{ t.badge }}
                  </q-badge>
                  <q-badge v-else-if="!t.ready" color="grey-5" text-color="white" class="tier-card-badge">
                    À venir
                  </q-badge>
                </div>
                <div class="tier-card-desc">{{ t.desc }}</div>
                <div v-if="t.picks_allowed" class="tier-card-speed">
                  <q-icon name="check_circle_outline" size="13px" color="pink-5" class="q-mr-xs" />
                  {{ t.picks_allowed }} chaînes au choix
                </div>
                <div class="tier-card-price">
                  <template v-if="t.price_effective">
                    <span class="tier-card-price-big">{{ t.price_effective.toFixed(2) }}$</span>
                    <span class="tier-card-price-unit">/mois</span>
                  </template>
                  <span v-else class="tier-card-price-unit">selon les chaînes choisies</span>
                </div>
                <q-icon v-if="selectedTvTier && selectedTvTier.id === t.id"
                  name="check_circle" size="18px" color="green-6" class="tier-card-check" />
              </div>
            </div>
          </div>

          <!-- Téléphonie hero group: click-expand with two options.
               Nouveau numéro = add the preset directly. Conservation = add
               the preset + inject a port-in "collecter preuve" tech step. -->
          <div class="preset-hero-group q-mt-sm"
            :class="{ 'preset-hero-group-expanded': phoneExpanded }">
            <div class="preset-hero preset-hero-phone"
              :class="{ 'preset-hero-added': phoneApplied }"
              @click="toggleHeroGroup('phone')">
              <div class="preset-hero-icon">
                <q-icon name="phone" size="28px"
                  :color="phoneApplied ? 'green-6' : 'teal-6'" />
              </div>
              <div class="preset-hero-text">
                <div class="preset-hero-label">
                  Téléphonie
                  <span v-if="phoneApplied" class="preset-hero-label-tier preset-hero-label-phone">
                    · {{ phoneMode === 'keep' ? 'Conservation' : 'Nouveau numéro' }}
                    <template v-if="phoneMode === 'keep' && phoneKeepNumber">
                      · {{ phoneKeepNumber }}
                    </template>
                  </span>
                </div>
                <div class="preset-hero-desc">
                  <template v-if="phoneApplied && phoneMode === 'keep' && !phoneKeepNumber">
                    <q-icon name="warning" size="12px" color="warning" class="q-mr-xs" />
                    <span class="text-warning">Cliquez pour saisir le numéro à conserver</span>
                  </template>
                  <template v-else>Illimitée CA/US · bonification combo</template>
                </div>
              </div>
              <div class="preset-hero-price">
                <div class="preset-hero-price-tag">à partir de</div>
                <div class="preset-hero-price-big preset-hero-price-phone">
                  +10.00<span class="preset-hero-price-unit">$/mois</span>
                </div>
              </div>
              <div class="preset-hero-action">
                <q-icon name="expand_more" size="18px" color="grey-6"
                  class="preset-hero-chevron" :class="{ 'rotated': phoneExpanded }" />
                <q-icon :name="phoneApplied ? 'check_circle' : 'add_circle'"
                  size="22px" :color="phoneApplied ? 'green-6' : 'teal-4'" />
              </div>
            </div>

            <div v-show="phoneExpanded" class="tier-expand tier-expand-phone">
              <div class="tier-card"
                :class="{ 'tier-card-active': phoneApplied && phoneMode === 'new' }"
                @click.stop="applyPhonePreset('new')">
                <div class="tier-card-head">
                  <q-icon name="dialpad" size="20px"
                    :color="phoneApplied && phoneMode === 'new' ? 'green-6' : 'teal-6'" />
                  <div class="tier-card-title">Nouveau numéro</div>
                </div>
                <div class="tier-card-desc">Attribution automatique à l'installation — indicatif local.</div>
                <div class="tier-card-price">
                  <span class="tier-card-price-big">+10.00$</span>
                  <span class="tier-card-price-unit">/mois</span>
                </div>
                <q-icon v-if="phoneApplied && phoneMode === 'new'"
                  name="check_circle" size="18px" color="green-6" class="tier-card-check" />
              </div>

              <div class="tier-card"
                :class="{ 'tier-card-active': phoneApplied && phoneMode === 'keep' }"
                @click.stop="applyPhonePreset('keep')">
                <div class="tier-card-head">
                  <q-icon name="swap_calls" size="20px"
                    :color="phoneApplied && phoneMode === 'keep' ? 'green-6' : 'teal-6'" />
                  <div class="tier-card-title">Conservation (port-in)</div>
                  <q-badge color="warning" text-color="white" class="tier-card-badge">
                    Preuve requise
                  </q-badge>
                </div>
                <div class="tier-card-desc">
                  Le client garde son numéro actuel. Le tech doit photographier la facture du fournisseur actuel comme preuve de propriété.
                </div>
                <div v-if="phoneApplied && phoneMode === 'keep'" class="tier-card-keep-input" @click.stop>
                  <q-input v-model="phoneKeepNumber" dense outlined
                    placeholder="Numéro à conserver (ex. 514-555-1234)"
                    :input-style="{ fontSize: '0.82rem' }"
                    @click.stop />
                </div>
                <div class="tier-card-price">
                  <span class="tier-card-price-big">+10.00$</span>
                  <span class="tier-card-price-unit">/mois</span>
                </div>
                <q-icon v-if="phoneApplied && phoneMode === 'keep'"
                  name="check_circle" size="18px" color="green-6" class="tier-card-check" />
              </div>
            </div>
          </div>

          <!-- Remaining upsells (WiFi booster etc.) -->
          <div class="row q-gutter-sm q-mt-sm items-stretch">
            <div v-for="p in otherUpsells" :key="p.id" class="preset-upsell"
              :class="[`tier-${p.tier}`, { 'preset-added': usedServiceTypes.has(p.service_type) }]"
              @click="applyPreset(p)">
              <q-icon :name="p.icon" size="18px"
                :color="usedServiceTypes.has(p.service_type) ? 'green-6' : 'primary'" />
              <div class="preset-upsell-body">
                <div class="preset-upsell-delta">
                  +{{ p.price_delta.toFixed(2) }}<span class="preset-upsell-mo">$/mois</span>
                </div>
                <div class="preset-upsell-label">{{ p.label }}</div>
              </div>
              <q-icon :name="usedServiceTypes.has(p.service_type) ? 'check_circle' : 'add'"
                size="16px" :color="usedServiceTypes.has(p.service_type) ? 'green-6' : 'grey-5'" />
            </div>
          </div>
        </section>

        <!-- Referral code — own block, not nested inside the two step blocks -->
        <div class="referral-row q-mt-md">
          <q-icon name="redeem" size="18px" color="deep-purple-5" class="q-mr-sm" />
          <div class="col" style="min-width:0">
            <div class="referral-title">Code de référence</div>
            <div class="referral-desc">50$ de crédit pour le nouvel abonné · 50$ pour le parrain à l'installation</div>
          </div>
          <q-input v-model="referralCode" dense outlined placeholder="Ex. GIGA-ABC123"
            :readonly="referralApplied" style="max-width:160px"
            :input-style="{ fontSize: '0.82rem' }"
            @keyup.enter="applyReferralCode" />
          <q-btn v-if="!referralApplied" unelevated dense no-caps color="deep-purple-5"
            text-color="white" size="sm" icon-right="check"
            :loading="referralChecking" label="Appliquer"
            :disable="!referralCode.trim()" @click="applyReferralCode" />
          <q-btn v-else flat dense no-caps color="deep-purple-7" size="sm"
            icon="check_circle" label="-50$ appliqué" @click="removeReferralCode"
            title="Retirer le crédit" />
        </div>
        <div v-if="referralError" class="referral-error">
          <q-icon name="error" size="13px" /> {{ referralError }}
        </div>

        <!-- Installation financée — créance réelle créditée chaque mois (conforme CRTC, pas de clawback) -->
        <div class="referral-row q-mt-md" style="border-color:#bde5cb;background:#f4fff6">
          <q-icon name="construction" size="18px" color="green-7" class="q-mr-sm" />
          <div class="col" style="min-width:0">
            <div class="referral-title" style="color:#00733a">Installation</div>
            <div class="referral-desc">Financée sur {{ maxContractMonths }} mois, créditée chaque mois — 0&nbsp;$ net pour le client tant qu'abonné</div>
          </div>
          <div class="row q-gutter-xs items-center no-wrap">
            <q-btn v-for="p in INSTALL_PRESETS" :key="p.key" dense no-caps size="sm"
              :outline="!isInstallPreset(p)" :unelevated="isInstallPreset(p)"
              color="green-7" :text-color="isInstallPreset(p) ? 'white' : 'green-8'"
              :label="p.label" @click="setInstallPreset(p)" />
          </div>
        </div>
        <div v-if="installFee > 0 || installRegular > 0" class="row q-col-gutter-sm q-mt-xs items-center">
          <div class="col-4">
            <q-input v-model.number="installRegular" dense outlined type="number" step="0.01" min="0"
              label="Prix original" :input-style="{ fontSize: '0.78rem' }"
              title="Prix barré affiché au client (0 = aucun barré)">
              <template v-slot:append><q-icon name="local_offer" size="13px" color="warning" /></template>
            </q-input>
          </div>
          <div class="col-4">
            <q-input v-model.number="installFee" dense outlined type="number" step="0.01" min="0"
              label="Prix financé $" :input-style="{ fontSize: '0.78rem' }"
              title="Montant réellement financé/dû (créance). 0 = installation gratuite" />
          </div>
          <div v-if="installFee > 0" class="col-4" style="font-size:0.76rem">
            <div v-if="installRegular > installFee">
              <s style="color:#9ca3af">{{ Number(installRegular).toFixed(2) }}$</s>
              <b class="text-green-8 q-ml-xs">{{ Number(installFee).toFixed(2) }}$</b>
            </div>
            <div class="text-grey-7">+{{ (Number(installFee) / (maxContractMonths || 24)).toFixed(2) }}$/mois · crédité → <b class="text-green-8">0$</b></div>
          </div>
        </div>

        <!-- "Autre item" + "Item manuel" shortcuts live at the bottom —
             the sticky cart pill at the top handles navigation to Sommaire
             which is where cart details and totals are edited/reviewed. -->
        <div class="row q-gutter-sm q-mt-md q-mb-sm">
          <q-btn flat dense icon="add" label="Autre item du catalogue" color="primary" no-caps size="sm"
            @click="openCatalog()" />
          <q-btn flat dense icon="edit" label="Item manuel" color="grey-7" no-caps size="sm"
            @click="addOrderItem" />
        </div>

        <q-expansion-item dense dense-toggle label="Conditions & contrat" icon="gavel"
          header-class="text-caption text-grey-6 q-mt-sm" default-opened>
          <div class="q-pa-sm">
            <q-input v-model="contractNotes" dense outlined type="textarea" placeholder="Conditions spéciales, annexe au contrat maître..."
              :input-style="{ fontSize: '0.8rem', minHeight: '48px' }" />
            <q-checkbox v-model="requireAcceptance" dense label="Requiert acceptation client avant exécution"
              class="q-mt-xs" style="font-size:0.78rem" />
            <div v-if="requireAcceptance" class="q-mt-sm q-gutter-sm">
              <q-btn-toggle v-model="acceptanceMethod" no-caps dense unelevated size="sm"
                toggle-color="primary" color="grey-3" text-color="grey-8" :options="ACCEPTANCE_METHOD_OPTIONS" />
              <div class="row q-gutter-sm q-mt-xs">
                <q-input v-model="clientPhone" dense outlined placeholder="+15145551234" label="Téléphone"
                  class="col" :input-style="{ fontSize: '0.8rem' }" />
                <q-input v-model="clientEmail" dense outlined placeholder="client@example.com" label="Courriel"
                  class="col" :input-style="{ fontSize: '0.8rem' }" />
              </div>
              <div class="text-caption text-grey-6 q-mt-xs">
                {{ acceptanceMethod === 'docuseal'
                  ? 'Le client recevra un lien DocuSeal pour signer électroniquement le contrat + conditions.'
                  : 'Le client recevra un lien pour voir le devis et cliquer "J\'accepte" avec horodatage + IP.' }}
              </div>
            </div>
          </div>
        </q-expansion-item>
      </q-card-section>

      <!-- ──────────────────────────────────────────────────────────────
           Step 3 — Sommaire (cart detail + inline editor + drag-drop)
           Each row collapses by default to a compact summary; clicking the
           row expands an inline editor (name, qty, rate, billing, contract
           months, regular price, bundle steps). Drag the left handle to
           reorder — item order drives the quote's line order downstream. ─── -->
      <q-card-section v-if="currentStep === 3" class="col q-pt-md" style="overflow-y:auto;min-height:300px;max-height:60vh">
        <div class="row items-center q-mb-sm">
          <q-icon name="shopping_cart" size="18px" color="primary" class="q-mr-xs" />
          <div class="text-weight-bold text-green-8" style="font-size:0.9rem">
            Sommaire &mdash; {{ orderItems.length }} item{{ orderItems.length > 1 ? 's' : '' }}
          </div>
          <q-space />
          <q-btn flat dense no-caps size="sm" icon="arrow_back" label="Retour au catalogue"
            color="grey-7" @click="currentStep = 2" />
        </div>

        <div v-if="!orderItems.length" class="sommaire-empty">
          <q-icon name="shopping_cart_checkout" size="40px" color="grey-4" />
          <div class="text-grey-6 q-mt-sm" style="font-size:0.85rem">Aucun item dans le panier</div>
          <q-btn unelevated color="primary" no-caps icon="add_shopping_cart" size="sm"
            label="Retour au catalogue" class="q-mt-sm" @click="currentStep = 2" />
        </div>

        <div v-else class="sommaire-list">
          <div v-for="(item, i) in orderItems" :key="item._uid || i"
            class="sommaire-row"
            :class="{
              'sommaire-row-expanded': expandedSommaireIdx === i,
              'sommaire-row-dragging': dragIndex === i,
              'sommaire-row-drop-target': dragOverIndex === i && dragIndex !== i,
            }"
            draggable="true"
            @dragstart="onSommaireDragStart(i, $event)"
            @dragend="onSommaireDragEnd"
            @dragover.prevent="onSommaireDragOver(i, $event)"
            @dragleave="onSommaireDragLeave(i)"
            @drop.prevent="onSommaireDrop(i)">
            <!-- Compact header (always visible) -->
            <div class="sommaire-head" @click="toggleSommaireExpand(i)">
              <div class="sommaire-handle" title="Glisser pour réordonner"
                @click.stop>
                <q-icon name="drag_indicator" size="18px" color="grey-5" />
              </div>
              <div class="sommaire-badge">{{ i + 1 }}</div>
              <q-icon :name="item.billing === 'recurring' ? 'autorenew' : 'shopping_cart'" size="16px"
                :color="item.billing === 'recurring' ? 'orange-7' : 'primary'" class="sommaire-type-icon" />
              <div class="sommaire-main">
                <div class="sommaire-name">{{ item.item_name || 'Item sans nom' }}</div>
                <div class="sommaire-meta">
                  <span>Qté {{ item.qty }}</span>
                  <span class="sommaire-sep">·</span>
                  <span class="text-weight-bold">{{ (item.qty * item.rate).toFixed(2) }}$</span>
                  <span v-if="item.billing === 'recurring'" class="text-warning">/mois</span>
                  <template v-if="item.billing === 'recurring' && item.contract_months">
                    <span class="sommaire-sep">·</span>
                    <span class="text-grey-6">{{ item.contract_months }} mois</span>
                  </template>
                  <template v-if="item.regular_price > item.rate">
                    <span class="sommaire-sep">·</span>
                    <q-chip dense square color="amber-1" text-color="amber-9"
                      icon="celebration" size="sm" style="margin:0">
                      Promo {{ ((item.regular_price - item.rate) * item.qty).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}
                    </q-chip>
                  </template>
                </div>
              </div>
              <q-btn flat round dense size="sm" icon="delete" color="negative"
                @click.stop="deleteSommaireItem(i)" title="Supprimer" />
              <q-icon :name="expandedSommaireIdx === i ? 'expand_less' : 'expand_more'"
                size="18px" color="grey-6" />
            </div>

            <!-- Expanded editor -->
            <div v-if="expandedSommaireIdx === i" class="sommaire-body" @click.stop>
              <div class="row q-col-gutter-sm items-center">
                <div class="col-12">
                  <q-input v-model="item.item_name" dense outlined placeholder="Produit / Service"
                    :input-style="{ fontSize: '0.82rem' }"
                    :readonly="!!item.item_code && !item.manual_edit"
                    :class="{ 'catalog-bound': !!item.item_code && !item.manual_edit }"
                    @click="onItemFieldClick(i, $event)">
                    <template v-slot:prepend>
                      <q-icon :name="item.billing === 'recurring' ? 'autorenew' : 'shopping_cart'" size="16px"
                        :color="item.billing === 'recurring' ? 'orange-7' : 'primary'" />
                    </template>
                    <template v-slot:append>
                      <q-btn v-if="item.item_code" flat dense round size="sm" icon="swap_horiz" color="primary"
                        @click.stop="openCatalogForLine(i)" title="Changer du catalogue" />
                      <q-btn v-else flat dense round size="sm" icon="inventory_2" color="primary"
                        @click.stop="openCatalogForLine(i)" title="Choisir du catalogue" />
                      <q-btn flat dense round size="xs" icon="edit" color="grey-6"
                        @click.stop="enableManualEdit(i)"
                        :title="item.manual_edit ? 'Édition manuelle active' : 'Modifier le nom manuellement'" />
                    </template>
                  </q-input>
                </div>
              </div>
              <div class="row q-col-gutter-sm q-mt-xs items-center">
                <div :class="item.billing === 'recurring' ? 'col-2' : 'col-3'">
                  <q-input v-model.number="item.qty" dense outlined type="number" min="1"
                    label="Qté" :input-style="{ fontSize: '0.82rem' }" />
                </div>
                <div :class="item.billing === 'recurring' ? 'col-4' : 'col-5'">
                  <q-input v-model.number="item.rate" dense outlined type="number" step="0.01"
                    label="Prix marketing $" :input-style="{ fontSize: '0.82rem' }"
                    title="Prix réellement facturé / affiché au client">
                    <template v-slot:append>
                      <button type="button" class="billing-pill" :class="billingPillClass(item)"
                        @click.stop="cycleBilling(item)"
                        :title="'Cliquer pour changer — prochain: ' + nextBillingLabel(item)">
                        {{ billingPillLabel(item) }}
                        <q-icon name="swap_horiz" size="11px" class="q-ml-xs" />
                      </button>
                    </template>
                  </q-input>
                </div>
                <div v-if="item.billing === 'recurring'" class="col-3">
                  <q-input v-model.number="item.contract_months"
                    dense outlined type="number" label="Mois" title="Durée du contrat en mois"
                    :input-style="{ fontSize: '0.78rem', textAlign: 'center' }" />
                </div>
                <div :class="item.billing === 'recurring' ? 'col-3' : 'col-4'">
                  <q-input v-model.number="item.regular_price" dense outlined type="number" step="0.01"
                    label="Prix original" placeholder="vide = aucun"
                    :input-style="{ fontSize: '0.75rem' }"
                    title="Prix barré affiché au client. Laisser vide (ou ≤ prix marketing) = aucun prix barré.">
                    <template v-slot:append>
                      <q-icon name="local_offer" size="14px" color="warning" />
                    </template>
                  </q-input>
                </div>
              </div>
              <!-- Aperçu client WYSIWYG : le prix marketing barre le prix original -->
              <div v-if="Number(item.regular_price) > Number(item.rate)" class="q-mt-xs"
                style="font-size:0.78rem;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
                <span class="text-grey-6">Aperçu client :</span>
                <span style="text-decoration:line-through;color:#9ca3af">{{ Number(item.regular_price).toFixed(2) }}$</span>
                <span class="text-weight-bold text-green-8">{{ Number(item.rate).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}</span>
                <q-chip dense square color="amber-1" text-color="amber-9" size="sm" style="margin:0">
                  −{{ ((Number(item.regular_price) - Number(item.rate)) * (item.qty || 1)).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}
                </q-chip>
              </div>

              <!-- Bundle steps (installation pill) kept here so editing a
                   service still lets the rep tweak which install steps come
                   with it. -->
              <div v-if="item.project_template_id" class="item-steps-wrap q-mt-xs">
                <template v-if="!templateLoadedFor.has(item.project_template_id)">
                  <q-btn flat dense size="sm" icon="auto_fix_high" no-caps color="primary"
                    label="Charger les étapes d'installation" @click="loadTemplateFromItem(item)" />
                </template>
                <template v-else>
                  <button type="button" class="item-steps-pill"
                    :class="{ expanded: expandedBundles.has(item.project_template_id) }"
                    @click="toggleBundleExpanded(item.project_template_id)">
                    <q-icon name="account_tree" size="12px" class="q-mr-xs" />
                    <span>{{ bundleStepCount(item.project_template_id) }} étape{{ bundleStepCount(item.project_template_id) > 1 ? 's' : '' }} d'installation</span>
                    <q-icon :name="expandedBundles.has(item.project_template_id) ? 'expand_less' : 'expand_more'"
                      size="14px" class="q-ml-xs" />
                  </button>
                </template>
              </div>
            </div>
          </div>
        </div>

        <!-- Running totals — mirrors Step 2 so rep can sanity-check before
             publishing. Kept compact and right-aligned. -->
        <div v-if="orderItems.length" class="totals-box q-mt-md">
          <div class="row justify-between" style="font-size:0.82rem">
            <span class="text-grey-7">Sous-total unique</span>
            <span class="text-weight-bold">{{ onetimeTotal.toFixed(2) }} $</span>
          </div>
          <div v-if="stepsExtraTotal > 0" class="row justify-between q-pl-md" style="font-size:0.76rem">
            <span class="text-grey-6"><q-icon name="add_circle_outline" size="12px" color="warning" /> dont extras sur étapes</span>
            <span class="text-warning">{{ stepsExtraTotal.toFixed(2) }} $</span>
          </div>
          <div class="row justify-between" style="font-size:0.82rem">
            <span class="text-grey-7">Récurrent mensuel</span>
            <span class="text-weight-bold text-warning">{{ recurringTotal.toFixed(2) }} $/mois</span>
          </div>
          <div v-if="promoTotal > 0" class="row justify-between" style="font-size:0.82rem">
            <span class="text-warning"><q-icon name="celebration" size="14px" /> Valeur des promotions étalées</span>
            <span class="text-weight-bold text-warning">{{ promoTotal.toFixed(2) }} $</span>
          </div>
          <q-separator class="q-my-xs" />
          <div class="row justify-between" style="font-size:0.82rem">
            <span class="text-grey-7">Taxes (TPS+TVQ ~14.975%)</span>
            <span>{{ ((onetimeTotal + recurringTotal) * 0.14975).toFixed(2) }} $</span>
          </div>
        </div>
      </q-card-section>

      <q-card-section v-if="currentStep === 4" class="col q-pt-md" style="overflow-y:auto;min-height:300px;max-height:60vh">
        <div class="text-caption text-grey-6 q-mb-md">Vérifiez le projet avant publication</div>
        <div v-if="orderItems.length" class="ops-card q-mb-md" style="padding:10px 12px;border-color:#c7d2fe">
          <div class="text-weight-bold text-green-8" style="font-size:0.82rem;margin-bottom:6px;">
            <q-icon name="receipt_long" size="16px" class="q-mr-xs" />
            {{ orderMode === 'quotation' ? 'Devis' : orderMode === 'prepaid' ? 'Facture' : 'Bon de commande' }}
            &mdash; {{ orderItems.length }} items
          </div>
          <div v-for="(item, i) in orderItems" :key="i" style="font-size:0.78rem;color:#475569;display:flex;gap:6px;align-items:center">
            <q-icon :name="item.billing === 'recurring' ? 'autorenew' : 'shopping_cart'" size="14px"
              :color="item.billing === 'recurring' ? 'orange-7' : 'primary'" />
            <span class="col">{{ item.qty }}x {{ item.item_name }}</span>
            <span v-if="item.regular_price > item.rate"
              class="text-caption text-warning" style="text-decoration:line-through;opacity:0.7">
              {{ (item.qty * item.regular_price).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}
            </span>
            <span class="text-weight-bold">{{ (item.qty * item.rate).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}</span>
          </div>
          <q-separator class="q-my-xs" />
          <div style="font-size:0.78rem" class="row justify-between text-weight-bold">
            <span>Total</span>
            <span>{{ onetimeTotal.toFixed(2) }}$ + {{ recurringTotal.toFixed(2) }}$/mois</span>
          </div>
          <div v-if="promoTotal > 0" style="font-size:0.75rem" class="row justify-between text-warning q-mt-xs">
            <span><q-icon name="celebration" size="12px" />&nbsp;Valeur promotions étalées sur {{ maxContractMonths }} mois</span>
            <span class="text-weight-bold">{{ promoTotal.toFixed(2) }}$</span>
          </div>
        </div>

        <div v-if="willCreateContract" class="ops-card q-mb-md" style="padding:10px 12px;border-color:#fed7aa;background:#fff7ed">
          <div class="text-weight-bold text-warning" style="font-size:0.82rem;margin-bottom:4px;">
            <q-icon name="handshake" size="16px" class="q-mr-xs" />
            Contrat de service &mdash; {{ acceptanceMethod === 'docuseal' ? 'Commercial' : 'Résidentiel' }}
          </div>
          <div style="font-size:0.78rem;color:#7c2d12">
            {{ recurringTotal.toFixed(2) }}$/mois &middot; {{ maxContractMonths }} mois
            <template v-if="promoTotal > 0"> &middot; {{ promoTotal.toFixed(2) }}$ en promotions étalées</template>
          </div>
          <div v-if="acceptanceMethod !== 'docuseal'" class="text-caption text-warning q-mt-xs">
            Récapitulatif envoyé au client &mdash; click-to-accept avec horodatage + IP. Pas de "pénalité"; changement avant {{ maxContractMonths }} mois = portion non étalée au prorata.
          </div>
        </div>

        <div class="review-tree">
          <div v-for="(step, i) in wizardSteps" :key="i" class="review-step">
            <div v-if="i > 0" class="review-step-connector">
              <q-icon name="arrow_downward" size="16px" color="grey-4" />
            </div>
            <div class="review-step-card" :class="{ 'has-dep': step.depends_on_step != null }">
              <div class="row items-center no-wrap q-gutter-x-sm">
                <div class="step-num small">{{ i + 1 }}</div>
                <div class="col" style="min-width:0">
                  <div class="text-weight-bold" style="font-size:0.85rem">{{ step.subject }}</div>
                  <div class="text-caption text-grey-6 row items-center q-gutter-x-sm">
                    <span v-if="step.job_type">{{ step.job_type }}</span>
                    <span v-if="step.assigned_group"><q-icon name="group" size="12px" /> {{ step.assigned_group }}</span>
                    <span v-if="step.scheduled_date"><q-icon name="event" size="12px" /> {{ step.scheduled_date }}</span>
                    <span>{{ step.duration_h }}h</span>
                    <span v-if="step.on_open_webhook || step.on_close_webhook" class="text-info">
                      <q-icon name="webhook" size="12px" /> n8n
                    </span>
                  </div>
                </div>
                <span class="ops-badge" :class="step.priority === 'high' ? 'open' : step.priority === 'low' ? 'closed' : 'draft'"
                  style="font-size:10px;padding:1px 6px">{{ step.priority }}</span>
              </div>
            </div>
          </div>
        </div>
        <div class="q-mt-md ops-card" style="padding:10px 12px;background:#f0fdf4;border-color:#bbf7d0">
          <div class="row items-center q-gutter-x-sm">
            <q-icon name="check_circle" color="green-6" size="20px" />
            <div>
              <div class="text-weight-bold text-green-8" style="font-size:0.85rem">Prêt à publier</div>
              <div class="text-caption text-green-7">
                {{ wizardSteps.length }} tâches + {{ orderItems.length }} items
                &mdash; {{ orderMode === 'quotation' ? 'Devis → Bon de commande' : orderMode === 'prepaid' ? 'Facture pré-payée' : 'Commande directe' }}
              </div>
            </div>
          </div>
        </div>
      </q-card-section>

      <q-card-section v-if="publishedDone" class="col q-pt-md" style="overflow-y:auto;min-height:250px;max-height:70vh;">
        <div style="text-align:center">
          <q-icon :name="pendingAcceptance ? 'hourglass_top' : 'check_circle'" :color="pendingAcceptance ? 'orange-6' : 'green-6'" size="56px" />
          <div class="text-h6 text-weight-bold q-mt-sm" :class="pendingAcceptance ? 'text-warning' : 'text-green-8'">
            {{ pendingAcceptance ? 'En attente d\'acceptation client' : 'Projet créé avec succès' }}
          </div>
          <div class="text-caption text-grey-6 q-mt-xs">{{ publishedDocType }} <b>{{ publishedDocName }}</b></div>
          <div v-if="publishedContractName" class="text-caption text-warning q-mt-xs">
            <q-icon name="handshake" size="14px" class="q-mr-xs" />Contrat <b>{{ publishedContractName }}</b>
          </div>
          <div v-if="!pendingAcceptance && publishedJobCount" class="text-caption text-grey-6">{{ publishedJobCount }} tâches créées</div>
        </div>

        <div v-if="pendingAcceptance && !agentAccepted" class="ops-card q-mx-auto q-my-sm" style="max-width:440px;padding:12px 16px;background:#fff7ed;border-color:#fed7aa;text-align:left">
          <div class="text-caption text-warning" style="line-height:1.5">
            <q-icon name="info" size="14px" class="q-mr-xs" />
            Les tâches seront créées automatiquement lorsque le client acceptera le devis.
          </div>
          <q-separator class="q-my-sm" />
          <q-btn unelevated color="green-7" icon="verified" label="Accepter pour le client" no-caps dense class="full-width"
            :loading="agentAccepting" @click="acceptForClient" />
          <div class="text-caption text-grey-6 q-mt-xs" style="text-align:center">
            À utiliser si le client accepte verbalement (téléphone, en personne)
          </div>
        </div>
        <div v-if="agentAccepted" class="ops-card q-mx-auto q-my-sm" style="max-width:440px;padding:12px 16px;background:#f0fdf4;border-color:#bbf7d0;text-align:center">
          <q-icon name="check_circle" color="green-6" size="24px" />
          <div class="text-weight-bold text-green-8 q-mt-xs" style="font-size:0.85rem">Devis accepté — tâches créées</div>
        </div>

        <div v-if="publishedDocType === 'Quotation'" class="ops-card q-mx-auto q-my-sm" style="max-width:440px;padding:12px 16px;border-color:#c7d2fe;text-align:left">
          <div class="text-weight-bold text-green-8 q-mb-sm" style="font-size:0.82rem">
            <q-icon name="send" size="16px" class="q-mr-xs" /> Envoyer au client
          </div>
          <div class="row q-col-gutter-sm items-end q-mb-xs">
            <div class="col">
              <q-input v-model="sendTo" dense outlined
                :placeholder="sendChannel === 'email' ? 'client@exemple.com' : '+15145551234'"
                :label="sendChannel === 'email' ? 'Courriel' : 'Téléphone'" :input-style="{ fontSize: '0.82rem' }">
                <template v-slot:prepend>
                  <q-icon :name="sendChannel === 'email' ? 'email' : 'sms'" size="18px" color="primary" />
                </template>
              </q-input>
            </div>
            <div class="col-auto">
              <q-btn-toggle v-model="sendChannel" dense unelevated toggle-color="primary" color="grey-3" text-color="grey-8"
                size="sm" no-caps style="margin-bottom:1px"
                :options="[{ label: 'Courriel', value: 'email' }, { label: 'SMS', value: 'sms' }]" />
            </div>
          </div>
          <q-btn unelevated :label="sending ? 'Envoi...' : sendChannel === 'email' ? 'Envoyer par courriel' : 'Envoyer par SMS'"
            :icon="sendChannel === 'email' ? 'email' : 'sms'"
            color="primary" no-caps dense class="full-width q-mt-xs"
            :disable="!sendTo || sending" :loading="sending" @click="sendToClient" />
          <div v-if="sendResult" class="text-caption q-mt-xs" :class="sendResult.ok ? 'text-green-7' : 'text-negative'">
            {{ sendResult.message }}
          </div>
        </div>

        <div class="row justify-center q-gutter-sm q-mt-sm">
          <q-btn unelevated icon="picture_as_pdf" label="PDF" color="primary" dense no-caps @click="downloadPdf" />
          <q-btn outline icon="content_copy" label="Lien PDF" color="grey-7" dense no-caps @click="copyPdfLink" />
          <q-btn v-if="acceptanceLinkUrl" outline icon="link" label="Lien acceptation" color="purple-6" dense no-caps
            @click="copyAcceptanceLink" />
        </div>
        <div v-if="pdfCopied" class="text-caption text-green-7 q-mt-xs" style="text-align:center">Copié!</div>
      </q-card-section>

      <q-card-actions align="right" class="q-px-md q-pb-md" style="border-top:1px solid var(--ops-border, #e2e8f0)">
        <q-btn v-if="publishedDone" flat label="Fermer" color="grey-7" @click="cancel" />
        <template v-else>
          <q-btn flat label="Annuler" color="grey-7" @click="cancel" />
          <q-btn v-if="currentStep > 0" flat label="Précédent" color="grey-7" icon="arrow_back" @click="goBack" />
          <q-btn v-if="currentStep < 4" unelevated label="Suivant" color="primary" icon-right="arrow_forward"
            :disable="!canProceed" @click="goNext" />
          <q-btn v-if="currentStep === 4" unelevated :label="publishLabel" color="green-7" icon="rocket_launch"
            :loading="publishing" @click="publish" />
        </template>
      </q-card-actions>
    </q-card>

    <q-dialog v-model="catalogOpen" :maximized="$q.screen.lt.sm" transition-show="slide-up" transition-hide="slide-down">
      <q-card class="catalog-modal column no-wrap">
        <div class="catalog-modal-header">
          <div class="row items-center no-wrap q-mb-sm">
            <q-icon name="inventory_2" size="22px" color="primary" class="q-mr-sm" />
            <div class="col">
              <div class="text-weight-bold" style="font-size:1rem">Catalogue</div>
              <div class="text-caption text-grey-6">Ajoutez des produits ou services au devis</div>
            </div>
            <q-btn flat round dense icon="close" @click="catalogOpen = false" />
          </div>
          <q-input v-model="catalogSearch" dense outlined clearable autofocus
            placeholder="Rechercher un produit ou service..."
            class="catalog-search">
            <template v-slot:prepend><q-icon name="search" color="grey-6" /></template>
          </q-input>
          <div class="catalog-cat-row">
            <div v-for="cat in catalogCategories" :key="cat"
              class="catalog-cat-chip" :class="{ active: catalogFilter === cat }"
              @click="catalogFilter = cat">
              <q-icon :name="cat === 'Tous' ? 'apps' : catIcon(cat)" size="14px" class="q-mr-xs" />
              {{ cat }}
            </div>
          </div>
        </div>

        <q-scroll-area class="catalog-modal-body col">
          <div v-if="catalogLoading" class="catalog-loading">
            <q-spinner size="28px" color="primary" />
            <div class="text-caption text-grey-6 q-mt-sm">Chargement du catalogue...</div>
          </div>
          <div v-else-if="!displayedCatalog.length" class="catalog-empty">
            <q-icon name="search_off" size="40px" color="grey-4" />
            <div class="text-caption text-grey-6 q-mt-sm">Aucun produit ne correspond à votre recherche</div>
          </div>
          <div v-else class="catalog-list">
            <div v-for="p in displayedCatalog" :key="p.item_code" class="catalog-card"
              :class="{ 'just-added': catalogRecentlyAdded.has(p.item_code) }"
              @click="addFromCatalog(p)">
              <div class="catalog-card-icon" :style="{ background: catBgColor(p.service_category) }">
                <q-icon :name="catIcon(p.service_category)" size="22px" :color="catIconColor(p.service_category)" />
              </div>
              <div class="catalog-card-body">
                <div class="catalog-card-name">{{ p.item_name }}</div>
                <div class="catalog-card-meta">
                  <span class="catalog-card-code">{{ p.item_code }}</span>
                  <q-chip v-if="cartItemCodes.has(p.item_code)" dense square color="green-1" text-color="green-9"
                    icon="check_circle" size="11px" class="catalog-card-chip">Au panier</q-chip>
                </div>
              </div>
              <div class="catalog-card-price">
                <div class="catalog-card-rate">{{ p.rate.toFixed(2) }} $</div>
                <div class="catalog-card-billing" :class="p.billing_type === 'Mensuel' ? 'recurring' : 'onetime'">
                  {{ p.billing_type === 'Mensuel' ? '/ mois' : p.billing_type === 'Annuel' ? '/ an' : 'unique' }}
                </div>
              </div>
              <q-btn round unelevated color="primary" icon="add" size="sm" class="catalog-card-add" />
            </div>
          </div>
        </q-scroll-area>

        <div class="catalog-modal-footer">
          <div class="catalog-footer-counter">
            <q-icon name="shopping_cart" size="16px" class="q-mr-xs" />
            <span>{{ orderItems.length }} {{ orderItems.length > 1 ? 'items' : 'item' }} au devis</span>
            <span v-if="catalogRecentlyAdded.size" class="catalog-footer-added">
              &middot; <span class="text-green-7 text-weight-bold">+{{ catalogRecentlyAdded.size }}</span> ajouté{{ catalogRecentlyAdded.size > 1 ? 's' : '' }}
            </span>
          </div>
          <q-btn unelevated color="primary" label="Terminé" no-caps icon-right="check"
            @click="catalogOpen = false" />
        </div>
      </q-card>
    </q-dialog>

    <q-dialog v-model="addStepDialog.open">
      <q-card style="min-width: 340px; max-width: 90vw">
        <q-card-section class="row items-center q-pb-none">
          <q-icon name="add_task" size="22px" color="primary" class="q-mr-sm" />
          <div class="text-weight-bold">Ajouter une étape</div>
          <q-space />
          <q-btn flat round dense icon="close" @click="addStepDialog.open = false" />
        </q-card-section>
        <q-card-section class="q-gutter-sm">
          <q-input v-model="addStepDialog.subject" label="Sujet de l'étape" dense outlined autofocus
            :rules="[v => !!v && v.trim().length > 0 || 'Requis']" />
          <div class="row q-col-gutter-sm">
            <div class="col-6">
              <q-select v-model="addStepDialog.job_type" :options="JOB_TYPE_OPTIONS" label="Type" dense outlined
                emit-value map-options />
            </div>
            <div class="col-6">
              <q-input v-model.number="addStepDialog.duration_h" type="number" step="0.25" min="0.25"
                label="Durée (h)" dense outlined />
            </div>
          </div>
          <q-select v-model="addStepDialog.assigned_group" :options="groupOptions" label="Groupe assigné"
            dense outlined emit-value map-options clearable />
        </q-card-section>
        <q-card-actions align="right" class="q-pa-md">
          <q-btn flat no-caps label="Annuler" @click="addStepDialog.open = false" />
          <q-btn unelevated color="primary" no-caps label="Ajouter" icon-right="check"
            :disable="!(addStepDialog.subject && addStepDialog.subject.trim())"
            @click="confirmAddStep()" />
        </q-card-actions>
      </q-card>
    </q-dialog>

    <!-- TV channel picker: Mix 5 / Mix 10 channel selection. -->
    <q-dialog v-model="channelPicker.open" :maximized="$q.screen.lt.sm"
      transition-show="slide-up" transition-hide="slide-down">
      <q-card class="channel-picker-card">
        <q-card-section class="row items-center q-pb-sm channel-picker-head">
          <q-icon name="playlist_add_check" size="22px" color="pink-6" class="q-mr-sm" />
          <div class="col" style="min-width:0">
            <div class="text-weight-bold">Configurateur chaînes</div>
            <div class="channel-picker-sub">
              {{ pickerPicksUsed }} / {{ pickerPicksAllowed }} choix utilisés
              <span v-if="pickerOverage > 0" class="channel-picker-overage">
                · +{{ pickerOverage }} à la carte (tarification à venir)
              </span>
            </div>
          </div>
          <q-btn flat round dense icon="close" @click="closeChannelPicker()" />
        </q-card-section>

        <!-- Tier segmented toggle: Mix 5 ↔ Mix 10 without losing the current
             selection. Overage recomputes on the fly when switching down. -->
        <q-card-section class="q-py-sm channel-picker-tiers">
          <div v-for="t in mixTiers" :key="t.id"
            class="channel-picker-tier"
            :class="{ 'channel-picker-tier-active': channelPicker.tier?.id === t.id }"
            @click="switchPickerTier(t)">
            <div class="channel-picker-tier-label">{{ t.label }}</div>
            <div class="channel-picker-tier-price">{{ t.price_effective.toFixed(2) }}$/mois</div>
            <div class="channel-picker-tier-meta">
              {{ t.picks_allowed }} choix · {{ t.pick_unit_cost?.toFixed(2) }}$/pick régulier
            </div>
          </div>
        </q-card-section>

        <q-card-section class="q-py-sm">
          <q-input v-model="channelPicker.search" dense outlined
            placeholder="Rechercher une chaîne (ex. sports, TVA, comédie)"
            clearable>
            <template #prepend>
              <q-icon name="search" size="18px" />
            </template>
          </q-input>
        </q-card-section>

        <q-card-section v-if="channelPicker.selection.length" class="q-py-sm channel-picker-chips">
          <q-chip v-for="ch in channelPicker.selection" :key="ch.name"
            :color="ch.premium_group ? 'pink-1' : 'grey-3'"
            text-color="grey-9" removable dense
            class="channel-chip"
            @remove="toggleChannel(ch)">
            <span class="channel-chip-name">{{ ch.name }}</span>
            <q-badge v-if="ch.premium_group" color="negative" text-color="white"
              class="channel-chip-pastille" rounded>
              2
            </q-badge>
          </q-chip>
        </q-card-section>

        <q-card-section class="channel-picker-list">
          <div v-for="ch in filteredChannels" :key="ch.name"
            class="channel-row"
            :class="{
              'channel-row-selected': isChannelSelected(ch),
              'channel-row-premium': !!ch.premium_group,
            }"
            @click="toggleChannel(ch)">
            <q-icon :name="isChannelSelected(ch) ? 'check_circle' : 'radio_button_unchecked'"
              size="18px" :color="isChannelSelected(ch) ? 'green-6' : 'grey-5'" />
            <div class="col" style="min-width:0">
              <div class="channel-row-name">
                {{ ch.name }}
                <q-badge v-if="ch.premium_group" color="negative" text-color="white"
                  class="q-ml-xs" rounded>2 choix</q-badge>
              </div>
              <div v-if="ch.sub && ch.sub.length" class="channel-row-sub">
                incl. {{ ch.sub.join(', ') }}
              </div>
            </div>
            <div v-if="ch.premium_group" class="channel-row-surcharge">+3$</div>
          </div>
          <div v-if="!filteredChannels.length" class="channel-row-empty">
            Aucune chaîne ne correspond.
          </div>
        </q-card-section>

        <q-card-actions align="right" class="q-pa-md channel-picker-foot">
          <q-btn flat no-caps label="Annuler" @click="closeChannelPicker()" />
          <q-btn unelevated color="pink-6" no-caps icon-right="check"
            :label="pickerOverage > 0
              ? `Appliquer (${channelPicker.selection.length} chaînes · +${pickerOverage} à la carte)`
              : `Appliquer (${channelPicker.selection.length} chaînes)`"
            :disable="channelPicker.selection.length === 0"
            @click="applyChannelPicker()" />
        </q-card-actions>
      </q-card>
    </q-dialog>

  </q-dialog>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import { Notify, useQuasar } from 'quasar'
import { PROJECT_TEMPLATES, ASSIGNED_GROUPS } from 'src/config/project-templates'
import { STEP_LABELS, JOB_TYPE_OPTIONS, PRIORITY_OPTIONS, ORDER_MODE_OPTIONS, ACCEPTANCE_METHOD_OPTIONS, HUB_URL, REFERRAL_ITEM_CODE, REFERRAL_CREDIT_AMOUNT, EXTRA_FEE_PRESETS, TV_CHANNELS, catIcon } from 'src/data/wizard-constants'
import { useWizardCatalog } from 'src/composables/useWizardCatalog'
import { useWizardPublish } from 'src/composables/useWizardPublish'
import FlowQuickButton from 'src/components/flow-editor/FlowQuickButton.vue'

const props = defineProps({
  modelValue: Boolean,
  issue: { type: Object, default: () => ({}) },
  customer: { type: Object, default: () => null },
  existingJobs: { type: Array, default: () => [] },
  deliveryAddressId: { type: String, default: '' },
  initialTier: { type: Object, default: null }, // forfait pré-choisi dans QuoteWizard (vitrine avant contact) → pré-remplit le panier
})

const emit = defineEmits(['update:modelValue', 'created'])

const $q = useQuasar()
const templates = PROJECT_TEMPLATES
const currentStep = ref(0)
const selectedTemplate = ref(null)
const wizardSteps = ref([])
const customSteps = ref([])
const publishing = ref(false)

const orderMode = ref('direct')
const orderItems = ref([])
const contractNotes = ref('')
const requireAcceptance = ref(false)
const acceptanceMethod = ref('jwt')
const clientPhone = ref('')
const clientEmail = ref('')

const publishedDocName = ref('')
const publishedDocType = ref('')
const publishedContractName = ref('')
const publishedDone = ref(false)
const pdfCopied = ref(false)
const pendingAcceptance = ref(false)
const acceptanceLinkSent = ref(false)
const acceptanceSentVia = ref('')
const acceptanceLinkUrl = ref('')
const publishedJobCount = ref(0)

const sendChannel = ref('email')
const sendTo = ref('')
const sending = ref(false)
const sendResult = ref(null)

const agentAccepting = ref(false)
const agentAccepted = ref(false)

const catalogOpen = ref(false)
const catalogSearch = ref('')
const catalogRecentlyAdded = ref(new Set())
const catalogTargetLineIdx = ref(null)
const expandedBundles = ref(new Set())
const installStepExpanded = ref(false)

function toggleBundleExpanded (id) {
  const next = new Set(expandedBundles.value)
  if (next.has(id)) next.delete(id)
  else next.add(id)
  expandedBundles.value = next
}

// ── Billing cycle pill: Mensuel → Annuel → Unique → loop ──
// The pill replaces the separate interval select + recurring/onetime toggle.
// A recurring service with a yearly rate sets billing_interval='Year'; onetime
// clears the interval entirely so totals & subscription code read it correctly.
const BILLING_CYCLE = [
  { billing: 'recurring', billing_interval: 'Month', label: '/mois', cls: 'recurring' },
  { billing: 'recurring', billing_interval: 'Year',  label: '/an',   cls: 'yearly' },
  { billing: 'onetime',   billing_interval: null,    label: 'unique', cls: 'onetime' },
]
function billingIndex (item) {
  if (item.billing === 'onetime') return 2
  return item.billing_interval === 'Year' ? 1 : 0
}
function billingPillLabel (item) { return BILLING_CYCLE[billingIndex(item)].label }
function billingPillClass (item) { return BILLING_CYCLE[billingIndex(item)].cls }
function nextBillingLabel (item) {
  return BILLING_CYCLE[(billingIndex(item) + 1) % BILLING_CYCLE.length].label
}
function cycleBilling (item) {
  const next = BILLING_CYCLE[(billingIndex(item) + 1) % BILLING_CYCLE.length]
  item.billing = next.billing
  item.billing_interval = next.billing_interval
  if (next.billing === 'onetime' && (!item.regular_price || item.regular_price < item.rate)) {
    item.regular_price = item.rate
  }
}

// Lookup helpers for the inline bundle accordion under each cart line.
function bundleForTemplate (tplId) {
  return loadedBundles.value.find(b => b.id === tplId) || null
}
function bundleStepCount (tplId) {
  return bundleForTemplate(tplId)?.steps.length || 0
}

// Per-step extra fee — stored directly on the wizardSteps entry. At publish
// time, each step with a positive extra_fee becomes a FEE-EXTRA line in the
// order so the extra flows through to the customer invoice/quote. extra_label
// carries the preset label (Émondage, Creusage…) or the custom free-form name.
function setStepExtraFee (idx, v) {
  const step = wizardSteps.value[idx]
  if (!step) return
  const n = Number(v)
  step.extra_fee = Number.isFinite(n) && n > 0 ? n : 0
}

function applyExtraPreset (idx, preset) {
  const step = wizardSteps.value[idx]
  if (!step) return
  step.extra_fee = Number(preset.amount) || 0
  step.extra_label = preset.label
  closeExtraMenu(idx)
}

function clearExtra (idx) {
  const step = wizardSteps.value[idx]
  if (!step) return
  step.extra_fee = 0
  step.extra_label = ''
  closeExtraMenu(idx)
}

// Per-step custom extra draft (label + amount) — keyed by step idx so each
// row edits independently. Flushed into step.extra_{fee,label} on confirm.
const customExtraDraft = ref({})
function getCustomDraft (idx) {
  if (!customExtraDraft.value[idx]) {
    const step = wizardSteps.value[idx]
    customExtraDraft.value[idx] = {
      label: step?.extra_label || '',
      amount: step?.extra_fee || null,
    }
  }
  return customExtraDraft.value[idx]
}
function updateCustomDraft (idx, field, value) {
  const current = getCustomDraft(idx)
  customExtraDraft.value = {
    ...customExtraDraft.value,
    [idx]: { ...current, [field]: value },
  }
}
function applyCustomExtra (idx) {
  const draft = customExtraDraft.value[idx]
  if (!draft) return
  const amount = Number(draft.amount)
  const label = (draft.label || '').trim()
  if (!Number.isFinite(amount) || amount <= 0) return
  const step = wizardSteps.value[idx]
  if (!step) return
  step.extra_fee = amount
  step.extra_label = label || 'Frais extra'
  closeExtraMenu(idx)
}

const extraMenuOpen = ref({})
function toggleExtraMenu (idx) {
  extraMenuOpen.value = { ...extraMenuOpen.value, [idx]: !extraMenuOpen.value[idx] }
  if (extraMenuOpen.value[idx]) {
    // Seed the draft from the current step value so the custom inputs prefill.
    const step = wizardSteps.value[idx]
    customExtraDraft.value[idx] = {
      label: step?.extra_label || '',
      amount: step?.extra_fee || null,
    }
  }
}
function closeExtraMenu (idx) {
  extraMenuOpen.value = { ...extraMenuOpen.value, [idx]: false }
}

const addStepDialog = ref({
  open: false,
  templateId: '',
  subject: '',
  job_type: 'Autre',
  duration_h: 0.5,
  assigned_group: '',
})

function openAddStepDialog (templateId) {
  addStepDialog.value = {
    open: true,
    templateId,
    subject: '',
    job_type: 'Autre',
    duration_h: 0.5,
    assigned_group: '',
  }
}

function confirmAddStep () {
  const d = addStepDialog.value
  if (!d.subject || !d.subject.trim()) return
  addStepToBundle(d.templateId, {
    subject: d.subject.trim(),
    job_type: d.job_type,
    duration_h: d.duration_h,
    assigned_group: d.assigned_group,
  })
  addStepDialog.value.open = false
}

const templateLoadedFor = ref(new Set())

const { catalogProducts, catalogLoading, catalogFilter, catalogCategories, residentialPresets, internetHeroTiers, tvHeroTiers, selectedHeroTier, selectHeroTier, selectedTvTier, selectTvTier, filteredCatalog, loadCatalog, addCatalogItem, matchCompetitor, clearCompetitor, applyPreset, loadTemplateFromItem, toggleTemplate, removeStepFromBundle, addStepToBundle, loadedBundles, lastMergeCount, mergedTemplateLabels } =
  useWizardCatalog({ orderItems, wizardSteps, templates, templateLoadedFor })
// Égaler un concurrent (terrain) : saisir le total mensuel du concurrent → rabais promo (rab-x) sur l'Internet, 24 mois.
const competitorPrice = ref(null)
const competitorInfo = ref(null)
function applyCompetitor () {
  const r = matchCompetitor(competitorPrice.value, 24)
  if (r.ok) competitorInfo.value = { ok: true, msg: `Rabais −${r.rebate.toFixed(2)}$/mois sur l'Internet (24 mois) → total ${r.target.toFixed(2)}$/mois` }
  else competitorInfo.value = { ok: false, msg: (r.target >= r.base && r.base > 0) ? `Concurrent (${(r.target || 0).toFixed(2)}$) ≥ notre prix (${r.base.toFixed(2)}$) — aucun rabais requis.` : 'Ajoutez d\'abord des services au panier.' }
}
function removeCompetitor () { clearCompetitor(); competitorInfo.value = null; competitorPrice.value = null }

// Internet accordion defaults to open — Internet is required so we want
// the tier list visible on first landing. Clicking a tier closes it.
const heroExpanded = ref(true)
const tvExpanded = ref(false)
// Anchor tiles show the currently-selected tier, or fall back to the
// "default" tier (Megafibre 80 / Télé Base) when nothing from that family
// is in the cart yet. This keeps the marketing "à partir de X$" framing up front.
const heroAnchor = computed(() =>
  selectedHeroTier.value || internetHeroTiers.find(t => t.default) || internetHeroTiers[0])
const tvAnchor = computed(() =>
  selectedTvTier.value || tvHeroTiers.find(t => t.default) || tvHeroTiers[0])

// Internet tier select: keep the group open while the rep is comparing
// tiers, but collapse it once a selection lands — clearer signal that the
// anchor now reflects what's in the cart.
function onInternetTierClick (tier) {
  selectHeroTier(tier)
  heroExpanded.value = false
}

function onTvTierClick (tier) {
  if (!selectedHeroTier.value) {
    $q.notify({
      type: 'warning',
      message: 'Ajoutez un service Internet d\'abord',
      caption: 'La télé nécessite un abonnement Internet à la même adresse',
      timeout: 3000,
      position: 'top',
    })
    return
  }
  // Tiers with picks_allowed open the channel picker. Base tier and
  // non-ready tiers (à la carte for now) fall straight through.
  if (tier.picks_allowed && tier.ready) {
    openChannelPicker(tier)
    tvExpanded.value = false
    return
  }
  if (!tier.ready) {
    $q.notify({
      type: 'info',
      message: `${tier.label} — configurateur de chaînes à venir`,
      caption: 'Pour l\'instant, la ligne est ajoutée sans sélection détaillée',
      timeout: 3000,
      position: 'top',
    })
  }
  selectTvTier(tier)
  tvExpanded.value = false
}

// Click-to-expand for hero groups. Only one group can be open at a time
// (accordion behavior) but toggling requires a click, never hover. TV is
// gated behind Internet — attempting to expand without an Internet tier
// shows a toast instead of opening.
function toggleHeroGroup (which) {
  if (which === 'tv' && !selectedHeroTier.value) {
    $q.notify({
      type: 'warning',
      message: 'Ajoutez un service Internet d\'abord',
      caption: 'La télé nécessite un abonnement Internet à la même adresse',
      timeout: 3000,
      position: 'top',
    })
    return
  }
  if (which === 'internet') {
    heroExpanded.value = !heroExpanded.value
    if (heroExpanded.value) { tvExpanded.value = false; phoneExpanded.value = false }
  } else if (which === 'tv') {
    tvExpanded.value = !tvExpanded.value
    if (tvExpanded.value) { heroExpanded.value = false; phoneExpanded.value = false }
  } else if (which === 'phone') {
    phoneExpanded.value = !phoneExpanded.value
    if (phoneExpanded.value) { heroExpanded.value = false; tvExpanded.value = false }
  }
}

// Contract months for the currently-selected Internet tier, read from the
// actual cart line (authoritative) so manual edits are reflected.
const internetContractMonths = computed(() => {
  const tier = selectedHeroTier.value
  if (!tier) return null
  const line = orderItems.value.find(i => i.item_code === tier.code)
  return line?.contract_months || null
})

// ── Step 2 summary (services additionnels) ───────────────────────────────
// Compact chip row under the step-2 header — shows what's currently added
// (TV tier + chaînes, téléphonie mode, upsells) so the rep sees the running
// tally above the detail accordions.
const step2Summary = computed(() => {
  const chips = []
  if (selectedTvTier.value) {
    const tier = selectedTvTier.value
    const line = orderItems.value.find(i => i.item_code && i.item_code.startsWith('TV-MIX'))
    const channelCount = Array.isArray(line?.tv_channels) ? line.tv_channels.length : 0
    let label = tier.label
    if (channelCount) label += ` · ${channelCount} chaîne${channelCount > 1 ? 's' : ''}`
    chips.push({ key: 'tv', icon: 'live_tv', cls: 'wizard-step-chip-tv', label })
  }
  if (phoneApplied.value) {
    const mode = phoneMode.value === 'keep' ? 'Conservation' : 'Nouveau numéro'
    const num = phoneMode.value === 'keep' && phoneKeepNumber.value ? ` · ${phoneKeepNumber.value}` : ''
    chips.push({ key: 'phone', icon: 'phone', cls: 'wizard-step-chip-phone', label: `Téléphonie · ${mode}${num}` })
  }
  // Other upsells (WiFi booster, etc.) that are currently applied
  for (const p of otherUpsells.value) {
    const codes = new Set((p.items || []).map(i => i.item_code).filter(Boolean))
    if (orderItems.value.some(i => codes.has(i.item_code))) {
      chips.push({ key: p.id, icon: p.icon || 'add_circle', cls: 'wizard-step-chip-upsell', label: p.label })
    }
  }
  return chips
})

// ── Sommaire step (cart detail with edit/delete/qty/drag-drop) ───────────
// Only one row expands at a time (keeps the page short and focused).
// Drag the handle at the left to reorder — the array order drives quote
// line order on export. We use HTML5 native drag API rather than Sortable.js
// to keep the bundle lean (the list is always short).
const expandedSommaireIdx = ref(null)
const dragIndex = ref(null)
const dragOverIndex = ref(null)

// Clicking the cart pill on Step 2 jumps to the Sommaire step. No-op when
// empty — the pill already shows a muted "empty" state in that case.
function goToSommaire () {
  if (!orderItems.value.length) return
  currentStep.value = 3
}

function toggleSommaireExpand (idx) {
  expandedSommaireIdx.value = expandedSommaireIdx.value === idx ? null : idx
}

function deleteSommaireItem (idx) {
  orderItems.value.splice(idx, 1)
  if (expandedSommaireIdx.value === idx) {
    expandedSommaireIdx.value = null
  } else if (expandedSommaireIdx.value != null && idx < expandedSommaireIdx.value) {
    // Keep pointer at the same item after deleting an earlier row
    expandedSommaireIdx.value--
  }
}

function onSommaireDragStart (idx, e) {
  dragIndex.value = idx
  if (e.dataTransfer) {
    e.dataTransfer.effectAllowed = 'move'
    // Some browsers require data for drag events to fire reliably
    e.dataTransfer.setData('text/plain', String(idx))
  }
}

function onSommaireDragEnd () {
  dragIndex.value = null
  dragOverIndex.value = null
}

function onSommaireDragOver (idx, e) {
  if (dragIndex.value == null || dragIndex.value === idx) return
  if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
  dragOverIndex.value = idx
}

function onSommaireDragLeave (idx) {
  if (dragOverIndex.value === idx) dragOverIndex.value = null
}

function onSommaireDrop (idx) {
  const from = dragIndex.value
  if (from == null || from === idx) {
    dragIndex.value = null
    dragOverIndex.value = null
    return
  }
  const [moved] = orderItems.value.splice(from, 1)
  orderItems.value.splice(idx, 0, moved)
  // Track the expanded row across the reorder so UX feels stable
  if (expandedSommaireIdx.value === from) {
    expandedSommaireIdx.value = idx
  } else if (expandedSommaireIdx.value != null) {
    if (from < expandedSommaireIdx.value && idx >= expandedSommaireIdx.value) {
      expandedSommaireIdx.value--
    } else if (from > expandedSommaireIdx.value && idx <= expandedSommaireIdx.value) {
      expandedSommaireIdx.value++
    }
  }
  dragIndex.value = null
  dragOverIndex.value = null
}

// ── TV channel picker ────────────────────────────────────────────────────
// Opens when the rep clicks a Mix 5 / Mix 10 tier. Tracks the tier being
// configured (mix-5 or mix-10), the working selection (array of channel
// objects), and the fuzzy search query. Applying writes back through
// selectTvTier(tier, channelSelection) which handles surcharges + overage.
const channelPicker = ref({
  open: false,
  tier: null,
  selection: [],
  search: '',
})

function openChannelPicker (tier) {
  // Preload any existing selection from the current cart so reopening a Mix
  // tier surfaces what's already been chosen (channels stored on TV-MIX line).
  const existing = orderItems.value.find(i => i.item_code && i.item_code.startsWith('TV-MIX') && Array.isArray(i.tv_channels))
  const names = existing?.tv_channels || []
  const selection = names
    .map(n => TV_CHANNELS.find(c => c.name === n))
    .filter(Boolean)
  channelPicker.value = {
    open: true,
    tier,
    selection,
    search: '',
  }
}

function closeChannelPicker () {
  channelPicker.value.open = false
}

function isChannelSelected (ch) {
  return channelPicker.value.selection.some(s => s.name === ch.name)
}

function toggleChannel (ch) {
  const list = channelPicker.value.selection
  const idx = list.findIndex(s => s.name === ch.name)
  if (idx >= 0) {
    list.splice(idx, 1)
  } else {
    list.push(ch)
  }
}

// Pick cost: premium channels = 2, regular = 1.
function channelPickCost (ch) {
  return ch.premium_group ? 2 : 1
}

const pickerPicksUsed = computed(() =>
  channelPicker.value.selection.reduce((n, c) => n + channelPickCost(c), 0),
)
const pickerPicksAllowed = computed(() => channelPicker.value.tier?.picks_allowed || 0)
const pickerOverage = computed(() => Math.max(0, pickerPicksUsed.value - pickerPicksAllowed.value))

// Fuzzy-ish match against name + sub[] aliases. Space-separated tokens all
// have to appear somewhere. Case-insensitive. No external lib — plenty fast
// for ~65 entries and a couple of tokens.
const filteredChannels = computed(() => {
  const q = (channelPicker.value.search || '').trim().toLowerCase()
  if (!q) return TV_CHANNELS
  const tokens = q.split(/\s+/).filter(Boolean)
  return TV_CHANNELS.filter(ch => {
    const hay = [ch.name, ...(ch.sub || [])].join(' ').toLowerCase()
    return tokens.every(t => hay.includes(t))
  })
})

function applyChannelPicker () {
  const { tier, selection } = channelPicker.value
  if (!tier) return
  selectTvTier(tier, selection.map(c => ({
    name: c.name,
    premium_group: c.premium_group || null,
    pick_cost: channelPickCost(c),
  })))
  channelPicker.value.open = false
}

// Mix tiers available as segmented toggle inside the picker. Only tiers with
// picks_allowed qualify — that's Mix 5 and Mix 10 today.
const mixTiers = computed(() => tvHeroTiers.filter(t => t.picks_allowed && t.ready))

// Switch the picker tier without losing the current channel selection.
// Picks used stay the same; the allowance (and therefore overage) simply
// recomputes against the new picks_allowed.
function switchPickerTier (tier) {
  if (!tier || channelPicker.value.tier?.id === tier.id) return
  channelPicker.value.tier = tier
}


const displayedCatalog = computed(() => {
  const q = (catalogSearch.value || '').trim().toLowerCase()
  if (!q) return filteredCatalog.value
  return filteredCatalog.value.filter(p =>
    (p.item_name || '').toLowerCase().includes(q)
    || (p.item_code || '').toLowerCase().includes(q)
    || (p.service_category || '').toLowerCase().includes(q),
  )
})

const cartItemCodes = computed(() => new Set(orderItems.value.map(i => i.item_code).filter(Boolean)))

const CAT_BG = { Internet: '#e0f2fe', Téléphonie: '#fef3c7', Bundle: '#ede9fe', Équipement: '#dcfce7', Frais: '#fee2e2' }
const CAT_COLOR = { Internet: 'blue-7', Téléphonie: 'amber-8', Bundle: 'purple-6', Équipement: 'green-7', Frais: 'red-6' }
function catBgColor (cat) { return CAT_BG[cat] || '#f1f5f9' }
function catIconColor (cat) { return CAT_COLOR[cat] || 'grey-7' }

function openCatalog () {
  catalogTargetLineIdx.value = null
  catalogOpen.value = true
  catalogSearch.value = ''
  catalogRecentlyAdded.value = new Set()
  if (!catalogProducts.value.length) loadCatalog()
}

// Open catalog targeted at a specific empty row — when the user picks
// something, we replace that row in-place instead of appending a new one.
function openCatalogForLine (idx) {
  catalogTargetLineIdx.value = idx
  catalogOpen.value = true
  catalogSearch.value = ''
  catalogRecentlyAdded.value = new Set()
  if (!catalogProducts.value.length) loadCatalog()
}

function onItemFieldClick (idx, ev) {
  const line = orderItems.value[idx]
  if (!line) return
  // Manual-edit mode: let the user type freely.
  if (line.manual_edit) return
  if (ev?.target?.blur) ev.target.blur()
  // TV Mix supplement line → reopen channel picker (edit-in-place for the
  // current tier, preserving selected channels).
  if (line.item_code === 'TV-MIX5' || line.item_code === 'TV-MIX10') {
    const tier = tvHeroTiers.find(t => t.code === line.item_code)
    if (tier) {
      openChannelPicker(tier)
      return
    }
  }
  openCatalogForLine(idx)
}

function enableManualEdit (idx) {
  const line = orderItems.value[idx]
  if (!line) return
  line.manual_edit = true
}

function addFromCatalog (p) {
  const targetIdx = catalogTargetLineIdx.value
  // In-place swap when targeting a line that already has a product — preserve
  // qty + manual_edit flag so the agent keeps quantity overrides across swaps.
  if (targetIdx != null && orderItems.value[targetIdx]) {
    const existing = orderItems.value[targetIdx]
    const keptQty = existing.qty && existing.qty > 0 ? existing.qty : 1
    orderItems.value.splice(targetIdx, 1, {
      item_code: p.item_code,
      item_name: p.item_name,
      qty: keptQty,
      rate: p.rate,
      regular_price: 0,
      billing: p.billing_type === 'Mensuel' || p.billing_type === 'Annuel' ? 'recurring' : 'onetime',
      billing_interval: p.billing_type === 'Annuel' ? 'Year' : 'Month',
      contract_months: existing.contract_months || 12,
      project_template_id: p.project_template_id || '',
      requires_visit: p.requires_visit || false,
      manual_edit: false,
    })
    if (p.project_template_id && !templateLoadedFor.value.has(p.project_template_id)) {
      loadTemplateFromItem({ project_template_id: p.project_template_id })
    }
    catalogTargetLineIdx.value = null
  } else {
    addCatalogItem(p)
  }
  catalogRecentlyAdded.value = new Set([...catalogRecentlyAdded.value, p.item_code])
  setTimeout(() => {
    const next = new Set(catalogRecentlyAdded.value)
    next.delete(p.item_code)
    catalogRecentlyAdded.value = next
  }, 900)
}

// installRegular / installFee : déclarés AVANT useWizardPublish car son `state` les référence
// dès la création de l'objet — sinon ReferenceError (TDZ) au setup → le wizard ne monte plus.
const installRegular = ref(0)   // prix de référence barré (marketing)
const installFee = ref(0)       // montant réellement financé/dû (créance)
const { publish } = useWizardPublish({
  props, emit,
  state: {
    publishing, wizardSteps, orderItems, orderMode,
    contractNotes, requireAcceptance, acceptanceMethod,
    clientPhone, clientEmail,
    publishedDocName, publishedDocType, publishedDone,
    pendingAcceptance, acceptanceLinkUrl, acceptanceLinkSent,
    acceptanceSentVia, publishedJobCount,
    sendTo, sendChannel,
    publishedContractName,
    installFee, installRegular,
    cancel () { cancel() },
  },
})

function getPdfUrl () {
  return `${HUB_URL}/accept/doc-pdf/${encodeURIComponent(publishedDocType.value)}/${encodeURIComponent(publishedDocName.value)}`
}

function downloadPdf () { window.open(getPdfUrl(), '_blank') }

async function copyToClipboard (text) {
  try { await navigator.clipboard.writeText(text) } catch {
    const ta = document.createElement('textarea')
    ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta)
  }
  pdfCopied.value = true
  setTimeout(() => { pdfCopied.value = false }, 2500)
}

async function copyPdfLink () { await copyToClipboard(getPdfUrl()) }
async function copyAcceptanceLink () { if (acceptanceLinkUrl.value) await copyToClipboard(acceptanceLinkUrl.value) }

async function sendToClient () {
  if (!sendTo.value || sending.value) return
  sending.value = true
  sendResult.value = null
  try {
    const res = await fetch(`${HUB_URL}/accept/send`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        quotation: publishedDocName.value,
        customer: props.issue?.customer || '',
        channel: sendChannel.value,
        to: sendTo.value,
      }),
    })
    const data = await res.json()
    if (data.ok && data.sent) {
      sendResult.value = {
        ok: true,
        message: sendChannel.value === 'email'
          ? `Courriel envoyé à ${sendTo.value} avec le PDF en pièce jointe`
          : `SMS envoyé à ${sendTo.value}`,
      }
      acceptanceLinkUrl.value = data.link || acceptanceLinkUrl.value
    } else {
      sendResult.value = {
        ok: false,
        message: data.error || `Échec de l'envoi ${sendChannel.value === 'email' ? 'du courriel' : 'du SMS'}`,
      }
    }
  } catch (e) {
    sendResult.value = { ok: false, message: `Erreur: ${e.message}` }
  } finally {
    sending.value = false
  }
}

async function acceptForClient () {
  if (agentAccepting.value || !publishedDocName.value) return
  agentAccepting.value = true
  try {
    const res = await fetch(`${HUB_URL}/api/accept-for-client`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ quotation: publishedDocName.value, agent_name: 'Ops Agent' }),
    })
    const data = await res.json()
    if (data.ok) {
      agentAccepted.value = true
      pendingAcceptance.value = false
      Notify.create({ type: 'positive', message: 'Devis accepté — tâches créées automatiquement', timeout: 5000 })
    } else {
      Notify.create({ type: 'negative', message: data.error || 'Erreur lors de l\'acceptation' })
    }
  } catch (e) {
    Notify.create({ type: 'negative', message: `Erreur: ${e.message}` })
  } finally {
    agentAccepting.value = false
  }
}

function addOrderItem () {
  orderItems.value.push({ item_name: '', item_code: '', qty: 1, rate: 0, regular_price: 0, billing: 'onetime', billing_interval: 'Month', contract_months: 12 })
}

// Step extras are recorded on each wizardSteps entry (step.extra_fee). They
// roll up into onetimeTotal live; at publish time each extra becomes a
// FEE-EXTRA line so the customer invoice reflects the charge.
const stepsExtraTotal = computed(() =>
  wizardSteps.value.reduce((s, step) => s + (Number(step.extra_fee) || 0), 0))
const onetimeTotal = computed(() =>
  orderItems.value.filter(i => i.billing === 'onetime').reduce((s, i) => s + (i.qty * i.rate), 0)
  + stepsExtraTotal.value)
const recurringTotal = computed(() => orderItems.value.filter(i => i.billing === 'recurring').reduce((s, i) => s + (i.qty * i.rate), 0))
const promoTotal = computed(() => orderItems.value
  .filter(i => i.billing === 'onetime' && (i.regular_price || 0) > i.rate)
  .reduce((s, i) => s + ((i.regular_price || 0) - i.rate) * i.qty, 0))

const maxContractMonths = computed(() => {
  const months = orderItems.value.filter(i => i.billing === 'recurring').map(i => i.contract_months || 0)
  return months.length ? Math.max(...months) : 0
})

const willCreateContract = computed(() => orderMode.value === 'quotation'
  && orderItems.value.some(i => i.billing === 'recurring' && (i.contract_months || 0) > 0))

const usedServiceTypes = computed(() => {
  const s = new Set()
  for (const i of orderItems.value) {
    if (i.service_type) s.add(i.service_type)
  }
  return s
})

// Upsell chips — all residential presets are upsells now (the Internet hero
// ships as a separate INTERNET_HERO_TIERS list with swap-on-click semantics).
const upsellPresets = computed(() => residentialPresets.filter(p => p.tier !== 'hero'))

// Split out the telephony preset — it now has a dedicated hero group with
// Nouveau/Conservation sub-options. Everything else (WiFi booster, …) stays
// in the plain chip row.
const phonePreset = computed(() => residentialPresets.find(p => p.service_type === 'phone'))
const otherUpsells = computed(() => upsellPresets.value.filter(p => p.service_type !== 'phone'))

const phoneExpanded = ref(false)
const phoneMode = ref('new') // 'new' | 'keep'
const phoneKeepNumber = ref('')

const phoneApplied = computed(() => {
  const preset = phonePreset.value
  if (!preset) return false
  const codes = new Set(preset.items.map(i => i.item_code))
  return orderItems.value.some(i => codes.has(i.item_code))
})

// Port-in proof step: one-off tech step injected when the customer keeps
// their existing number. Bundled under the Internet install template so it
// rides the same truck roll. merge_key keeps it idempotent across re-apply.
const PORT_IN_MERGE_KEY = 'port_in_proof'
function ensurePortInStep () {
  const exists = wizardSteps.value.some(s => s.merge_key === PORT_IN_MERGE_KEY)
  if (exists) return
  wizardSteps.value.push({
    merge_key: PORT_IN_MERGE_KEY,
    subject: 'Collecter preuve de numéro (port-in téléphonie)',
    job_type: 'Autre',
    priority: 'high',
    duration_h: 0.25,
    assigned_group: 'Installation',
    depends_on_step: null,
    scheduled_date: '',
    on_open_webhook: '',
    on_close_webhook: '',
    source_templates: ['phone_port_in'],
    requires_photo: true,
    notify_group: 'Telephony Provisioning',
  })
}
function removePortInStep () {
  wizardSteps.value = wizardSteps.value.filter(s => s.merge_key !== PORT_IN_MERGE_KEY)
}

function applyPhonePreset (mode) {
  const preset = phonePreset.value
  if (!preset) return
  if (!phoneApplied.value) applyPreset(preset)
  phoneMode.value = mode
  if (mode === 'keep') {
    ensurePortInStep()
  } else {
    removePortInStep()
    phoneKeepNumber.value = ''
  }
  // Stamp mode + number on the TEL-ILL line so publish/print pick them up.
  const telLine = orderItems.value.find(i => i.item_code === 'TEL-ILL')
  if (telLine) {
    telLine.phone_mode = mode
    telLine.phone_keep_number = mode === 'keep' ? phoneKeepNumber.value : ''
  }
  // Auto-close the accordion on both modes — "new" is done; "keep" still
  // needs the phone number but the compact anchor now shows a warning +
  // re-clicking the anchor reopens to edit. This matches the "click to
  // select, accordion closes" pattern requested across all services.
  phoneExpanded.value = false
}

// Keep the TEL-ILL line in sync when the number field changes.
watch(phoneKeepNumber, (v) => {
  const telLine = orderItems.value.find(i => i.item_code === 'TEL-ILL')
  if (telLine && phoneMode.value === 'keep') telLine.phone_keep_number = v
})

// Referral: one-time -50$ line appended when a valid code is entered. The hub
// endpoint validates + reserves the code; if the endpoint isn't reachable we
// accept the code locally and server-side validation runs at submit time so
// the UI works in dev/offline without blocking the agent.
const referralCode = ref('')
const referralApplied = ref(false)
const referralChecking = ref(false)
const referralError = ref('')

// Installation financée (CRTC-compliant) — alimente install_fee / install_regular du contrat.
// NB: installRegular / installFee sont déclarés plus HAUT (avant useWizardPublish) — voir TDZ.
const INSTALL_PRESETS = [
  { key: 'standard', label: 'Standard', regular: 360, fee: 240 },
  { key: 'simple', label: 'Simple', regular: 180, fee: 120 },
  { key: 'none', label: 'Aucune', regular: 0, fee: 0 },
]
function setInstallPreset (p) { installRegular.value = p.regular; installFee.value = p.fee }
function isInstallPreset (p) { return Number(installFee.value) === p.fee && Number(installRegular.value) === p.regular }

async function applyReferralCode () {
  const code = referralCode.value.trim().toUpperCase()
  if (!code) return
  referralChecking.value = true
  referralError.value = ''
  let accept = false
  try {
    const res = await fetch(`${HUB_URL}/api/referral/validate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code }),
    })
    if (res.ok) {
      const data = await res.json()
      if (data.ok) accept = true
      else referralError.value = data.error || 'Code invalide'
    } else if (res.status === 404) {
      accept = true // endpoint not yet deployed — accept, validate at submit
    } else {
      referralError.value = 'Erreur de validation (' + res.status + ')'
    }
  } catch (e) {
    accept = true // offline / endpoint missing — accept, validate at submit
  }
  if (accept) {
    referralCode.value = code
    orderItems.value.push({
      item_code: REFERRAL_ITEM_CODE,
      item_name: 'Crédit parrainage · ' + code,
      qty: 1,
      rate: -Math.abs(REFERRAL_CREDIT_AMOUNT),
      regular_price: 0,
      billing: 'onetime',
      billing_interval: null,
      contract_months: 0,
      is_rebate: true,
      referral_code: code,
    })
    referralApplied.value = true
  }
  referralChecking.value = false
}

function removeReferralCode () {
  orderItems.value = orderItems.value.filter(i => i.item_code !== REFERRAL_ITEM_CODE)
  referralApplied.value = false
  referralError.value = ''
  referralCode.value = ''
}

const publishLabel = computed(() => {
  if (orderMode.value === 'quotation') return 'Créer le devis & projet'
  if (orderMode.value === 'prepaid') return 'Créer la facture & projet'
  return 'Créer la commande & projet'
})

const groupOptions = ASSIGNED_GROUPS.map(g => ({ label: g, value: g }))

function dependencyOptionsFor (stepIndex) {
  return wizardSteps.value
    .map((s, i) => ({ label: `${i + 1}. ${s.subject?.substring(0, 35) || 'Étape'}`, value: i }))
    .filter((_, i) => i !== stepIndex)
}

// Quick-sale = no installation steps in the cart. Derived from wizardSteps
// length, so loading a template automatically exits quick-sale and clearing
// the steps re-enters it. Matches the rule "wizard is forced only when a
// product needs dependency-of-tasks".
const isQuickSale = computed(() => wizardSteps.value.length === 0)

const canProceed = computed(() => {
  if (currentStep.value === 0) return true
  if (currentStep.value === 1) return wizardSteps.value.every(s => s.subject?.trim())
  // Step 2 (Items/Devis) → Step 3 (Sommaire): block if no Internet tier picked.
  // Internet is mandatory; skipping Sommaire on an empty cart wastes a click.
  if (currentStep.value === 2) return orderItems.value.length > 0
  // Step 3 (Sommaire) → Step 4 (Publier): always OK once there's at least one item.
  if (currentStep.value === 3) return orderItems.value.length > 0
  return true
})

function goNext () {
  if (currentStep.value === 0 && isQuickSale.value) { currentStep.value = 2; return }
  currentStep.value++
}

function goBack () {
  if (currentStep.value === 2 && isQuickSale.value) { currentStep.value = 0; return }
  currentStep.value--
}

function handleStepDotClick (i) {
  if (i >= currentStep.value) return
  if (isQuickSale.value && i === 1) { currentStep.value = 0; return }
  currentStep.value = i
}

function selectQuickSale () {
  selectedTemplate.value = null
  wizardSteps.value = []
  customSteps.value = []
  currentStep.value = 2
  setTimeout(() => openCatalog(), 180)
}

function selectTemplate (tpl) {
  selectedTemplate.value = tpl
  wizardSteps.value = tpl.steps.map(s => ({
    subject: s.subject, job_type: s.job_type, priority: s.priority, duration_h: s.duration_h,
    assigned_group: s.assigned_group || '', depends_on_step: s.depends_on_step, scheduled_date: '',
    on_open_webhook: s.on_open_webhook || '', on_close_webhook: s.on_close_webhook || '',
  }))
}

function selectCustom () {
  selectedTemplate.value = null
  if (!customSteps.value.length) customSteps.value = [makeEmptyStep()]
  wizardSteps.value = customSteps.value
}

function makeEmptyStep () {
  return { subject: '', job_type: 'Autre', priority: 'medium', duration_h: 1, assigned_group: '', depends_on_step: null, scheduled_date: '', on_open_webhook: '', on_close_webhook: '' }
}

function addStep () { wizardSteps.value.push(makeEmptyStep()) }

function removeStep (i) {
  wizardSteps.value.forEach(s => {
    if (s.depends_on_step === i) s.depends_on_step = null
    else if (s.depends_on_step != null && s.depends_on_step > i) s.depends_on_step--
  })
  wizardSteps.value.splice(i, 1)
}

function cancel () {
  currentStep.value = 0
  selectedTemplate.value = null
  wizardSteps.value = []
  customSteps.value = []
  orderItems.value = []
  orderMode.value = 'direct'
  contractNotes.value = ''
  requireAcceptance.value = false
  acceptanceMethod.value = 'jwt'
  clientPhone.value = ''
  clientEmail.value = ''
  publishedDone.value = false
  publishedDocName.value = ''
  publishedDocType.value = ''
  publishedContractName.value = ''
  pdfCopied.value = false
  pendingAcceptance.value = false
  acceptanceLinkSent.value = false
  acceptanceSentVia.value = ''
  acceptanceLinkUrl.value = ''
  publishedJobCount.value = 0
  sendChannel.value = 'email'
  sendTo.value = ''
  sending.value = false
  sendResult.value = null
  emit('update:modelValue', false)
}

watch(() => props.modelValue, (v) => {
  if (v) {
    selectedTemplate.value = null
    wizardSteps.value = []
    customSteps.value = []
    templateLoadedFor.value = new Set()
    lastMergeCount.value = 0
    mergedTemplateLabels.value = []
    orderItems.value = []
    expandedBundles.value = new Set()
    installStepExpanded.value = false
    // Residential commitment is the default expected path when launching
    // from a customer page without a pre-existing Issue.
    const fromCustomer = !!(props.customer && !props.issue?.name)
    orderMode.value = fromCustomer ? 'quotation' : 'direct'
    contractNotes.value = ''
    requireAcceptance.value = fromCustomer
    acceptanceMethod.value = 'jwt'
    clientPhone.value = props.customer?.cell_phone || props.customer?.tel_home || ''
    clientEmail.value = props.customer?.email_billing || props.customer?.email_id || ''
    // Launched from a customer → skip straight to the Items step (no template
    // pre-selection). Installation steps appear only if a product requires one.
    // Launched from an existing Issue → start at step 0 so the agent can pick
    // a project template aligned with the ticket.
    currentStep.value = fromCustomer ? 2 : 0
    // Forfait déjà présenté/choisi dans QuoteWizard (services + promos avant contact) → pré-remplir le panier.
    if (props.initialTier && props.initialTier.id) {
      const full = internetHeroTiers.find(t => t.id === props.initialTier.id)
      if (full) { try { onInternetTierClick(full) } catch (e) {} }
    }
  }
})
</script>

<style scoped>
.wizard-step-dot {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  padding: 8px 4px; cursor: default; opacity: 0.4; transition: opacity 0.2s;
}
.wizard-step-dot.active, .wizard-step-dot.done { opacity: 1; }
.wizard-step-dot.done { cursor: pointer; }
.wizard-step-num {
  width: 24px; height: 24px; border-radius: 50%; background: #e2e8f0; color: #475569;
  display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700;
}
.wizard-step-dot.active .wizard-step-num { background: #6366f1; color: #fff; }
.wizard-step-dot.done .wizard-step-num { background: #10b981; color: #fff; }
.wizard-step-dot.skipped { opacity: 0.3; }
.wizard-step-dot.skipped .wizard-step-num { background: #f1f5f9; color: #94a3b8; }
.wizard-step-label { font-size: 0.7rem; color: #6b7280; margin-top: 2px; }
.quick-sale-card {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border: 2px solid #c7d2fe; border-radius: 12px;
  background: linear-gradient(135deg, #eef2ff 0%, #ffffff 100%);
  cursor: pointer; transition: all 0.15s;
}
.quick-sale-card:hover { border-color: #6366f1; box-shadow: 0 4px 12px rgba(99,102,241,0.18); transform: translateY(-1px); }
.quick-sale-card.selected { border-color: #6366f1; background: #eef2ff; }
.quick-sale-icon {
  width: 46px; height: 46px; border-radius: 12px;
  background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  box-shadow: 0 4px 10px rgba(99,102,241,0.3);
}
.quick-sale-title { font-weight: 700; font-size: 0.92rem; color: #1e293b; }
.quick-sale-desc { font-size: 0.76rem; color: #64748b; margin-top: 2px; line-height: 1.35; }
.quick-sale-notice {
  display: flex; align-items: flex-start; padding: 12px 14px;
  background: #eef2ff; border: 1px solid #c7d2fe; border-radius: 10px;
  margin-bottom: 8px;
}
.merge-notice {
  display: flex; align-items: flex-start; padding: 12px 14px;
  background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 10px;
  margin-bottom: 8px;
}
.bundles-summary {
  border: 1px solid #c7d2fe; background: #eef2ff; border-radius: 10px;
  padding: 10px 12px; margin-bottom: 10px;
}
.bundles-summary-header {
  display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
}
.bundles-summary-title {
  font-weight: 700; font-size: 0.85rem; color: #3730a3;
  display: flex; align-items: center; gap: 8px;
}
.bundles-summary-count {
  background: #c7d2fe; color: #3730a3; font-weight: 600;
  padding: 2px 8px; border-radius: 10px; font-size: 0.72rem;
}
.bundles-summary-list { display: flex; flex-direction: column; gap: 6px; }
.bundle-item {
  background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px;
  overflow: hidden;
}
.bundle-item-header {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  cursor: pointer; transition: background 0.15s;
}
.bundle-item-header:hover { background: #f8fafc; }
.bundle-item-name { flex: 1; min-width: 0; }
.bundle-item-steps {
  border-top: 1px solid #e2e8f0; padding: 6px 10px 8px 34px;
  background: #fafbfc; display: flex; flex-direction: column; gap: 4px;
}
.bundle-step-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 0.78rem; color: #334155;
}
.bundle-step-subject { flex: 1; }
.bundle-add-step {
  display: flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 6px 8px; margin-top: 2px; border-radius: 6px;
  color: #4f46e5; font-size: 0.76rem; font-weight: 600;
  border: 1px dashed #c7d2fe; background: #ffffff;
  transition: background 0.15s;
}
.bundle-add-step:hover { background: #eef2ff; }

.pricing-compact {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: #f0fdfa; border: 1px solid #99f6e4; border-radius: 8px;
  padding: 6px 10px; margin-bottom: 8px;
}
.pricing-compact-label { font-weight: 600; color: #115e59; font-size: 0.78rem; }
.pricing-area-badge {
  background: #ccfbf1; color: #115e59; font-weight: 600;
  padding: 2px 8px; border-radius: 10px; font-size: 0.7rem;
}
.pricing-area-default { background: #fef3c7; color: #92400e; }

.bundle-step-install { background: #f0fdfa; border-radius: 6px; padding: 4px 6px; }
.install-step-panel {
  background: #f0fdfa; border: 1px solid #99f6e4; border-radius: 8px;
  margin: 4px 0 4px 26px; padding: 10px 12px;
  display: flex; flex-direction: column; gap: 8px;
}
.install-step-area-header {
  display: flex; align-items: center; gap: 6px;
  padding-bottom: 6px; border-bottom: 1px solid #ccfbf1;
}
.install-step-area-name { font-weight: 700; color: #115e59; font-size: 0.8rem; }
.install-step-area-meta { font-size: 0.72rem; color: #475569; }
.install-step-row { display: flex; align-items: center; gap: 10px; }
.install-step-row-label { font-size: 0.8rem; color: #0f172a; font-weight: 600; flex: 1; }
.install-step-extras { background: #ffffff; border: 1px solid #ccfbf1; border-radius: 6px; padding: 6px 8px; }
.install-step-extras-head { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.install-step-extras-empty { font-size: 0.74rem; color: #64748b; font-style: italic; padding: 3px 0; }
.install-step-extra-row {
  display: flex; align-items: center; gap: 8px; padding: 5px 0;
  border-top: 1px solid #f1f5f9;
}
.install-step-extra-row:first-of-type { border-top: 0; }
.install-step-extra-text { flex: 1; min-width: 0; }
.install-step-extra-amount { font-weight: 700; color: #0f766e; font-size: 0.8rem; }
.install-step-total {
  display: flex; align-items: center; gap: 8px;
  background: #ccfbf1; border-radius: 6px; padding: 5px 8px;
}
.template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
.template-card {
  border: 2px solid #e2e8f0; border-radius: 10px; padding: 14px;
  cursor: pointer; transition: all 0.15s; text-align: center;
}
.template-card:hover { border-color: #6366f1; background: #f5f3ff; }
.template-card.selected { border-color: #6366f1; background: #eef2ff; }
.template-card-name { font-weight: 700; font-size: 0.85rem; margin-top: 6px; color: #1e293b; }
.template-card-desc { font-size: 0.75rem; color: #6b7280; margin-top: 2px; }
.steps-list { display: flex; flex-direction: column; gap: 10px; }
.step-card {
  border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 12px;
  background: #fff; transition: border-color 0.15s;
}
.step-card:hover { border-color: #6366f1; }
.step-num {
  width: 26px; height: 26px; border-radius: 50%; background: #6366f1; color: #fff;
  font-weight: 700; font-size: 0.8rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.step-num.small { width: 22px; height: 22px; font-size: 0.7rem; }
.step-dep-indicator {
  display: flex; align-items: center; gap: 4px;
  margin-top: 4px; padding: 2px 6px; background: #fff7ed; border-radius: 4px;
}
.review-tree { display: flex; flex-direction: column; }
.review-step-connector { text-align: center; padding: 0; line-height: 1; }
.review-step-card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 12px; background: #fff; }
.review-step-card.has-dep { border-left: 3px solid #f59e0b; }
.catalog-picker { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px 12px; }
.catalog-open-btn { border-radius: 8px; padding: 4px 14px; font-weight: 600; letter-spacing: 0.2px; }
.catalog-modal {
  width: 560px; max-width: 95vw; height: 80vh; max-height: 720px;
  display: flex; flex-direction: column; border-radius: 14px; overflow: hidden;
}
.catalog-modal-header {
  padding: 14px 16px 10px; border-bottom: 1px solid #e2e8f0;
  background: linear-gradient(180deg, #ffffff, #f8fafc);
}
.catalog-search :deep(.q-field__control) { border-radius: 10px; }
.catalog-cat-row {
  display: flex; gap: 6px; overflow-x: auto; margin-top: 10px;
  padding-bottom: 4px; scrollbar-width: none;
}
.catalog-cat-row::-webkit-scrollbar { display: none; }
.catalog-cat-chip {
  display: inline-flex; align-items: center; flex-shrink: 0;
  padding: 5px 12px; border-radius: 999px; background: #f1f5f9; color: #475569;
  font-size: 0.78rem; font-weight: 500; cursor: pointer; transition: all 0.15s;
  white-space: nowrap;
}
.catalog-cat-chip:hover { background: #e2e8f0; }
.catalog-cat-chip.active { background: #6366f1; color: #fff; box-shadow: 0 2px 6px rgba(99,102,241,0.3); }
.catalog-modal-body { padding: 10px 12px; background: #fafbfc; }
.catalog-list { display: flex; flex-direction: column; gap: 8px; padding-bottom: 8px; }
.catalog-card {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; background: #fff; border: 1px solid #e2e8f0;
  border-radius: 12px; cursor: pointer; transition: all 0.15s;
  position: relative;
}
.catalog-card:hover { border-color: #6366f1; box-shadow: 0 2px 8px rgba(99,102,241,0.12); transform: translateY(-1px); }
.catalog-card:active { transform: translateY(0); }
.catalog-card.just-added {
  animation: catalogPulse 0.9s ease-out;
}
@keyframes catalogPulse {
  0% { background: #fff; }
  30% { background: #dcfce7; border-color: #10b981; box-shadow: 0 0 0 4px rgba(16,185,129,0.2); }
  100% { background: #fff; }
}
.catalog-card-icon {
  width: 42px; height: 42px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.catalog-card-body { flex: 1; min-width: 0; }
.catalog-card-name {
  font-size: 0.88rem; font-weight: 600; color: #1e293b;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.catalog-card-meta { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
.catalog-card-code { font-size: 0.7rem; color: #94a3b8; font-family: ui-monospace, monospace; }
.catalog-card-chip { height: 18px; font-size: 0.65rem !important; }
.catalog-card-price { text-align: right; flex-shrink: 0; }
.catalog-card-rate { font-size: 0.95rem; font-weight: 700; color: #1e293b; }
.catalog-card-billing { font-size: 0.68rem; font-weight: 500; margin-top: -2px; }
.catalog-card-billing.recurring { color: #ea580c; }
.catalog-card-billing.onetime { color: #64748b; }
.catalog-card-add { flex-shrink: 0; }
.catalog-modal-footer {
  padding: 10px 14px; border-top: 1px solid #e2e8f0; background: #fff;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.catalog-footer-counter { font-size: 0.8rem; color: #475569; display: flex; align-items: center; }
.catalog-footer-added { margin-left: 4px; }
.catalog-loading, .catalog-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 60px 20px; text-align: center;
}
@media (max-width: 599px) {
  .catalog-modal { border-radius: 0; height: 100vh; max-height: 100vh; width: 100vw; max-width: 100vw; }
}
.presets-row { background: #fefce8; border: 1px solid #fde68a; border-radius: 10px; padding: 8px 10px; }
.preset-hero {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border: 1.5px solid #c7d2fe; border-radius: 10px;
  background: linear-gradient(135deg, #ffffff 0%, #eef2ff 100%);
  cursor: pointer; transition: all 0.15s;
}
.preset-hero:hover {
  border-color: #6366f1; box-shadow: 0 2px 10px rgba(99, 102, 241, 0.18);
  transform: translateY(-1px);
}
.preset-hero.preset-hero-added {
  border-color: #10b981;
  background: linear-gradient(135deg, #ffffff 0%, #ecfdf5 100%);
}
.preset-hero-icon {
  width: 44px; height: 44px; border-radius: 10px;
  background: #fff; border: 1px solid #e2e8f0;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.preset-hero-text { flex: 1; min-width: 0; }
.preset-hero-label { font-size: 0.95rem; font-weight: 700; color: #1e293b; }
.preset-hero-desc { font-size: 0.75rem; color: #64748b; margin-top: 2px; }
.preset-hero-price { text-align: right; line-height: 1.1; }
.preset-hero-price-tag { font-size: 0.66rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.4px; }
.preset-hero-price-big { font-size: 1.35rem; font-weight: 800; color: #3730a3; }
.preset-hero-price-unit { font-size: 0.7rem; font-weight: 600; color: #64748b; margin-left: 2px; }
.preset-hero-price-strike {
  font-size: 0.7rem; color: #94a3b8; text-decoration: line-through; margin-top: 1px;
}
.preset-hero-group { position: relative; }
.preset-hero-label-tier { color: #3730a3; }
.preset-hero-action { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.preset-hero-chevron { transition: transform 0.18s; }
.preset-hero-chevron.rotated { transform: rotate(180deg); }

.tier-expand {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 8px; margin-top: 8px;
  padding: 10px; border: 1px solid #e0e7ff; border-radius: 10px;
  background: #fafbff;
  animation: tier-fade-in 0.18s ease-out;
}
@keyframes tier-fade-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.tier-card {
  position: relative; padding: 10px 12px; border: 1.5px solid #e2e8f0;
  border-radius: 8px; background: #fff; cursor: pointer; transition: all 0.15s;
  display: flex; flex-direction: column; gap: 4px;
}
.tier-card:hover {
  border-color: #6366f1; box-shadow: 0 2px 8px rgba(99, 102, 241, 0.18);
  transform: translateY(-1px);
}
.tier-card.tier-card-active {
  border-color: #10b981; background: linear-gradient(135deg, #ffffff 0%, #ecfdf5 100%);
}
.tier-card-head { display: flex; align-items: center; gap: 6px; }
.tier-card-title { font-size: 0.88rem; font-weight: 700; color: #1e293b; flex: 1; }
.tier-card-badge { font-size: 0.6rem; padding: 2px 6px; }
.tier-card-speed { font-size: 0.72rem; color: #475569; font-weight: 600; }
.tier-card-desc { font-size: 0.7rem; color: #64748b; line-height: 1.25; }
.tier-card-price { display: flex; align-items: baseline; gap: 4px; margin-top: 4px; }
.tier-card-price-big { font-size: 1.05rem; font-weight: 800; color: #3730a3; }
.tier-card-price-unit { font-size: 0.65rem; color: #64748b; font-weight: 600; }
.tier-card-price-strike {
  font-size: 0.65rem; color: #94a3b8; text-decoration: line-through; margin-left: 4px;
}
.tier-card-check { position: absolute; top: 8px; right: 8px; }
.preset-upsell {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 8px;
  background: #fff; cursor: pointer; transition: all 0.15s;
  flex: 1; min-width: 150px;
}
.preset-upsell:hover { border-color: #6366f1; background: #eef2ff; box-shadow: 0 1px 4px rgba(99, 102, 241, 0.15); }
.preset-upsell.preset-added { border-color: #10b981; background: #f0fdf4; }
.preset-upsell.tier-addon { border-style: dashed; }
.preset-upsell-body { min-width: 0; flex: 1; line-height: 1.15; }
.preset-upsell-delta { font-size: 0.82rem; font-weight: 700; color: #3730a3; }
.preset-upsell-mo { font-size: 0.65rem; font-weight: 600; color: #64748b; margin-left: 2px; }
.preset-upsell-label { font-size: 0.72rem; color: #475569; }
.preset-upsell.preset-added .preset-upsell-delta { color: #047857; }

.referral-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; border: 1px dashed #c4b5fd; border-radius: 8px;
  background: #faf5ff;
}
.referral-title { font-size: 0.8rem; font-weight: 600; color: #4c1d95; }
.referral-desc { font-size: 0.7rem; color: #6b7280; }
.referral-error { font-size: 0.72rem; color: #b91c1c; margin-top: 4px; padding-left: 4px; }
.catalog-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 6px; max-height: 220px; overflow-y: auto;
}
.catalog-item {
  border: 1px solid #e2e8f0; border-radius: 8px; padding: 8px 10px;
  background: #fff; cursor: pointer; transition: all 0.15s;
}
.catalog-item:hover { border-color: #6366f1; background: #f5f3ff; box-shadow: 0 1px 4px rgba(99, 102, 241, 0.12); }
.item-line { border: 1px solid #e2e8f0; border-radius: 8px; padding: 8px 10px; margin-bottom: 6px; background: #fafbfc; }

.billing-pill {
  display: inline-flex; align-items: center;
  border: 1px solid #e2e8f0; border-radius: 999px;
  background: #fff; padding: 2px 8px; font-size: 0.7rem; font-weight: 700;
  color: #475569; cursor: pointer; transition: all 0.15s;
  font-family: inherit; line-height: 1.2;
}
.billing-pill:hover { border-color: #6366f1; background: #eef2ff; }
.billing-pill.recurring { background: #fff7ed; border-color: #fed7aa; color: #c2410c; }
.billing-pill.recurring:hover { background: #ffedd5; border-color: #fb923c; }
.billing-pill.yearly { background: #ecfdf5; border-color: #a7f3d0; color: #047857; }
.billing-pill.yearly:hover { background: #d1fae5; border-color: #34d399; }
.billing-pill.onetime { background: #eef2ff; border-color: #c7d2fe; color: #3730a3; }
.billing-pill.onetime:hover { background: #e0e7ff; border-color: #818cf8; }

.item-steps-wrap { display: flex; flex-direction: column; gap: 4px; }
.item-steps-pill {
  align-self: flex-start;
  display: inline-flex; align-items: center;
  padding: 3px 10px; border-radius: 999px;
  background: #eef2ff; border: 1px solid #c7d2fe;
  color: #3730a3; font-size: 0.72rem; font-weight: 600;
  cursor: pointer; transition: all 0.15s; font-family: inherit;
}
.item-steps-pill:hover { background: #e0e7ff; border-color: #6366f1; }
.item-steps-pill.expanded { background: #6366f1; border-color: #4f46e5; color: #fff; }
.item-steps-panel {
  border: 1px solid #e2e8f0; border-radius: 8px;
  background: #ffffff; padding: 6px 10px;
  display: flex; flex-direction: column; gap: 4px;
  margin-top: 2px;
}
.item-steps-panel .bundle-step-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 0.78rem; color: #334155; padding: 2px 0;
}
.item-steps-panel .bundle-step-subject { flex: 1; min-width: 0; }
.step-extra-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: 999px;
  background: #fff7ed; border: 1px dashed #fed7aa;
  color: #9a3412; font-size: 0.72rem; font-weight: 600;
  cursor: pointer; font-family: inherit;
  transition: background 0.15s, border-color 0.15s;
  max-width: 240px;
}
.step-extra-chip:hover { background: #ffedd5; border-color: #fb923c; }
.step-extra-chip-set {
  background: #ffedd5; border-style: solid; border-color: #fb923c;
}
.step-extra-chip-label { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.step-extra-chip-amount { font-weight: 700; color: #c2410c; margin-left: 2px; }
.step-extra-chip-placeholder { color: #b45309; }
.step-extra-menu { min-width: 300px; padding: 8px 10px; }
.step-extra-menu-head {
  font-size: 0.72rem; font-weight: 700; color: #9a3412;
  text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 4px 6px;
}
.step-extra-menu-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
}
.step-extra-preset {
  display: flex; align-items: center; justify-content: space-between;
  padding: 5px 8px; border-radius: 6px; cursor: pointer; font-family: inherit;
  background: #fff7ed; border: 1px solid #fed7aa; color: #7c2d12;
  font-size: 0.75rem; text-align: left; gap: 6px;
  transition: background 0.15s, border-color 0.15s;
}
.step-extra-preset:hover { background: #ffedd5; border-color: #fb923c; }
.step-extra-preset.active {
  background: #fb923c; border-color: #ea580c; color: #fff;
}
.step-extra-preset.active .step-extra-preset-amount { color: #fff; }
.step-extra-preset-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.step-extra-preset-amount { font-weight: 700; color: #c2410c; font-size: 0.72rem; }
.step-extra-custom { padding: 4px; }
.step-extra-custom-title {
  font-size: 0.72rem; font-weight: 700; color: #64748b;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.step-extra-remove {
  display: flex; align-items: center; gap: 4px; width: 100%;
  padding: 5px 8px; border-radius: 6px; cursor: pointer; font-family: inherit;
  background: transparent; border: none; color: #dc2626;
  font-size: 0.75rem; font-weight: 600;
}
.step-extra-remove:hover { background: #fee2e2; }
.item-steps-panel .bundle-add-step {
  display: flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 4px 8px; margin-top: 2px; border-radius: 6px;
  color: #4f46e5; font-size: 0.74rem; font-weight: 600;
  border: 1px dashed #c7d2fe; background: #ffffff;
  transition: background 0.15s; font-family: inherit;
}
.item-steps-panel .bundle-add-step:hover { background: #eef2ff; }
.catalog-bound :deep(.q-field__control) { cursor: pointer; }
.catalog-bound :deep(.q-field__control):hover { border-color: #6366f1; background: #f5f3ff; }
.catalog-bound :deep(input) { cursor: pointer !important; color: #1e293b; font-weight: 600; }
.totals-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 12px; }

/* TV channel picker dialog */
.channel-picker-card {
  width: 560px; max-width: 96vw;
  display: flex; flex-direction: column; max-height: 90vh;
}
.channel-picker-head { border-bottom: 1px solid #f1f5f9; }
.channel-picker-sub {
  font-size: 0.78rem; color: #64748b; margin-top: 2px;
}
.channel-picker-overage {
  color: #ea580c; font-weight: 600; margin-left: 4px;
}
.channel-picker-chips {
  display: flex; flex-wrap: wrap; gap: 6px;
  border-bottom: 1px solid #f1f5f9; padding-top: 6px; padding-bottom: 10px;
}
.channel-chip {
  position: relative; padding-right: 32px !important;
  font-size: 0.78rem; font-weight: 600;
}
.channel-chip-pastille {
  position: absolute; top: -4px; right: 4px;
  min-width: 16px; height: 16px; font-size: 0.62rem;
  padding: 0 4px; font-weight: 700;
}
.channel-chip-name { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.channel-picker-list {
  flex: 1; overflow-y: auto; padding: 4px 0;
  display: flex; flex-direction: column; gap: 2px;
}
.channel-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 16px; border-radius: 0; cursor: pointer;
  transition: background 0.12s;
}
.channel-row:hover { background: #f8fafc; }
.channel-row-selected { background: #ecfdf5; }
.channel-row-selected:hover { background: #d1fae5; }
.channel-row-premium .channel-row-name { color: #9d174d; }
.channel-row-name {
  font-size: 0.85rem; font-weight: 600; color: #1e293b;
  display: flex; align-items: center; gap: 4px;
}
.channel-row-sub {
  font-size: 0.72rem; color: #94a3b8; margin-top: 1px;
}
.channel-row-surcharge {
  font-size: 0.78rem; font-weight: 700; color: #be185d;
}
.channel-row-empty {
  padding: 24px; text-align: center; color: #94a3b8; font-size: 0.85rem;
}
.channel-picker-foot { border-top: 1px solid #f1f5f9; }

/* Tier segmented toggle inside picker */
.channel-picker-tiers {
  display: flex; gap: 8px;
  border-bottom: 1px solid #f1f5f9;
}
.channel-picker-tier {
  flex: 1; padding: 8px 10px; border-radius: 8px;
  border: 1px solid #e5e7eb; background: #fafafa;
  cursor: pointer; transition: all 0.15s;
  display: flex; flex-direction: column; gap: 2px;
}
.channel-picker-tier:hover { border-color: #f472b6; background: #fdf2f8; }
.channel-picker-tier-active {
  border-color: #ec4899; background: #fdf2f8;
  box-shadow: 0 0 0 2px #fbcfe8;
}
.channel-picker-tier-label { font-size: 0.84rem; font-weight: 700; color: #1e293b; }
.channel-picker-tier-price { font-size: 0.82rem; font-weight: 700; color: #be185d; }
.channel-picker-tier-meta { font-size: 0.7rem; color: #64748b; }

/* TV group gated behind Internet */
.preset-hero-group-disabled .preset-hero {
  background: #f8fafc; border-color: #e5e7eb; opacity: 0.65;
}
.preset-hero-group-disabled .preset-hero:hover { background: #f1f5f9; }
.preset-hero-group-disabled .preset-hero-label::after {
  content: ' — Internet requis'; color: #9ca3af; font-weight: 400; font-size: 0.78rem;
}

/* Telephony hero group + port-in input */
.preset-hero-phone { border-color: #14b8a6; }
.preset-hero-phone:hover { background: #f0fdfa; }
.preset-hero-label-phone { color: #0f766e; }
.preset-hero-price-phone { color: #0f766e; }
.tier-expand-phone .tier-card:hover { border-color: #14b8a6; background: #f0fdfa; }
.tier-card-keep-input {
  margin-top: 8px; padding-top: 8px;
  border-top: 1px dashed #e5e7eb;
}

/* ── Wizard step blocks (Étape 1 Internet, Étape 2 Services additionnels) ── */
.wizard-step-block {
  border: 1px solid #e2e8f0; border-radius: 10px;
  background: #fefefe; padding: 10px 12px;
}
.wizard-step-block-required {
  background: linear-gradient(180deg, #fefce8 0%, #ffffff 50%);
  border-color: #fde68a;
}
.wizard-step-header {
  display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px;
}
.wizard-step-header-clickable {
  cursor: pointer;
  padding: 2px;
  border-radius: 8px;
  margin: -2px -2px 6px -2px;
  transition: background 0.15s;
}
.wizard-step-header-clickable:hover {
  background: rgba(99, 102, 241, 0.05);
}
.wizard-step-action {
  display: flex; align-items: center; gap: 6px;
  flex-shrink: 0; padding-top: 2px;
}
.wizard-step-block-expanded { box-shadow: 0 1px 4px rgba(99, 102, 241, 0.08); }
.wizard-step-number {
  flex-shrink: 0;
  width: 26px; height: 26px; border-radius: 50%;
  background: #6366f1; color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.85rem; font-weight: 800;
  box-shadow: 0 1px 3px rgba(99, 102, 241, 0.35);
}
.wizard-step-number-optional {
  background: #94a3b8;
  box-shadow: 0 1px 3px rgba(148, 163, 184, 0.35);
}
.wizard-step-headline { flex: 1; min-width: 0; }
.wizard-step-title {
  font-size: 0.92rem; font-weight: 800; color: #1e293b; line-height: 1.3;
}
.wizard-step-title-note {
  font-weight: 500; color: #64748b; font-size: 0.8rem;
}
.wizard-step-summary {
  font-size: 0.76rem; color: #475569; margin-top: 2px;
  line-height: 1.35;
}
.wizard-step-summary-chips { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.wizard-step-chip {
  display: inline-flex; align-items: center;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid transparent;
  font-size: 0.72rem; font-weight: 600;
  background: #eef2ff; color: #3730a3; border-color: #c7d2fe;
}
.wizard-step-chip-tv { background: #fdf2f8; color: #9d174d; border-color: #fbcfe8; }
.wizard-step-chip-phone { background: #f0fdfa; color: #0f766e; border-color: #99f6e4; }
.wizard-step-chip-upsell { background: #ecfeff; color: #155e75; border-color: #a5f3fc; }

/* ── Internet tier list (accordion content) ── */
.tier-list {
  display: flex; flex-direction: column; gap: 6px;
  animation: tier-fade-in 0.18s ease-out;
}
.tier-row {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  border: 1.5px solid #e2e8f0; border-radius: 10px;
  background: #fff; cursor: pointer; transition: all 0.15s;
}
.tier-row:hover {
  border-color: #6366f1; background: #f5f3ff;
  box-shadow: 0 1px 4px rgba(99, 102, 241, 0.18);
}
.tier-row-active {
  border-color: #10b981;
  background: linear-gradient(90deg, #ecfdf5 0%, #ffffff 60%);
  box-shadow: 0 1px 4px rgba(16, 185, 129, 0.15);
}
.tier-row-icon {
  flex-shrink: 0;
  width: 40px; height: 40px; border-radius: 8px;
  background: #fff; border: 1px solid #e2e8f0;
  display: flex; align-items: center; justify-content: center;
}
.tier-row-active .tier-row-icon { border-color: #86efac; background: #ecfdf5; }
.tier-row-body { flex: 1; min-width: 0; }
.tier-row-head {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.tier-row-title { font-size: 0.92rem; font-weight: 700; color: #1e293b; }
.tier-row-badge { font-size: 0.6rem; padding: 2px 6px; }
.tier-row-speed { font-size: 0.78rem; color: #475569; font-weight: 600; }
.tier-row-desc { font-size: 0.72rem; color: #64748b; line-height: 1.3; margin-top: 2px; }
.tier-row-price { text-align: right; flex-shrink: 0; line-height: 1.1; }
.tier-row-price-big { font-size: 1.05rem; font-weight: 800; color: #3730a3; }
.tier-row-active .tier-row-price-big { color: #047857; }
.tier-row-price-unit { font-size: 0.68rem; font-weight: 600; color: #64748b; margin-left: 2px; }
.tier-row-price-strike {
  font-size: 0.68rem; color: #94a3b8; text-decoration: line-through; margin-top: 1px;
}
.tier-row-select { flex-shrink: 0; }
@media (max-width: 480px) {
  .tier-row { padding: 8px 10px; gap: 8px; }
  .tier-row-icon { width: 32px; height: 32px; }
  .tier-row-title { font-size: 0.86rem; }
  .tier-row-desc { display: none; }
  .tier-row-price-big { font-size: 0.95rem; }
}

/* ── Cart pill (Step 2 → Sommaire handoff) ─────────────────────────────
   Sticky summary at the top of the Items/Devis scroll area. Combines
   item count + running totals + taxes into a single strip; details live
   on the Sommaire step (click → navigate). */
.cw-compet { border: 1px solid #fde3d3; background: #fff8f4; border-radius: 10px; }
.cart-pill {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px;
  border: 1.5px solid #c7d2fe; border-radius: 12px;
  background: linear-gradient(90deg, #eef2ff 0%, #ffffff 70%);
  cursor: pointer; transition: all 0.15s;
  position: relative;
}
.cart-pill:hover { border-color: #6366f1; box-shadow: 0 1px 6px rgba(99, 102, 241, 0.25); }
.cart-pill-empty {
  border-color: #e2e8f0; background: #f8fafc; cursor: default;
}
.cart-pill-empty:hover { border-color: #e2e8f0; box-shadow: none; }
/* Sticky variant — stays pinned at the top of the card section so the
   running tally is always visible while scrolling through pickers. */
.cart-pill-sticky {
  position: sticky; top: 0; z-index: 5;
  /* Slightly stronger shadow to float above pickers below. */
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
  background: linear-gradient(90deg, #eef2ff 0%, #f5f3ff 100%);
  backdrop-filter: saturate(1.1);
  margin-bottom: 10px;
}
.cart-pill-sticky.cart-pill-empty {
  background: #f8fafc;
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
.cart-pill-left { position: relative; flex-shrink: 0; padding: 2px; }
.cart-pill-badge { font-size: 0.65rem; padding: 1px 5px; min-height: 16px; }
.cart-pill-body { flex: 1; min-width: 0; }
.cart-pill-title { font-size: 0.88rem; font-weight: 800; color: #1e293b; line-height: 1.2; }
.cart-pill-empty .cart-pill-title { color: #64748b; font-weight: 700; }
.cart-pill-sub { font-size: 0.74rem; line-height: 1.3; margin-top: 2px; }
.cart-pill-totals {
  text-align: right; line-height: 1.15; flex-shrink: 0;
  border-left: 1px solid #e0e7ff; padding-left: 10px;
}
.cart-pill-total-line { font-size: 0.88rem; font-weight: 800; color: #3730a3; }
.cart-pill-total-recurring { color: #c2410c; }
.cart-pill-mo { font-size: 0.62rem; font-weight: 600; margin-left: 1px; }
.cart-pill-tax { font-size: 0.66rem; color: #64748b; margin-top: 1px; }

/* ── Sommaire step (cart detail) ───────────────────────────────────────
   Each row collapses to a compact summary; clicking expands an inline
   editor. Drag handle on the left reorders the list. */
.sommaire-empty {
  text-align: center; padding: 30px 16px;
  border: 2px dashed #e2e8f0; border-radius: 10px; background: #f8fafc;
}
.sommaire-list { display: flex; flex-direction: column; gap: 6px; }
.sommaire-row {
  border: 1.5px solid #e2e8f0; border-radius: 10px;
  background: #fff; transition: all 0.15s;
  overflow: hidden;
}
.sommaire-row:hover { border-color: #c7d2fe; }
.sommaire-row-expanded { border-color: #6366f1; box-shadow: 0 1px 6px rgba(99, 102, 241, 0.18); }
.sommaire-row-dragging { opacity: 0.4; }
.sommaire-row-drop-target {
  border-color: #10b981; border-style: dashed;
  background: linear-gradient(90deg, #ecfdf5 0%, #ffffff 60%);
}
.sommaire-head {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; cursor: pointer;
}
.sommaire-handle {
  cursor: grab; padding: 2px; touch-action: none;
  display: flex; align-items: center;
}
.sommaire-handle:active { cursor: grabbing; }
.sommaire-badge {
  flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%;
  background: #6366f1; color: #fff; font-size: 0.72rem; font-weight: 800;
  display: flex; align-items: center; justify-content: center;
}
.sommaire-type-icon { flex-shrink: 0; }
.sommaire-main { flex: 1; min-width: 0; }
.sommaire-name {
  font-size: 0.88rem; font-weight: 700; color: #1e293b;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sommaire-meta {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
  font-size: 0.74rem; color: #475569; margin-top: 1px;
}
.sommaire-sep { color: #cbd5e1; }
.sommaire-body {
  padding: 8px 12px 10px 12px;
  border-top: 1px dashed #e2e8f0;
  background: #f8fafc;
}

@media (max-width: 480px) {
  .cart-pill { padding: 8px 10px; gap: 8px; }
  .cart-pill-title { font-size: 0.82rem; }
  .cart-pill-sub { font-size: 0.7rem; }
  .cart-pill-total-line { font-size: 0.8rem; }
  .cart-pill-tax { font-size: 0.62rem; }
  .cart-pill-totals { padding-left: 6px; }
  .sommaire-head { padding: 6px 8px; gap: 8px; }
  .sommaire-name { font-size: 0.82rem; }
  .sommaire-meta { font-size: 0.7rem; }
  .sommaire-badge { width: 20px; height: 20px; font-size: 0.66rem; }
}
</style>
