/**
 * Fichier  : assets/js/pages/products.js
 * Auteur   : KREMER Régis
 * Version  : 2.6.125
 * Date     : 2026-08-28
 * Changelog : v2.6.101 (2026-07-20) — Filtres compacts cliquables, recherche stable et scroll compensé.
 * Description : JS — Catalogue produits : CRUD, images, packaging, filtres, déclinaisons et localisation imprimable premium
 *
 * Changelog :
 *   v2.1.0 (2026-02-21) — Version initiale auditée et sécurisée
 *   v2.1.1 (2026-02-21) — Corrections sécurité, utils.js centralisé
 *   v2.2.0 (2026-02-21) — Permissions magasins, bons de commande, rapport d'écart,
 *                      import CSV, historique prix, bon de livraison multi-lignes
 *   v2.3.25 (2026-06-09) — Fiabilisation rechargement produit, EAN et rangement stock.
 *   v2.3.26 (2026-06-09) — Refonte ergonomique et accessibilité de la modale produit.
 *   v2.6.3 (2026-07-08) — Lot 3 : fiabilisation sauvegarde produits + PDF produits sélectionnés.
 *   v2.6.9 (2026-07-08) — Correctif cartes produits : le bouton mouvement rapide ne déclenche plus le zoom image ; filtre scroll stabilisé.
 *   v2.6.17 (2026-07-08) — Correctif images produits : URLs stables après filtres et panneau mobile stabilisé.
 *   v2.6.18 (2026-07-08) — Images locales servies via proxy sécurisé pour rester visibles après filtre magasin/recherche/pagination.
 *   v2.6.29 (2026-07-09) — PLAN 5 : bouton Voir sur le plan et redirection vers la localisation cartographique.
 *   v2.6.31 (2026-07-09) — Correctif : bouton plan compact, icône SVG sobre et positionnement carte produit.
 *   v2.6.39 (2026-07-09) — PLANS LOCALISATION VIEW1 : ouverture en modale de consultation sans édition, plan complet et surbrillance.
 *   v2.6.42 (2026-07-09) — PLANS LOCALISATION2 : vue avancée avec zoom de zone, surbrillance renforcée et impression terrain.
 *   v2.6.40 (2026-07-09) — PLANS DESIGN1 : consultation produit en plan fini, magasin explicite et objets architecturaux.
 *   v2.6.43 (2026-07-09) — PLANS PRINT3 : impression localisation via trame premium MageStock et thème clair forcé.
 *   v2.6.67 (2026-07-10) — PLANS VIEWER LOCALIZE UX1 : rendu produit unifié avec le viewer Plans.
 *   v2.6.70 (2026-07-11) — I18N CORE LOCALE CURRENCY1 : coûts, prix et dates selon la locale/devise configurée.
 *   v2.6.81 (2026-07-17) — PRODUCTS MOBILE FLOW5 : intégration du parcours mobile/tablette et retour explicite de sauvegarde.
 *   v2.6.125 (2026-08-28) — PRODUCTS LIST STATUS CSV IMPORT MOBILE FOOTER FIX1 : statut zéro cohérent et import CSV branché sur la modale standard.
 */
// ================================================================
// products.js — Product grid with brand, cost, supplier, datasheets
// ================================================================
const ProdPage = {
    _units: [
        {key:'piece',  label:'pièce'},
        {key:'carton', label:'carton'},
        {key:'palette',label:'palette'},
        {key:'bidon',  label:'bidon'},
        {key:'fut',    label:'fût'},
        {key:'aerosol',label:'aérosol'},
        {key:'sac',    label:'sac'},
        {key:'seau',   label:'seau'},
        {key:'rouleau',label:'rouleau'},
        {key:'bobine', label:'bobine'},
        {key:'tube',   label:'tube'},
        {key:'boite',  label:'boîte'},
        {key:'paquet', label:'paquet'},
        // IMPORTANT : ici "ml" = mètre linéaire (et non millilitre)
        {key:'ml',     label:'mètre linéaire (ml)'},
        {key:'m',      label:'m'},
        {key:'m2',     label:'m²'},
        {key:'m3',     label:'m³'},
        {key:'litre',  label:'litre'},
        {key:'kg',     label:'kg'},
    ],

    // Pagination state
    pageIndex: 1,
    pageSize: 12,
    _shown: [],
    _currentView: 'grid',
    _currentWhFilter: '',

    _getBaseUnit(p){
        const u = (p?.packBaseUnit || '').trim();
        if(u) return u;
        const legacy = (p?.unitType || 'piece');
        if(legacy === 'liter') return 'litre';
        return legacy || 'piece';
    },

    /**
     * Statut de stock commun aux vues liste, vignette et détails Produits.
     * Un stock nul ou négatif ne doit jamais être affiché comme OK.
     */
    _stockStatus(quantity, minStock){
        const qty = Number.isFinite(Number(quantity)) ? Number(quantity) : 0;
        const parsedMin = (minStock === null || minStock === undefined || minStock === '') ? null : Number(minStock);
        const min = Number.isFinite(parsedMin) ? parsedMin : null;
        if(qty <= 0) return { key:'critical', label:'Critique', shortLabel:'Critique', badgeClass:'badge-danger', barClass:'crit' };
        if(min !== null && qty <= min) return { key:'critical', label:'Critique', shortLabel:'Critique', badgeClass:'badge-danger', barClass:'crit' };
        if(min !== null && qty <= min * 1.5) return { key:'low', label:'Stock bas', shortLabel:'Bas', badgeClass:'badge-warning', barClass:'low' };
        return { key:'ok', label:'OK', shortLabel:'OK', badgeClass:'badge-success', barClass:'ok' };
    },
    
    _buildUnitOptions(p){
        const base = this._getBaseUnit(p);
        const hasCarton = (parseFloat(p?.packCartonQty)||0) > 0;
        const hasPalette= (parseFloat(p?.packPaletteQty)||0) > 0;
        const hasBidon  = (parseFloat(p?.packBidonQty)||0) > 0;
        const hasFut    = (parseFloat(p?.packFutQty)||0) > 0;
        const hasAerosol= (parseFloat(p?.packAerosolQty)||0) > 0;
        const hasSac    = (parseFloat(p?.packSacQty)||0) > 0;
        const hasSeau   = (parseFloat(p?.packSeauQty)||0) > 0;
        const hasRouleau= (parseFloat(p?.packRouleauQty)||0) > 0;
        const hasBobine = (parseFloat(p?.packBobineQty)||0) > 0;
        const hasTube   = (parseFloat(p?.packTubeQty)||0) > 0;
        const hasBoite  = (parseFloat(p?.packBoiteQty)||0) > 0;
        const hasPaquet = (parseFloat(p?.packPaquetQty)||0) > 0;

        const allowed = new Set();
        allowed.add(base);
        if(base === 'piece') allowed.add('piece');
        if(hasCarton) allowed.add('carton');
        if(hasPalette) allowed.add('palette');
        if(hasBidon) allowed.add('bidon');
        if(hasFut) allowed.add('fut');
        if(hasAerosol) allowed.add('aerosol');
        if(hasSac) allowed.add('sac');
        if(hasSeau) allowed.add('seau');
        if(hasRouleau) allowed.add('rouleau');
        if(hasBobine) allowed.add('bobine');
        if(hasTube) allowed.add('tube');
        if(hasBoite) allowed.add('boite');
        if(hasPaquet) allowed.add('paquet');

        return this._units
            .filter(u=>allowed.has(u.key))
            .map(u=>`<option value="${u.key}">${u.label}</option>`)
            .join('');
    },
    _getUnitCostPrefill(p, unit){
        const map = (p && typeof p.costByUnit === 'object' && p.costByUnit) ? p.costByUnit : {};
        const v = map?.[unit];
        if(v!=null && v!=='' && !Number.isNaN(parseFloat(v))) return parseFloat(v);
        const base = this._getBaseUnit(p);
        if(unit === base && !Number.isNaN(parseFloat(p?.cump))) return parseFloat(p.cump);
        if(unit === base && !Number.isNaN(parseFloat(p?.cost))) return parseFloat(p.cost);
        if(!Number.isNaN(parseFloat(p?.cost))) return parseFloat(p.cost);
        return 0;
    },

    _esc(v){
        return String(v ?? '').replace(/[&<>'"]/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#039;','"':'&quot;'}[ch]));
    },
    _slug(v){
        return String(v ?? '').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toUpperCase().replace(/[^A-Z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,28);
    },
    _parseMaybeJson(raw, fallback){
        if(Array.isArray(raw) || (raw && typeof raw === 'object')) return raw;
        if(typeof raw === 'string'){
            try { return JSON.parse(raw); } catch(e){}
        }
        return fallback;
    },

    _sanitizeEanList(raw){
        let values = raw;
        if(typeof values === 'string'){
            const trimmed = values.trim();
            if(!trimmed || ['[]','{}','null','undefined'].includes(trimmed.toLowerCase())) return [];
            try { values = JSON.parse(trimmed); }
            catch(e){ values = trimmed.split(/[;,\s]+/); }
        }
        if(!Array.isArray(values)) return [];
        const out = [];
        values.forEach(v => {
            if(v == null || Array.isArray(v) || typeof v === 'object') return;
            const ean = String(v).trim();
            if(!ean || ['[]','{}','null','undefined'].includes(ean.toLowerCase())) return;
            if(!out.includes(ean)) out.push(ean);
        });
        return out;
    },

    _normalizeWarehouseIds(raw){
        let values = raw;
        if(typeof values === 'string'){
            try { values = JSON.parse(values || '[]'); }
            catch(e){ values = String(values).split(/[;,\s]+/); }
        }
        if(!Array.isArray(values)) return [];
        const out = [];
        values.forEach(v => {
            const id = parseInt(v, 10);
            if(Number.isFinite(id) && id > 0 && !out.includes(id)) out.push(id);
        });
        return out;
    },

    _getProductWarehouses(product){
        const allowed = this._normalizeWarehouseIds(product?.allowedWarehouses || []);
        const def = parseInt(product?.defaultWarehouse || 0, 10);
        if(def > 0 && !allowed.includes(def)) allowed.unshift(def);
        return allowed;
    },

    _getPrimaryWarehouseId(product){
        const def = parseInt(product?.defaultWarehouse || 0, 10);
        if(def > 0) return def;
        const allowed = this._getProductWarehouses(product);
        return allowed.length ? allowed[0] : 0;
    },

    _planLocationUrl(productId, extra={}){
        const params = new URLSearchParams({ productId:String(productId || '') });
        Object.entries(extra || {}).forEach(([k,v]) => {
            if(v !== undefined && v !== null && v !== '') params.set(k, String(v));
        });
        return Api.url('plans?' + params.toString());
    },

    async openProductPlan(productId, ev=null){
        if(ev){ ev.preventDefault(); ev.stopPropagation(); }
        if(!productId){ toast('Produit introuvable pour la localisation.', 'warning'); return; }
        try{
            toast('Recherche du plan de localisation…', 'info');
            const payload = await Api.planProductLocation(productId);
            const matches = Array.isArray(payload?.matches) ? payload.matches : [];
            const match = payload?.best || matches[0] || null;
            if(!match || !match.plan || !match.object){
                this._showProductPlanUnavailable(payload, productId);
                return;
            }
            const objects = await Api.planObjects(match.plan.id);
            this._showProductPlanLocationModal(payload, match, Array.isArray(objects) ? objects : []);
        }catch(e){
            console.error(e);
            toast(e.message || 'Impossible de localiser ce produit sur un plan.', 'error');
        }
    },

    _showProductPlanUnavailable(payload, productId){
        const product = payload?.product || this.items.find(p => String(p.id) === String(productId)) || {};
        const unplaced = Array.isArray(payload?.unplacedLocations) ? payload.unplacedLocations : [];
        const rows = unplaced.length
            ? unplaced.map(loc => `<li><strong>${this._esc(loc.code || loc.label || 'Emplacement')}</strong><span>${this._esc(loc.zone || loc.warehouseName || '')}</span></li>`).join('')
            : '<li><strong>Aucun emplacement placé</strong><span>Le produit n’est relié à aucun objet de plan consultable.</span></li>';
        Modal.show('Localisation indisponible', `<div class="product-plan-empty" data-modal-no-autofocus="1">
            <h3>${this._esc(product.name || 'Produit')}</h3>
            <p>Ce produit existe dans MageStock, mais son emplacement n’est pas encore placé sur un plan d’emplacements.</p>
            <ul>${rows}</ul>
            <div class="product-plan-empty-note">Un administrateur doit ouvrir le module Plans, placer l’emplacement concerné, puis le rattacher à l’objet dessiné.</div>
        </div>`, [
            {label:'Ouvrir les plans', cls:'btn-primary', onclick:()=>{ window.location.href = Api.url('plans'); }},
            {label:'Fermer', cls:'btn-secondary', onclick:()=>Modal.close()}
        ], 'warning');
    },

    _showProductPlanLocationModal(payload, match, objects){
        const product = payload?.product || {};
        const plan = match?.plan || {};
        const obj = match?.object || {};
        const location = match?.location || null;
        const stock = match?.stock || {};
        const qty = Number(stock.quantity || 0);
        const warehouseName = plan.warehouseName || location?.warehouseName || 'Magasin non renseigné';
        const detailParts = (Array.isArray(stock.rows) ? stock.rows : [])
            .map(r => [r.picking, r.location, r.rack, r.shelf].filter(Boolean).join(' · '))
            .filter(Boolean);
        const stockDetail = detailParts.length ? detailParts[0] : (location?.zone || 'Aucun détail complémentaire');
        const stockRows = Array.isArray(stock.rows) ? stock.rows : [];
        const terrainSteps = [warehouseName, location?.zone, location?.code || location?.label || obj.label, stockRows[0]?.rack, stockRows[0]?.shelf, stockRows[0]?.picking]
            .filter(Boolean)
            .map(v => this._esc(v));
        const svg = this._productPlanLocationSvg(plan, objects, obj.id, { variant:'overview', label:'Produit ici' });
        const focusSvg = this._productPlanLocationSvg(plan, objects, obj.id, { variant:'focus', focusObject: obj, label:'Zone exacte' });
        const matches = Array.isArray(payload?.matches) ? payload.matches : [];
        const alternatives = matches.filter(m => m?.plan && m?.object && String(m.object.id || '') !== String(obj.id || '')).slice(0,3);
        const alternativesHtml = alternatives.length ? `<section class="product-plan-location-alt">
            <span>Autres emplacements trouvés</span>
            ${alternatives.map(m => `<button type="button" onclick="window.location.href='${this._esc(this._planLocationUrl(product.id || payload?.product?.id || '', { planId:m.plan?.id || '', objectId:m.object?.id || '', locationId:m.location?.id || '' }))}'"><strong>${this._esc(m.plan?.name || 'Plan')}</strong><em>${this._esc(m.location?.code || m.object?.label || 'Emplacement')}</em></button>`).join('')}
        </section>` : '';
        const infoHtml = `<div class="product-plan-location-info">
            <section class="product-plan-location-route">
                <span>Chemin terrain</span>
                <strong>${terrainSteps.length ? terrainSteps.join(' › ') : 'Emplacement à contrôler'}</strong>
                <em>Repère rapide pour retrouver physiquement le produit</em>
            </section>
            <section>
                <span>Produit</span>
                <strong>${this._esc(product.name || 'Produit')}</strong>
                ${product.sku ? `<em>${this._esc(product.sku)}</em>` : ''}
            </section>
            <section>
                <span>Magasin</span>
                <strong>${this._esc(warehouseName)}</strong>
                <em>${this._esc(plan.type || 'Plan d’emplacements')}</em>
            </section>
            <section>
                <span>Plan</span>
                <strong>${this._esc(plan.name || 'Plan')}</strong>
                <em>Plan fini en lecture seule</em>
            </section>
            <section>
                <span>Emplacement</span>
                <strong>${this._esc(location?.code || location?.label || obj.label || 'Emplacement')}</strong>
                <em>${this._esc(location?.zone || 'Zone non renseignée')}</em>
            </section>
            <section>
                <span>Quantité ici</span>
                <strong>${Number.isFinite(qty) ? qty : 0} u</strong>
                <em>${match.matchType === 'product' ? 'Lien direct produit' : 'Emplacement du stock'}</em>
            </section>
            <section>
                <span>Détail terrain</span>
                <strong>${this._esc(stockDetail)}</strong>
                <em>Rack / rayon / picking si renseigné</em>
            </section>
            ${alternativesHtml}
        </div>`;
        const html = `<div class="product-plan-location product-plan-location--advanced" id="productPlanLocationPrint" data-modal-no-autofocus="1">
            <div class="product-plan-location-head">
                <div>
                    <span>Localisation produit</span>
                    <h3>${this._esc(product.name || 'Produit')}</h3>
                    <p>Plan fini en consultation. L’emplacement est surligné et une vue rapprochée aide à se repérer sans afficher les outils d’édition.</p>
                </div>
                <strong>${this._esc(warehouseName)} · ${this._esc(location?.code || obj.label || 'Emplacement')}</strong>
            </div>
            <div class="product-plan-location-grid">
                <div class="product-plan-location-mapset">
                    <div class="product-plan-location-canvas" aria-label="Plan complet avec emplacement surligné">
                        <div class="product-plan-location-canvas-title"><strong>Plan complet</strong><span>Vue d’ensemble</span></div>
                        ${svg}
                    </div>
                    <div class="product-plan-location-focus" aria-label="Vue rapprochée de l’emplacement surligné">
                        <div><strong>Zoom de localisation</strong><span>${this._esc(location?.code || obj.label || 'Emplacement ciblé')}</span></div>
                        ${focusSvg}
                    </div>
                </div>
                ${infoHtml}
            </div>
        </div>`;
        const cleanup = () => document.querySelector('#modalOverlay .modal')?.classList.remove('product-plan-location-modal-shell');
        document.querySelector('#modalOverlay .modal')?.classList.remove('product-plan-location-modal-shell');
        Modal.show({
            title:'Localiser sur le plan',
            sub:'Plan fini, impression terrain et surbrillance avancée',
            html,
            icon:'info',
            buttons:[
                {label:'Imprimer la localisation', cls:'btn-secondary', onclick:()=>this._printProductPlanLocation()},
                {label:'Voir tout le plan', cls:'btn-secondary', onclick:()=>{ window.location.href = this._planLocationUrl(product.id || payload?.product?.id || '', { planId:plan.id, objectId:obj.id, locationId:location?.id || '' }); }},
                {label:'Fermer', cls:'btn-primary', onclick:()=>{ cleanup(); Modal.close(); }}
            ]
        });
        const modal = document.querySelector('#modalOverlay .modal');
        modal?.classList.add('product-plan-location-modal-shell');
        document.querySelector('#modalOverlay .modal-close')?.addEventListener('click', cleanup, {once:true});
    },

    _productPlanLocationSvg(plan, objects, highlightId, options={}){
        const rawW = Number(plan?.width || 1600);
        const rawH = Number(plan?.height || 1000);
        const planW = Math.max(1, rawW);
        const planH = Math.max(1, rawH);
        const focusObject = options?.focusObject || null;
        const isFocus = options?.variant === 'focus' && focusObject;
        const fw = Math.max(20, Number(focusObject?.width || 120));
        const fh = Math.max(20, Number(focusObject?.height || 80));
        const pad = Math.max(80, Math.min(260, Math.max(fw, fh) * 1.45));
        const vbX = isFocus ? Math.max(-20, Number(focusObject.x || 0) - pad) : Math.min(-28, -Math.max(16, planW * .04));
        const vbY = isFocus ? Math.max(-20, Number(focusObject.y || 0) - pad) : Math.min(-28, -Math.max(16, planH * .04));
        const vbW = isFocus ? Math.min(planW - vbX + 20, Math.max(280, fw + pad * 2)) : planW + Math.abs(vbX) + Math.max(28, planW * .04);
        const vbH = isFocus ? Math.min(planH - vbY + 20, Math.max(220, fh + pad * 2)) : planH + Math.abs(vbY) + Math.max(28, planH * .04);
        const uid = 'ppl' + Math.random().toString(36).slice(2,8);
        const major = this._productPlanGridMajor(planW, planH);
        const minor = Math.max(10, major / 5);
        const rows = (Array.isArray(objects) ? objects : [])
            .slice()
            .sort((a,b)=>this._productPlanRenderRank(a)-this._productPlanRenderRank(b))
            .map(o => this._productPlanObjectSvg(o, String(o.id || '') === String(highlightId || ''), options))
            .join('');
        const labels = this._productPlanRulers(planW, planH, major);
        return `<svg class="product-plan-location-svg ${isFocus ? 'product-plan-location-svg--focus' : 'product-plan-location-svg--overview'}" viewBox="${vbX} ${vbY} ${vbW} ${vbH}" role="img" aria-label="Plan ${this._esc(plan?.name || '')}">
            <defs>
                <pattern id="${uid}Grid" width="${minor}" height="${minor}" patternUnits="userSpaceOnUse"><path d="M${minor} 0L0 0 0 ${minor}" fill="none" stroke="#e6edf7" stroke-width="1" vector-effect="non-scaling-stroke"/></pattern>
                <pattern id="${uid}GridMajor" width="${major}" height="${major}" patternUnits="userSpaceOnUse"><path d="M${major} 0L0 0 0 ${major}" fill="none" stroke="#cbdaf0" stroke-width="1.25" vector-effect="non-scaling-stroke"/></pattern>
            </defs>
            <rect class="ppl-shell" x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}"></rect>
            <rect class="ppl-stage" x="0" y="0" width="${planW}" height="${planH}" rx="0"></rect>
            <rect class="ppl-grid" x="0" y="0" width="${planW}" height="${planH}" rx="0" fill="url(#${uid}Grid)"></rect>
            <rect class="ppl-grid-major" x="0" y="0" width="${planW}" height="${planH}" rx="0" fill="url(#${uid}GridMajor)"></rect>
            ${isFocus ? '' : labels}
            ${rows}
        </svg>`;
    },

    _productPlanGridMajor(w, h){
        const max = Math.max(Number(w||0), Number(h||0));
        if(max <= 500) return 100;
        if(max <= 2500) return 200;
        if(max <= 6000) return 500;
        return 1000;
    },

    _productPlanRulers(w, h, step){
        const out = [];
        const fmt = (u) => {
            const m = Number(u || 0) / 100;
            if(Math.abs(m - Math.round(m)) < .001) return `${Math.round(m)} m`;
            return `${Locale.formatNumber(m, {maximumFractionDigits:2})} m`;
        };
        for(let x=step; x<=w+.01; x+=step){ out.push(`<line class="ppl-ruler-tick" x1="${x}" y1="-14" x2="${x}" y2="0"></line><text class="ppl-ruler-text" x="${x}" y="-22" text-anchor="middle">${this._esc(fmt(x))}</text>`); }
        for(let y=step; y<=h+.01; y+=step){ out.push(`<line class="ppl-ruler-tick" x1="-14" y1="${y}" x2="0" y2="${y}"></line><text class="ppl-ruler-text" x="-22" y="${y+4}" text-anchor="end">${this._esc(fmt(y))}</text>`); }
        return `<g class="ppl-rulers">${out.join('')}</g>`;
    },

    _productPlanRenderRank(o){
        const t = String(o?.type || '');
        if(['mur','cloison','cloison_grille'].includes(t)) return 1;
        if(['depot','local','piece','atelier','zone'].includes(t)) return 2;
        if(['ouverture','porte','porte_double','fenetre'].includes(t)) return 6;
        if(['rack','rayonnage','etagere','armoire','armoire_double','palette','emplacement'].includes(t)) return 8;
        return 5;
    },

    _productPlanObjectSvg(o, highlighted=false, options={}){
        if(!o) return '';
        if(this._productPlanIsWallSegment(o)) return this._productPlanWallSvg(o, highlighted);
        if(this._productPlanIsOpening(o)) return this._productPlanOpeningSvg(o, highlighted, options);
        return this._productPlanBoxObjectSvg(o, highlighted, options);
    },

    _productPlanIsWallSegment(o){
        const t = String(o?.type || '');
        return ['mur','cloison','cloison_grille'].includes(t) && Array.isArray(o?.metadata?.points) && o.metadata.points.length >= 2;
    },

    _productPlanIsOpening(o){ return ['ouverture','porte','porte_double','fenetre'].includes(String(o?.type || '')); },

    _productPlanWallSvg(o, highlighted=false){
        const pts = o.metadata.points;
        const p1 = {x:Number(pts[0]?.x||0), y:Number(pts[0]?.y||0)};
        const p2 = {x:Number(pts[1]?.x||0), y:Number(pts[1]?.y||0)};
        const t = String(o.type || 'mur');
        const thickness = Math.max(t === 'mur' ? 14 : 8, Number(o.metadata?.thickness || (t === 'mur' ? 22 : 12)));
        if(t === 'cloison_grille'){
            const len = Math.hypot(p2.x-p1.x, p2.y-p1.y) || 1;
            const ux=(p2.x-p1.x)/len, uy=(p2.y-p1.y)/len;
            const nx=-uy, ny=ux;
            const bars=[];
            const step = Math.max(28, Math.min(46, len/10));
            for(let d=step; d<len-step/2; d+=step){
                const cx=p1.x+ux*d, cy=p1.y+uy*d;
                bars.push(`<line class="ppl-wall-gridbar" x1="${cx-nx*7}" y1="${cy-ny*7}" x2="${cx+nx*7}" y2="${cy+ny*7}"></line>`);
            }
            return `<g class="ppl-wall ppl-wall--grid${highlighted?' is-highlighted':''}"><line class="ppl-wall-gridline" x1="${p1.x}" y1="${p1.y}" x2="${p2.x}" y2="${p2.y}"></line>${bars.join('')}</g>`;
        }
        const cls = t === 'mur' ? 'ppl-wall--solid' : 'ppl-wall--partition';
        return `<g class="ppl-wall ${cls}${highlighted?' is-highlighted':''}"><line class="ppl-wall-main" x1="${p1.x}" y1="${p1.y}" x2="${p2.x}" y2="${p2.y}" stroke-width="${thickness}"></line><line class="ppl-wall-axis" x1="${p1.x}" y1="${p1.y}" x2="${p2.x}" y2="${p2.y}"></line></g>`;
    },

    _productPlanOpeningSvg(o, highlighted=false, options={}){
        const x = Number(o.x || 0), y = Number(o.y || 0), w = Math.max(16, Number(o.width || 120)), h = Math.max(14, Number(o.height || 60));
        const type = String(o.type || 'ouverture');
        const rot = Number(o.rotation || 0);
        const cx = x + w/2, cy = y + h/2;
        const baseY = y + h/2;
        const left = x + 8, right = x + w - 8;
        const jamb = `<line class="ppl-opening-jamb" x1="${left}" y1="${baseY-16}" x2="${left}" y2="${baseY+16}"></line><line class="ppl-opening-jamb" x1="${right}" y1="${baseY-16}" x2="${right}" y2="${baseY+16}"></line>`;
        let shape = '';
        if(type === 'fenetre'){
            shape = `${jamb}<line class="ppl-window-line" x1="${left+6}" y1="${baseY-6}" x2="${right-6}" y2="${baseY-6}"></line><line class="ppl-window-line" x1="${left+6}" y1="${baseY}" x2="${right-6}" y2="${baseY}"></line><line class="ppl-window-line" x1="${left+6}" y1="${baseY+6}" x2="${right-6}" y2="${baseY+6}"></line><line class="ppl-window-mullion" x1="${cx}" y1="${baseY-9}" x2="${cx}" y2="${baseY+9}"></line>`;
        }else if(type === 'porte_double'){
            const mid = cx;
            shape = `${jamb}<line class="ppl-door-leaf" x1="${left}" y1="${baseY}" x2="${mid}" y2="${y+10}"></line><line class="ppl-door-leaf" x1="${right}" y1="${baseY}" x2="${mid}" y2="${y+10}"></line><path class="ppl-door-arc" d="M ${left} ${baseY} A ${w*.46} ${h*.88} 0 0 1 ${mid} ${y+10}"></path><path class="ppl-door-arc" d="M ${right} ${baseY} A ${w*.46} ${h*.88} 0 0 0 ${mid} ${y+10}"></path>`;
        }else if(type === 'porte'){
            const endX = right - 2, endY = y + 12;
            shape = `${jamb}<line class="ppl-door-leaf" x1="${left}" y1="${baseY}" x2="${endX}" y2="${endY}"></line><path class="ppl-door-arc" d="M ${left} ${baseY} A ${w*.84} ${h*1.55} 0 0 1 ${endX} ${endY}"></path>`;
        }else{
            shape = `${jamb}<line class="ppl-passage-line" x1="${left+8}" y1="${baseY}" x2="${right-8}" y2="${baseY}"></line>`;
        }
        const hi = highlighted ? `<rect class="ppl-highlight" x="${x-8}" y="${y-8}" width="${w+16}" height="${h+16}" rx="4"></rect>` : '';
        const pin = highlighted ? this._productPlanPin(cx, Math.max(18, y-34)) : '';
        return `<g class="ppl-object ppl-opening ppl-opening--${this._esc(type)}${highlighted?' is-highlighted':''}" transform="rotate(${rot} ${cx} ${cy})">${hi}<rect class="ppl-opening-hit" x="${x}" y="${y}" width="${w}" height="${h}" rx="2"></rect>${shape}${pin}</g>`;
    },

    _productPlanBoxObjectSvg(o, highlighted=false, options={}){
        const x = Number(o.x || 0), y = Number(o.y || 0), w = Math.max(10, Number(o.width || 120)), h = Math.max(10, Number(o.height || 70));
        const type = String(o.type || 'emplacement');
        const rot = Number(o.rotation || 0);
        const cx=x+w/2, cy=y+h/2;
        const fill = o.color || this._productPlanTypeColor(type);
        const hi = highlighted ? `<rect class="ppl-highlight-pulse" x="${x-18}" y="${y-18}" width="${w+36}" height="${h+36}" rx="8"></rect><rect class="ppl-highlight" x="${x-8}" y="${y-8}" width="${w+16}" height="${h+16}" rx="5"></rect>` : '';
        const deco = this._productPlanObjectDecor(type, x, y, w, h);
        const label = this._esc(this._shortPlanLabel(o.label || this._productPlanTypeLabel(type), Math.max(8, Math.min(18, Math.floor(w / 16)))));
        const showLabel = highlighted || (w >= 120 && h >= 42 && !['palette'].includes(type));
        const text = showLabel ? `<text class="ppl-object-text" x="${x+10}" y="${y+Math.min(22,Math.max(14,h*.25))}">${label}</text>` : '';
        const pin = highlighted ? this._productPlanPin(cx, Math.max(18, y-42)) : '';
        return `<g class="ppl-object ppl-object--${this._esc(type)}${highlighted?' is-highlighted':''}" transform="rotate(${rot} ${cx} ${cy})">${hi}<rect class="ppl-object-main" x="${x}" y="${y}" width="${w}" height="${h}" rx="3" fill="${this._esc(fill)}"></rect>${deco}${text}${pin}</g>`;
    },

    _productPlanPin(x, y){
        return `<g class="ppl-pin" transform="translate(${x} ${y})"><path d="M0 0c-7 0-12 5.6-12 12.5 0 9.2 12 22.5 12 22.5s12-13.3 12-22.5C12 5.6 7 0 0 0z"></path><circle cx="0" cy="12" r="4.1"></circle></g>`;
    },

    _productPlanObjectDecor(type, x, y, w, h){
        const d = '#334155', m = 'rgba(51,65,85,.42)', light = 'rgba(255,255,255,.58)';
        const L = (x1,y1,x2,y2,sw=1,st=d,dash='') => `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${st}" stroke-width="${sw}" ${dash ? `stroke-dasharray="${dash}"` : ''} vector-effect="non-scaling-stroke"></line>`;
        const R = (rx,ry,rw,rh,fill='none',st=m,sw=1,dash='') => `<rect x="${rx}" y="${ry}" width="${Math.max(0,rw)}" height="${Math.max(0,rh)}" rx="2" fill="${fill}" stroke="${st}" stroke-width="${sw}" ${dash ? `stroke-dasharray="${dash}"` : ''} vector-effect="non-scaling-stroke"></rect>`;
        const bx = x + Math.max(4, Math.min(10, w*.04)), by = y + Math.max(4, Math.min(9, h*.06));
        const bw = Math.max(18, w - (bx-x)*2), bh = Math.max(14, h - (by-y)*2);
        if(type === 'rack'){
            let out = R(bx,by,bw,bh,'rgba(248,250,252,.55)','rgba(15,23,42,.22)',1);
            out += R(bx,by+3,bw,7,'rgba(31,41,55,.86)','rgba(15,23,42,.28)',.8) + R(bx,by+bh-10,bw,7,'rgba(31,41,55,.86)','rgba(15,23,42,.28)',.8);
            const bays = Math.max(2, Math.min(6, Math.round(bw/70)));
            for(let i=0;i<=bays;i++){ const xx=bx+bw*i/bays; out += R(xx-2,by+1,4,bh-2,'rgba(71,85,105,.70)','rgba(15,23,42,.20)',.7); }
            return out;
        }
        if(['rayonnage','etagere'].includes(type)){
            const bays = Math.max(2, Math.min(5, Math.round(bw/55))), levels = Math.max(2, Math.min(5, Math.round(bh/18)));
            let out = R(bx,by,bw,bh,'rgba(240,249,255,.50)','rgba(71,85,105,.45)',1);
            for(let i=1;i<bays;i++){ const xx=bx+bw*i/bays; out += L(xx,by+3,xx,by+bh-3,.9,m); }
            for(let j=1;j<levels;j++){ const yy=by+bh*j/levels; out += L(bx+3,yy,bx+bw-3,yy,.9,m); }
            return out;
        }
        if(['armoire','armoire_double'].includes(type)){
            let out = R(bx,by,bw,bh,'rgba(255,255,255,.20)',light,1.1);
            out += L(bx+6, by+8, bx+bw-6, by+8, .8, m);
            if(type==='armoire_double') out += L(x+w/2, by+3, x+w/2, by+bh-3, 1.1, d) + `<circle cx="${x+w/2-6}" cy="${y+h/2}" r="2" fill="${d}"></circle><circle cx="${x+w/2+6}" cy="${y+h/2}" r="2" fill="${d}"></circle>`;
            else out += `<circle cx="${x+w-16}" cy="${y+h/2}" r="2.2" fill="${d}"></circle>`;
            return out;
        }
        if(type === 'palette') return `${L(x+10,y+14,x+w-10,y+14,1.2,d)}${L(x+10,y+h/2,x+w-10,y+h/2,1.2,d)}${L(x+10,y+h-14,x+w-10,y+h-14,1.2,d)}${L(x+w*.33,y+8,x+w*.33,y+h-8,1.1,d)}${L(x+w*.66,y+8,x+w*.66,y+h-8,1.1,d)}`;
        if(['depot','local','piece','atelier','zone'].includes(type)) return R(x+6,y+6,w-12,h-12,'none',m,1.1,'9 7');
        if(['allee','circulation'].includes(type)) return `${L(x+16,y+h/2,x+w-24,y+h/2,1.6,d,'4 8')}<path d="M ${x+w-35} ${y+h/2-7} L ${x+w-22} ${y+h/2} L ${x+w-35} ${y+h/2+7}" fill="none" stroke="${d}" stroke-width="1.6" vector-effect="non-scaling-stroke"></path>`;
        return R(x+7,y+7,w-14,h-14,'none',light,1);
    },

    _productPlanTypeColor(type){
        return ({depot:'#dbeafe',local:'#dbeafe',piece:'#ede9fe',atelier:'#fef3c7',zone:'#dcfce7',allee:'#f8fafc',circulation:'#f8fafc',ouverture:'#fffdf7',porte:'#fffdf7',porte_double:'#fffdf7',fenetre:'#f8fafc',armoire:'#e2e8f0',armoire_double:'#dbe4f0',rack:'#cbd5e1',rayonnage:'#e0f2fe',etagere:'#e0f2fe',palette:'#fed7aa',emplacement:'#dcfce7'})[type] || '#dbeafe';
    },

    _productPlanTypeLabel(type){
        return ({depot:'Dépôt',local:'Local',piece:'Pièce',atelier:'Atelier',zone:'Zone',allee:'Allée',circulation:'Circulation',ouverture:'Ouverture',porte:'Porte',porte_double:'Porte double',fenetre:'Fenêtre',armoire:'Armoire',armoire_double:'Armoire double',rack:'Rack',rayonnage:'Rayonnage',etagere:'Étagère',palette:'Zone palette',emplacement:'Emplacement'})[type] || 'Objet';
    },

    _shortPlanLabel(value, max=24){
        const clean = String(value ?? '').replace(/\s+/g, ' ').trim();
        if(clean.length <= max) return clean;
        return clean.slice(0, Math.max(4, max - 1)).trimEnd() + '…';
    },

    _printProductPlanLocation(){
        const source = document.getElementById('productPlanLocationPrint');
        if(!source){ window.print(); return; }
        const title = source.querySelector('.product-plan-location-head h3')?.textContent?.trim() || 'Produit';
        const context = source.querySelector('.product-plan-location-head > strong')?.textContent?.trim() || 'Localisation produit';
        const printSource = source.cloneNode(true);
        printSource.id = 'productPlanLocationPrintSheet';
        printSource.classList.add('is-print-sheet');
        // La fiche papier doit tenir sur une page : on garde le plan complet et le bloc infos,
        // la vue rapprochée écran est retirée pour éviter les 2-3 pages et les blancs.
        printSource.querySelector('.product-plan-location-focus')?.remove();
        printSource.querySelectorAll('.product-plan-location-alt').forEach(el => el.remove());
        const infoSections = Array.from(printSource.querySelectorAll('.product-plan-location-info section'));
        infoSections.slice(7).forEach(el => el.remove());
        const bodyHtml = `<article class="ms-product-plan-print"><header class="ms-product-plan-print-kicker"><div><span>Fiche terrain · Localisation produit</span><strong>${this._esc(title)}</strong></div><em>${this._esc(context)}</em></header>${printSource.outerHTML}<footer class="ms-product-plan-print-note"><strong>Lecture terrain :</strong> rejoindre l’emplacement encadré sur le plan. Document imprimé sans les outils d’édition.</footer></article>`;
        const w = window.open('', '_blank', 'noopener,noreferrer,width=1200,height=800');
        if(!w){ toast('Autorisez les fenêtres popup pour imprimer la localisation.', 'warning'); return; }
        w.document.open('text/html', 'replace');
        w.document.write(`<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Localisation produit — MageStock</title><style>${this._productPlanPrintCss()}</style></head><body>${bodyHtml}<script>window.onload=()=>{setTimeout(()=>{window.focus();window.print();},220)}<\/script></body></html>`);
        w.document.close();
    },

    _productPlanPrintCss(){
        return `:root{color-scheme:light}@page{size:A4 landscape;margin:6mm}*{box-sizing:border-box}html,body{margin:0;padding:0;background:#fff!important;color:#0f172a!important;font-family:"DM Sans",Inter,Arial,sans-serif;-webkit-print-color-adjust:exact;print-color-adjust:exact}.ms-product-plan-print{width:285mm;min-height:198mm;max-height:198mm;margin:0 auto;padding:0;background:#fff;overflow:hidden;color:#0f172a}.ms-product-plan-print-kicker{display:flex;align-items:flex-end;justify-content:space-between;gap:10mm;border:1px solid #dbe3ee;border-left:4px solid #1d4ed8;background:#f8fafc;padding:3mm 4mm;margin:0 0 3mm;break-inside:avoid}.ms-product-plan-print-kicker span{display:block;text-transform:uppercase;letter-spacing:.08em;font-size:7.4px;color:#2563eb;font-weight:1000}.ms-product-plan-print-kicker strong{display:block;margin-top:.8mm;font-size:13px;line-height:1.05;letter-spacing:-.02em;color:#0f172a;max-width:185mm;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ms-product-plan-print-kicker em{font-style:normal;font-size:8.4px;color:#1e3a8a;font-weight:900;max-width:78mm;text-align:right}.ms-product-plan-print-note{border:1px solid #fed7aa;border-left:4px solid #f59e0b;background:#fff7ed;color:#92400e;padding:2.5mm 3mm;margin-top:3mm;font-size:8px;line-height:1.25;break-inside:avoid}.product-plan-location{font-family:"DM Sans",Inter,Arial,sans-serif;color:#0f172a;background:#fff}.product-plan-location-head{display:none!important}.product-plan-location-grid{display:grid!important;grid-template-columns:minmax(0,1fr) 62mm;gap:5mm;align-items:start}.product-plan-location-mapset{display:block!important;min-width:0}.product-plan-location-canvas{border:1px solid #cbd5e1;padding:3mm;background:#f8fafc;break-inside:avoid;min-width:0}.product-plan-location-canvas-title{display:flex;justify-content:space-between;gap:8px;margin-bottom:2mm;font-size:7.6px;color:#64748b;font-weight:900}.product-plan-location-canvas-title strong{color:#0f172a;font-size:9px}.product-plan-location-focus,.product-plan-location-alt{display:none!important}.product-plan-location-svg{display:block;width:100%;height:auto!important;max-height:154mm!important;background:#fff;border:1px solid #cbd5e1}.product-plan-location-svg--focus{display:none!important}.ppl-shell{fill:#f8fafc}.ppl-stage{fill:#fffefa;stroke:#0f172a;stroke-width:2.2}.ppl-grid{opacity:.55}.ppl-grid-major{opacity:.72}.ppl-ruler line,.ppl-ruler-tick{stroke:#94a3b8;stroke-width:.9;vector-effect:non-scaling-stroke}.ppl-ruler text,.ppl-ruler-text{font-size:8.5px;font-weight:850;fill:#334155}.ppl-object-main{stroke:rgba(15,23,42,.42);stroke-width:1.15}.ppl-object-band{fill:rgba(255,255,255,.20);stroke:rgba(15,23,42,.18)}.ppl-object-text{font-size:6.8px;font-weight:650;fill:#0f172a}.ppl-opening-hit{fill:transparent!important;stroke:none!important}.ppl-highlight{fill:rgba(245,158,11,.09);stroke:#f59e0b;stroke-width:2.2;vector-effect:non-scaling-stroke}.ppl-highlight-pulse{display:none!important}.ppl-callout{display:none!important}.ppl-pin path{fill:#ef4444}.ppl-pin circle{fill:#fff}.product-plan-location-info{display:grid;gap:2mm}.product-plan-location-info section{border:1px solid #dbe3ee;border-left:3px solid #cbd5e1;border-radius:0;padding:2.2mm 2.4mm;background:#fff;break-inside:avoid}.product-plan-location-info section:first-child{border-color:#fed7aa;border-left-color:#f59e0b;background:#fff7ed}.product-plan-location-info span{display:block;font-size:6.7px;text-transform:uppercase;letter-spacing:.06em;color:#64748b;font-weight:1000}.product-plan-location-info strong{display:block;font-size:8.2px;line-height:1.18;margin-top:.6mm;color:#0f172a;word-break:break-word}.product-plan-location-info em{display:block;font-size:6.8px;color:#64748b;font-style:normal;margin-top:.6mm;line-height:1.15}@media print{html,body{width:297mm;height:210mm;overflow:hidden!important;background:#fff!important}.ms-product-plan-print{width:285mm!important;max-height:198mm!important;overflow:hidden!important;page-break-after:avoid!important}.product-plan-location-svg{max-height:154mm!important}.product-plan-location-canvas,.product-plan-location-info section,.ms-product-plan-print-kicker{box-shadow:none!important}body>*:not(.ms-product-plan-print){display:none!important}}@media(max-width:760px){.ms-product-plan-print{width:auto;max-height:none;padding:10px}.product-plan-location-grid{display:block!important}.product-plan-location-info{margin-top:4mm}.product-plan-location-svg{max-height:none!important}}`;
    },

    _planButtonHtml(productId, compact=false){
        const label = compact ? 'Plan' : 'Localiser';
        const cls = compact ? 'btn btn-sm btn-ghost prod-plan-btn' : 'prod-plan-card-btn';
        const icon = '<svg class="prod-plan-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.5c-3.04 0-5.5 2.34-5.5 5.23 0 3.92 5.5 11.77 5.5 11.77s5.5-7.85 5.5-11.77c0-2.89-2.46-5.23-5.5-5.23Zm0 7.35a2.1 2.1 0 1 1 0-4.2 2.1 2.1 0 0 1 0 4.2Z"/></svg>';
        return `<button type="button" class="${cls}" data-no-image-zoom="true" onclick="ProdPage.openProductPlan('${this._esc(productId)}', event)" title="Localiser l’emplacement sur un plan" aria-label="Localiser l’emplacement sur un plan">${icon}<span>${label}</span></button>`;
    },

    async _populateLocationSelect(warehouseId, selectedLocationId=''){
        const locSelect = document.getElementById('pmLocationId');
        const locHint = document.getElementById('pmLocHint');
        if(!locSelect) return;
        const wid = parseInt(warehouseId || 0, 10);
        if(!wid){
            locSelect.innerHTML = '<option value="">— Choisir un emplacement —</option>';
            if(locHint) locHint.textContent = "Choisissez d’abord un magasin pour filtrer les emplacements.";
            return;
        }
        try {
            const locs = await Api.locations(wid);
            const active = (Array.isArray(locs) ? locs : []).filter(l => l.active !== false);
            if(!active.length){
                locSelect.innerHTML = '<option value="">— Aucun emplacement pour ce magasin —</option>';
                if(locHint) locHint.textContent = 'Aucun emplacement créé pour ce magasin. Gérez-les dans Emplacements.';
                return;
            }
            locSelect.innerHTML = '<option value="">— Aucun emplacement spécifique —</option>' +
                active.map(l => {
                    const code = this._esc(l.code || '');
                    const label = this._esc(l.label || '');
                    const zone = this._esc(l.zone || '');
                    const text = [l.code, l.label, l.zone].filter(Boolean).join(' · ');
                    return `<option value="${this._esc(l.id)}" data-code="${code}" data-label="${label}" data-zone="${zone}">${this._esc(text)}</option>`;
                }).join('');
            if(selectedLocationId) locSelect.value = String(selectedLocationId);
            if(locHint) locHint.textContent = active.length + ' emplacement(s) disponible(s) pour ce magasin.';
        } catch(e) {
            locSelect.innerHTML = '<option value="">— Erreur chargement —</option>';
            if(locHint) locHint.textContent = 'Impossible de charger les emplacements du magasin.';
        }
    },

    _applyStockLocationToForm(row){
        const set = (id, value) => { const el = document.getElementById(id); if(el) el.value = value || ''; };
        set('pmPicking', row?.picking || '');
        set('pmLocation', row?.location || '');
        set('pmRack', row?.rack || '');
        set('pmShelfLoc', row?.shelf || '');
        const locSelect = document.getElementById('pmLocationId');
        if(locSelect && row?.locationId) locSelect.value = String(row.locationId);
    },

    async _getStockRowForWarehouse(productId, warehouseId){
        if(!productId || !warehouseId) return null;
        try {
            const st = await Api.stock(productId, warehouseId);
            const rows = Array.isArray(st) ? st : (Array.isArray(st?.stock) ? st.stock : []);
            return rows.find(r => String(r.productId) === String(productId) && String(r.warehouseId) === String(warehouseId)) || rows[0] || null;
        } catch(e){
            console.warn('[products] stock location load failed', e);
            return null;
        }
    },
    _getVariantAttributeText(p){
        const attrs = this._parseMaybeJson(p?.variantAttributes, {}) || {};
        const familyId = p?.variantFamily || '';
        const family = (this.variantFamilies || []).find(f => f.id === familyId);
        const defs = family?.attributes || [];
        const parts = [];
        defs.forEach(def => {
            const value = attrs?.[def.key];
            if(value === undefined || value === null || value === '') return;
            parts.push(`${def.label}: ${value}${def.unit ? ' '+def.unit : ''}`);
        });
        if(!parts.length){
            Object.entries(attrs).forEach(([k,v]) => { if(v !== '' && v != null) parts.push(`${k}: ${v}`); });
        }
        return parts.join(' · ');
    },
    _buildVariantLabel(family, attrs){
        attrs = attrs || {};
        const get = (k) => attrs[k] !== undefined && attrs[k] !== null && attrs[k] !== '' ? attrs[k] : '';
        const id = family?.id || '';
        if(id === 'signage_round') return get('diameterMm') ? `Ø${get('diameterMm')} mm` : '';
        if(id === 'signage_triangle') return get('sideMm') ? `Côté ${get('sideMm')} mm` : '';
        if(id === 'signage_rectangle'){
            const w = get('widthMm'), h = get('heightMm');
            if(w && h) return `${w} × ${h} mm`;
            if(w) return `L ${w} mm`;
            if(h) return `H ${h} mm`;
        }
        if(id === 'tube_dimension'){
            const d = get('diameterMm'), l = get('lengthMm'), e = get('thicknessMm');
            return [d ? `Ø${d} mm` : '', l ? `${l} mm` : '', e ? `ép. ${e} mm` : ''].filter(Boolean).join(' · ');
        }
        if(id === 'clothing_size') return get('size') ? `Taille ${get('size')}` : '';
        const first = Object.entries(attrs).find(([,v]) => v !== '' && v != null);
        return first ? `${first[0]} ${first[1]}` : '';
    },
    async _loadVariantFamilies(){
        try {
            this.variantFamilies = await Api.variantFamilies();
        } catch(e){
            console.warn('Familles de déclinaisons indisponibles', e);
            this.variantFamilies = [];
        }
        return this.variantFamilies;
    },
    async init(){
        const [cats, whs, suppliers, variantFamilies] = await Promise.all([Api.categories(), Api.warehouses(), Api.suppliers(), Api.variantFamilies ? Api.variantFamilies() : Promise.resolve([])]);
        this.cats = cats || [];
        this.whs = whs || [];
        this.suppliers = suppliers || [];
        this.variantFamilies = variantFamilies || [];

        // Fill category filter
        const catSel = document.getElementById('prodCatFilter');
        if(catSel){
            const allCategories = (window.StockCascade ? StockCascade.getWarehouseCategories(this.whs, '', this._allProds || [], this.cats || []) : (this.cats || []));
            catSel.innerHTML = '<option value="">Toutes catégories</option>' + allCategories.map(c=>`<option value="${c}">${c}</option>`).join('');
        }

        // Fill warehouse filter (visible)
        const whSel = document.getElementById('prodWhFilter');
        if(whSel){
            whSel.innerHTML = '<option value="">Tous magasins</option>' + this.whs.map(w=>`<option value="${w.id}">${w.name}</option>`).join('');
            whSel.onchange = ()=> this.load();
        }

        // Events
        const searchEl = document.getElementById('prodSearch');
        if(searchEl) searchEl.oninput = ()=>{ this._syncSearchClearButtons(); this.load(); };
        this._initSearchClearButtons();
        
        const catEl = document.getElementById('prodCatFilter');
        if(catEl) catEl.onchange = ()=> this.load();

        const brandEl = document.getElementById('prodBrandFilter');
        if(brandEl) brandEl.onchange = ()=> this.load();
        
        const btn = document.getElementById('btnNewProd');
        if(btn) btn.onclick = ()=> this.openModal();

        // CSV Import
        const btnImport = document.getElementById('btnImportCSV');
        if(btnImport) btnImport.onclick = () => this.openImportModal();

        const btnEntry = document.getElementById('btnStockEntry');
        // Use global movement modal instead of local one
        if(btnEntry) btnEntry.onclick = ()=> window.openMovementModal('entry');

        // View toggle (grid/list)
        this._initViewToggle();
        this._initSmartProductFilters();
        this._initBulkMode();

        // Pager events
        const prevBtn = document.getElementById('prodPrev');
        const nextBtn = document.getElementById('prodNext');
        const sizeSel = document.getElementById('prodPageSize');
        if(sizeSel){
            const v = parseInt(sizeSel.value||'12',10);
            if(v>0) this.pageSize = v;
            sizeSel.onchange = ()=>{ this.pageSize = parseInt(sizeSel.value||'12',10) || 12; this.pageIndex = 1; this.renderPaged(); };
        }
        if(prevBtn) prevBtn.onclick = ()=>{ if(this.pageIndex>1){ this.pageIndex--; this.renderPaged(); } };
        if(nextBtn) nextBtn.onclick = ()=>{ const max=this._getMaxPage(); if(this.pageIndex<max){ this.pageIndex++; this.renderPaged(); } };

        // ══════════════════════════════════════════════════════════════
        // INFINITE SCROLL - Init pour mobile/tablette
        // ══════════════════════════════════════════════════════════════
        this._initInfiniteScroll();
        this._initImageZoom();

        await this.load();

        // ── Tri des colonnes ─────────────────────────────────────
        if(typeof TableSort !== 'undefined'){
            TableSort.init('prodTable',
                () => this._shown || [],
                (sorted) => {
                    this._shown = sorted;
                    this.pageIndex = 1;
                    this.renderPaged();
                },
                {
                    defaultKey: 'name',
                    defaultDir: 'asc',
                    getters: {
                        __stock: p => {
                            const entries = (this.stock||[]).filter(s=>String(s.productId)===String(p.id));
                            return entries.reduce((sum,s)=>sum+(parseInt(s.quantity)||0), 0);
                        },
                        __price: p => getBaseCost(p),
                        __wh: p => {
                            let aw = p.allowedWarehouses;
                            if(typeof aw==='string') try{aw=JSON.parse(aw||'[]')}catch(e){aw=[];}
                            return Array.isArray(aw)&&aw.length
                                ? (this.whs.find(w=>String(w.id)===String(aw[0]))||{}).name||''
                                : '';
                        }
                    }
                }
            );
        }
    },
    
    /**
     * Initialise le scroll infini pour mobile
     */
    _initInfiniteScroll(){
        if(typeof InfiniteScroll === 'undefined') return;
        
        const grid = document.getElementById('prodGrid');
        const pager = document.querySelector('#pgProducts .pager, #pgProducts .pagination-bar');
        
        if(!grid) return;
        
        // Ajouter classe pour cacher le pager sur mobile
        if(pager) pager.classList.add('hide-mobile');
        
        InfiniteScroll.init({
            id: 'products',
            container: document.querySelector('.main-content') || window,
            target: grid,
            pagerEl: pager,
            pageSize: this.pageSize || 12,
            threshold: 300,
            loadMore: async (page, size) => {
                // Calculer les items à retourner
                const start = (page - 1) * size;
                const end = start + size;
                const items = (this._shown || []).slice(start, end);
                
                return {
                    items: items,
                    hasMore: end < (this._shown || []).length
                };
            },
            renderItems: (items, append) => {
                this._renderGridItems(items, this._currentWhFilter || '', append);
            }
        });
    },
    
    // View toggle: grid vs list
    _initViewToggle(){
        const btnGrid = document.getElementById('viewGrid');
        const btnList = document.getElementById('viewList');
        const grid = document.getElementById('prodGrid');
        const tableWrap = document.getElementById('prodTableWrap');
        
        // Restore from localStorage
        const savedView = localStorage.getItem('sm_products_view') || 'grid';
        this._currentView = savedView;
        this._applyView(savedView);
        
        if(btnGrid){
            btnGrid.onclick = () => {
                this._currentView = 'grid';
                localStorage.setItem('sm_products_view', 'grid');
                this._applyView('grid');
            };
        }
        if(btnList){
            btnList.onclick = () => {
                this._currentView = 'list';
                localStorage.setItem('sm_products_view', 'list');
                this._applyView('list');
            };
        }
    },
    
    _applyView(view){
        const btnGrid = document.getElementById('viewGrid');
        const btnList = document.getElementById('viewList');
        const grid = document.getElementById('prodGrid');
        const tableWrap = document.getElementById('prodTableWrap');

        if(btnGrid) btnGrid.classList.toggle('active', view === 'grid');
        if(btnList) btnList.classList.toggle('active', view === 'list');

        if(view === 'list'){
            if(grid) grid.style.display = 'none';
            if(tableWrap) tableWrap.style.display = 'block';
            this.renderPaged();
        } else {
            if(grid) grid.style.display = '';
            if(tableWrap) tableWrap.style.display = 'none';
            this.renderPaged();
        }
        // Sync visibilité du bouton Sélection (visible seulement en vue liste)
        this._syncBulkToggleVisibility();
    },

    // Create an initial stock movement if the user provided an initial quantity at creation.
    // SAFE: no-op when qty is empty/0 or productId missing.
    async _maybeInitStock(productId, initQty, baseUnit, initLot, initExp){
        const pid = parseInt(productId || 0);
        const qty = parseFloat(initQty || 0);
        if(!pid || !qty || qty <= 0) return;

        // Warehouse: use selected warehouse from the form
        const whSelect = document.getElementById('pmWarehouse');
        const wid = parseInt(whSelect?.value) || 0;
        
        if(!wid){
            toast('Stock initial ignoré : aucun magasin sélectionné.', 'warning');
            return;
        }

        // Enforce lot/expiry if required (the backend will also enforce).
        const needsLot = !!document.getElementById('pmTrackLot')?.checked;
        const needsExp = !!document.getElementById('pmTrackExp')?.checked;
        const lot = (initLot || '').trim();
        const exp = (initExp || '').trim();
        if(needsLot && !lot){
            toast('Stock initial ignoré : numéro de lot obligatoire.', 'warning');
            return;
        }
        if(needsExp && !exp){
            toast('Stock initial ignoré : DLUO / péremption obligatoire.', 'warning');
            return;
        }

        // Unit cost: use current base cost as purchase cost when initializing.
        const unitCost = parseFloat(document.getElementById('pmCost')?.value || 0) || 0;
        await Api.mvAdd({
            type: 'add',
            productId: pid,
            warehouseId: wid,
            quantity: qty,
            unit: baseUnit || 'piece',
            lotNumber: lot || '',
            expiryDate: exp || '',
            unitCost,
            reason: 'Stock initial',
            site: '',
            operator: '',
            note: ''
        });
    },
    async load(){
        const q     = document.getElementById('prodSearch').value;
        const cat   = document.getElementById('prodCatFilter').value;
        const wh    = document.getElementById('prodWhFilter') ? document.getElementById('prodWhFilter').value : '';

        // Location labels (for UX: show emplacement in table/cards). Only meaningful when a warehouse is selected.
        this._locationsById = {};
        if (wh) {
            try {
                const locs = await Api.locations(wh);
                (locs || []).forEach(l => { this._locationsById[String(l.id)] = l; });
            } catch (e) {
                this._locationsById = {};
            }
        }
        const brand = document.getElementById('prodBrandFilter') ? document.getElementById('prodBrandFilter').value : '';
        try {
            // Charger tous les produits (sans filtre catégorie) pour pouvoir construire la liste des catégories
            const [allProds, stock] = await Promise.all([Api.products(q), Api.stock()]);
            this.stock = stock;
            this._allProds = allProds; // Référence complète pour les cascades bulk
            let shown = allProds;
            
            // Filtrer par magasin sélectionné
            if(wh){
                const whStr = String(wh);
                shown = window.StockCascade && StockCascade.getProductsForWarehouseStock
                    ? StockCascade.getProductsForWarehouseStock(allProds, stock, wh, { includeZeroRows: true, checkAllowed: true })
                    : allProds.filter(p=>{
                        const hasStockRow = (stock||[]).some(st => String(st.productId) === String(p.id) && String(st.warehouseId) === whStr);
                        if(!hasStockRow) return false;
                        let allowed = p.allowedWarehouses;
                        if(typeof allowed === 'string') {
                            try { allowed = JSON.parse(allowed || '[]'); } catch(e) { allowed = []; }
                        }
                        if(!Array.isArray(allowed) || !allowed.length) return true;
                        return allowed.some(a => String(a) === whStr);
                    });

                this._updateCategoryFilter(wh, allProds, stock);
                this._updateBrandFilter(shown);
            } else {
                this._updateCategoryFilter(null, allProds, stock);
                this._updateBrandFilter(allProds);
            }
            
            // Appliquer le filtre catégorie APRÈS avoir mis à jour la liste des catégories
            if(cat){
                shown = shown.filter(p => p.category === cat);
            }

            // Appliquer le filtre marque
            if(brand){
                shown = shown.filter(p => (p.brand || '') === brand);
            }
            
            document.getElementById('prodCount').textContent = shown.length+' produit(s)';
            this._shown = shown;
            this.pageIndex = 1;
            this._currentWhFilter = wh || '';
            // Appliquer le tri actif si existant
            this._shown = TableSort.sortData('prodTable', this._shown);
            this.renderPaged();
        } catch(e){ toast(e.message,'error'); }
    },
    
    _updateCategoryFilter(warehouseId, products, stock){
        const catSel = document.getElementById('prodCatFilter');
        if(!catSel) return;
        
        const currentValue = catSel.value;
        
        const categories = window.StockCascade
            ? StockCascade.getWarehouseCategories(this.whs || [], warehouseId || '', products || [], this.cats || [], stock || this.stock || [])
            : [...new Set((products||[]).map(p => p.category).filter(Boolean))].sort();

        catSel.innerHTML = '<option value="">Toutes catégories</option>' + 
            categories.map(c => `<option value="${c}"${c === currentValue ? ' selected' : ''}>${c}</option>`).join('');
    },
    
    _updateBrandFilter(products){
        const brandSel = document.getElementById('prodBrandFilter');
        if(!brandSel) return;
        const currentValue = brandSel.value;
        const brands = [...new Set(products.map(p => p.brand).filter(Boolean))].sort();
        brandSel.innerHTML = '<option value="">Toutes marques</option>' +
            brands.map(b => `<option value="${b}"${b === currentValue ? ' selected' : ''}>${b}</option>`).join('');
    },

    _getMaxPage(){
        const total = (this._shown||[]).length;
        return Math.max(1, Math.ceil(total / (this.pageSize || 12)));
    },

    renderPaged(){
        const total = (this._shown||[]).length;
        const maxPage = this._getMaxPage();
        if(this.pageIndex > maxPage) this.pageIndex = maxPage;
        
        // ══════════════════════════════════════════════════════════════
        // MODE SCROLL INFINI (mobile/tablette)
        // ══════════════════════════════════════════════════════════════
        if(typeof InfiniteScroll !== 'undefined' && InfiniteScroll.isMobileMode()){
            // Reset et afficher la première page
            const firstPage = (this._shown||[]).slice(0, this.pageSize || 12);
            
            // Reset l'instance
            InfiniteScroll.reset('products', firstPage);
            InfiniteScroll.setTotal('products', total);
            
            // Rendre les items (mode replace, pas append)
            if(this._currentView === 'list'){
                this.renderTable(firstPage, this._currentWhFilter || '');
            } else {
                this._renderGridItems(firstPage, this._currentWhFilter || '', false);
            }
            return;
        }
        
        // ══════════════════════════════════════════════════════════════
        // MODE PAGINATION CLASSIQUE (desktop)
        // ══════════════════════════════════════════════════════════════
        const start = (this.pageIndex - 1) * (this.pageSize || 12);
        const end = start + (this.pageSize || 12);
        const slice = (this._shown||[]).slice(start, end);

        // Update pager UI
        const info = document.getElementById('prodPageInfo');
        if(info) info.textContent = `Page ${this.pageIndex} / ${maxPage} · ${total} résultat(s)`;
        const prevBtn = document.getElementById('prodPrev');
        const nextBtn = document.getElementById('prodNext');
        if(prevBtn) prevBtn.disabled = (this.pageIndex <= 1);
        if(nextBtn) nextBtn.disabled = (this.pageIndex >= maxPage);

        // Render based on current view
        if(this._currentView === 'list'){
            this.renderTable(slice, this._currentWhFilter || '');
        } else {
            this.renderGrid(slice, this._currentWhFilter || '');
        }
    },
    
    renderTable(prods, whFilter){
        const tbody = document.getElementById('prodTableBody');
        if(!tbody) return;

        if(!prods.length){
            tbody.innerHTML = `<tr><td colspan="${this._bulkMode ? 11 : 10}" class="tbl-empty">Aucun produit trouvé</td></tr>`;
            return;
        }

        const isAdmin  = (window.SM_ROLE === 'admin');
        const bulkMode = !!this._bulkMode;

        tbody.innerHTML = prods.map(p=>{
            const entries  = (this.stock||[]).filter(s=>String(s.productId)===String(p.id));
            const scopedEntries = whFilter ? entries.filter(s=>String(s.warehouseId)===String(whFilter)) : entries;
            const totalQty = scopedEntries.reduce((sum,s)=>sum+(parseInt(s.quantity)||0),0);
            let locLabel = '—';
            if (whFilter) {
                const e = entries.find(s=>String(s.warehouseId)===String(whFilter));
                const locId = e ? (e.locationId ?? '') : '';
                if (locId && this._locationsById && this._locationsById[String(locId)]) {
                    const l = this._locationsById[String(locId)];
                    locLabel = `${l.code || ''}${l.label ? ' · ' + l.label : ''}`.trim();
                } else if (e && e.locationLabel) {
                    locLabel = String(e.locationLabel);
                } else if (locId) {
                    locLabel = `#${locId}`;
                }
            }
            const baseCost = getBaseCost(p);
            const totalVal = totalQty * baseCost;
            const baseUnit = this._getBaseUnit(p);
            const unitLabel = baseUnit === 'piece' ? 'u' : baseUnit;
            const minStock  = scopedEntries.some(s=>s.minStock!=null)
                ? Math.min(...scopedEntries.filter(s=>s.minStock!=null).map(s=>parseInt(s.minStock)))
                : null;

            const stockStatus = this._stockStatus(totalQty, minStock);
            const status = `<span class="badge ${stockStatus.badgeClass}">${stockStatus.shortLabel}</span>`;

            // Colonne Magasin
            let aw = p.allowedWarehouses;
            if(typeof aw === 'string') try { aw = JSON.parse(aw||'[]'); } catch(e){ aw=[]; }
            const whNames = (Array.isArray(aw) && aw.length)
                ? aw.map(wid => (this.whs.find(w=>String(w.id)===String(wid))||{}).name||'?').join(', ')
                : '—';

            const locateBtn = this._planButtonHtml(p.id, true);
            const actionBtn = locateBtn + (isAdmin
                ? `<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation();ProdPage.quickAction('${p.id}')" title="Mouvement">${Icons.move}</button><button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();ProdPage.duplicateProduct('${p.id}')" title="Dupliquer">📋</button>`
                : '');
            const imgSrc = this._productImageSrc(p);

            const checkCell = bulkMode
                ? `<td class="cell-check" onclick="event.stopPropagation()" style="width:36px">
                    <input type="checkbox" class="prod-chk" data-pid="${p.id}"
                      ${(this._bulkSelected||new Set()).has(String(p.id))?'checked':''}>
                   </td>`
                : '';

            return `<tr onclick="${bulkMode?'':'ProdPage.openModal(\'' + p.id + '\')'}" style="cursor:${bulkMode?'default':'pointer'}">
                ${checkCell}
                <td>
                    <div style="display:flex;align-items:center;gap:10px">
                        ${imgSrc ? `<span class="prod-list-img-trigger" role="button" tabindex="0" aria-label="Agrandir l'image du produit ${p.name}" title="Agrandir l'image" style="display:inline-flex;width:44px;height:44px;border-radius:10px;overflow:hidden;flex-shrink:0;cursor:pointer;border:1px solid var(--border-light)"><img src="${imgSrc}" style="width:100%;height:100%;object-fit:cover" alt="${p.name}" onerror="this.parentElement.style.display='none'"></span>` : ''}
                        <strong>${p.name}</strong>
                    </div>
                </td>
                <td>${p.sku||'—'}</td>
                <td style="font-size:.85em;color:var(--text-muted);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${whNames}">${whNames}</td>
                <td>${locLabel||'—'}</td>
                <td>${p.category||'—'}</td>
                <td>${p.brand||'—'}</td>
                <td class="cell-number">${totalQty} ${unitLabel}</td>
                <td class="cell-number">${totalVal>0?Locale.formatMoney(totalVal):'—'}</td>
                <td>${status}</td>
                <td class="cell-actions">${actionBtn}</td>
            </tr>`;
        }).join('');
        this._attachImageClick();
        if(bulkMode) this._bindRowCheckboxes();
    },

    // ═══════════════════════════════════════════════════════════════
    // ÉDITION DE MASSE
    // ═══════════════════════════════════════════════════════════════

    _initBulkMode(){
        this._bulkMode    = false;
        this._bulkSelected = new Set();

        const btnToggle  = document.getElementById('btnToggleBulk');
        const bulkBar    = document.getElementById('prodBulkBar');
        const checkAll   = document.getElementById('checkAll');
        const thCheck    = document.getElementById('thCheck');
        const btnApply   = document.getElementById('btnBulkApply');
        const btnPrint   = document.getElementById('btnBulkPrint');
        const btnCancel  = document.getElementById('btnBulkCancel');

        if(!btnToggle) return;

        // Montrer le bouton seulement en vue liste
        this._syncBulkToggleVisibility();

        btnToggle.onclick = () => {
            this._bulkMode = !this._bulkMode;
            this._bulkSelected.clear();
            btnToggle.classList.toggle('btn-primary', this._bulkMode);
            btnToggle.classList.toggle('btn-ghost', !this._bulkMode);
            if(thCheck) thCheck.style.display = this._bulkMode ? '' : 'none';
            if(bulkBar) bulkBar.style.display  = this._bulkMode ? 'flex' : 'none';
            this._updateBulkCount();
            this.renderPaged();
        };

        if(checkAll) checkAll.onchange = (e) => {
            const cbs = document.querySelectorAll('.prod-chk');
            cbs.forEach(cb => {
                cb.checked = e.target.checked;
                const pid = cb.dataset.pid;
                if(e.target.checked) this._bulkSelected.add(pid);
                else this._bulkSelected.delete(pid);
            });
            this._updateBulkCount();
        };

        if(btnCancel) btnCancel.onclick = () => {
            this._bulkMode = false;
            this._bulkSelected.clear();
            if(btnToggle){ btnToggle.classList.remove('btn-primary'); btnToggle.classList.add('btn-ghost'); }
            if(thCheck) thCheck.style.display = 'none';
            if(bulkBar) bulkBar.style.display  = 'none';
            if(checkAll) checkAll.checked = false;
            this._updateBulkCount();
            this.renderPaged();
        };

        if(btnApply) btnApply.onclick = () => this._applyBulkEdit();
        if(btnPrint) btnPrint.onclick = () => this._printSelectedProducts();

        // ── Peuplement initial Magasin ────────────────────────────
        const bulkWh    = document.getElementById('bulkWh');
        const bulkLoc   = document.getElementById('bulkLoc');
        const bulkCat   = document.getElementById('bulkCat');
        const bulkBrand = document.getElementById('bulkBrand');

        if(bulkWh){
            bulkWh.innerHTML = '<option value="">— Changer magasin —</option>' +
                (this.whs||[]).map(w=>`<option value="${w.id}">${w.name}</option>`).join('');

            // Cascade ① : Magasin → recalculer Emplacements + Catégories + Marques
            bulkWh.onchange = () => {
                this._rebuildBulkLoc();
                this._rebuildBulkCat();
            };
        }

        // Cascade ② : Catégorie → recalculer Marques
        if(bulkCat) bulkCat.onchange = () => this._rebuildBulkBrand();

        // Peuplement initial (tous produits visibles, pas de filtre)
        this._rebuildBulkLoc();
        this._rebuildBulkCat();
        this._updateBulkCount();
    },

    // Recalcule les catégories disponibles selon le magasin bulk sélectionné
    _rebuildBulkCat(){
        const bulkWh  = document.getElementById('bulkWh');
        const bulkCat = document.getElementById('bulkCat');
        if(!bulkCat) return;

        const whId = bulkWh ? String(bulkWh.value) : '';
        let cats = [];

        if(whId){
            // Utiliser les catégories définies sur le magasin (warehouses.json → wh.categories)
            const wh = (this.whs||[]).find(w => String(w.id) === whId);
            if(wh){
                let whCats = wh.categories;
                if(typeof whCats === 'string') try { whCats = JSON.parse(whCats||'[]'); } catch(e){ whCats=[]; }
                cats = Array.isArray(whCats) ? [...whCats].sort() : [];
            }
        } else {
            // Pas de magasin sélectionné → toutes les catégories de tous les magasins
            cats = [...new Set(
                (this.whs||[]).flatMap(w => {
                    let wc = w.categories;
                    if(typeof wc === 'string') try { wc = JSON.parse(wc||'[]'); } catch(e){ wc=[]; }
                    return Array.isArray(wc) ? wc : [];
                })
            )].sort();
        }

        const cur = bulkCat.value;
        bulkCat.innerHTML = '<option value="">— Changer catégorie —</option>' +
            cats.map(c=>`<option value="${c}"${c===cur?' selected':''}>${c}</option>`).join('');

        // Recalcule aussi les marques
        this._rebuildBulkBrand();
    },

    // Recalcule les emplacements disponibles selon le magasin bulk sélectionné
    async _rebuildBulkLoc(){
        const bulkWh  = document.getElementById('bulkWh');
        const bulkLoc = document.getElementById('bulkLoc');
        if(!bulkLoc) return;

        const whId = bulkWh ? String(bulkWh.value) : '';
        const cur = String(bulkLoc.value || '');

        // Sans magasin => désactiver (source d'erreurs et options incohérentes)
        if(!whId){
            bulkLoc.disabled = true;
            bulkLoc.innerHTML = '<option value="">— Changer emplacement —</option>';
            return;
        }

        bulkLoc.disabled = true;
        bulkLoc.innerHTML = '<option value="">— Chargement… —</option>';

        try {
            // Même source que la modale produit : Api.locations(warehouseId)
            const locs = await Api.locations(parseInt(whId,10));
            const active = (Array.isArray(locs) ? locs : []).filter(l => l && l.active !== false);

            if(active.length === 0){
                bulkLoc.innerHTML = '<option value="">— Aucun emplacement —</option>';
                bulkLoc.disabled = false;
                return;
            }

            // 0 => retirer l'emplacement (stock par magasin)
            bulkLoc.innerHTML =
                '<option value="">— Changer emplacement —</option>' +
                '<option value="0">— Aucun emplacement spécifique —</option>' +
                active.map(l => {
                    const label = [l.code, l.label, l.zone].filter(Boolean).join(' · ');
                    return `<option value="${l.id}">${label}</option>`;
                }).join('');

            // Restaurer la sélection si possible
            if(cur && [...bulkLoc.options].some(o => String(o.value) === cur)){
                bulkLoc.value = cur;
            } else {
                bulkLoc.value = '';
            }
        } catch(e){
            console.error('Bulk locations load error:', e);
            bulkLoc.innerHTML = '<option value="">— Erreur chargement —</option>';
        } finally {
            bulkLoc.disabled = false;
        }
    },

    // Recalcule les marques disponibles selon le magasin + catégorie bulk sélectionnés
    _rebuildBulkBrand(){
        const bulkWh    = document.getElementById('bulkWh');
        const bulkLoc   = document.getElementById('bulkLoc');
        const bulkCat   = document.getElementById('bulkCat');
        const bulkBrand = document.getElementById('bulkBrand');
        if(!bulkBrand) return;

        const whId = bulkWh  ? String(bulkWh.value)  : '';
        const catV = bulkCat ? bulkCat.value          : '';

        // Utiliser TOUS les produits (pas seulement ceux affichés)
        let prodsInScope = this._allProds || this._shown || [];

        if(whId){
            prodsInScope = prodsInScope.filter(p => {
                let aw = p.allowedWarehouses;
                if(typeof aw === 'string') try { aw = JSON.parse(aw||'[]'); } catch(e){ aw=[]; }
                return Array.isArray(aw) && aw.some(a => String(a) === whId);
            });
        }
        if(catV) prodsInScope = prodsInScope.filter(p => p.category === catV);

        const brands = [...new Set(prodsInScope.map(p=>p.brand).filter(Boolean))].sort();
        const cur    = bulkBrand.value;
        bulkBrand.innerHTML = '<option value="">— Changer marque —</option>' +
            brands.map(b=>`<option value="${b}"${b===cur?' selected':''}>${b}</option>`).join('');
    },

    _syncBulkToggleVisibility(){
        const btn = document.getElementById('btnToggleBulk');
        if(btn) btn.style.display = (this._currentView === 'list') ? '' : 'none';
        // Fermer le mode bulk si on quitte la vue liste
        if(this._bulkMode && this._currentView !== 'list'){
            const btnCancel = document.getElementById('btnBulkCancel');
            if(btnCancel) btnCancel.click();
        }
    },

    _bindRowCheckboxes(){
        document.querySelectorAll('.prod-chk').forEach(cb => {
            cb.onchange = (e) => {
                const pid = e.target.dataset.pid;
                if(e.target.checked) this._bulkSelected.add(pid);
                else this._bulkSelected.delete(pid);
                this._updateBulkCount();
                // Sync "tout sélectionner"
                const all = document.querySelectorAll('.prod-chk');
                const allChecked = [...all].every(c=>c.checked);
                const chkAll = document.getElementById('checkAll');
                if(chkAll) chkAll.checked = allChecked;
            };
        });
    },

    _updateBulkCount(){
        const count = this._bulkSelected ? this._bulkSelected.size : 0;
        const el = document.getElementById('prodBulkCount');
        if(el) el.textContent = `${count} sélectionné(s)`;
        const printBtn = document.getElementById('btnBulkPrint');
        if(printBtn){
            printBtn.style.display = count > 0 ? 'inline-flex' : 'none';
            printBtn.disabled = count <= 0;
        }
    },

    async _printSelectedProducts(){
        const ids = [...(this._bulkSelected || new Set())].map(v => parseInt(v, 10)).filter(Boolean);
        if(!ids.length){
            toast('Sélectionnez au moins un produit à imprimer', 'warning');
            return;
        }
        const btn = document.getElementById('btnBulkPrint');
        const oldText = btn ? btn.innerHTML : '';
        if(btn){ btn.disabled = true; btn.innerHTML = 'Préparation…'; }
        try {
            const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
            const res = await fetch(Api.url('api/product_print_pdf.php'), {
                method: 'POST',
                credentials: 'same-origin',
                headers: {
                    'Content-Type': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRF-TOKEN': csrfToken
                },
                body: JSON.stringify({
                    ids,
                    warehouseId: this._currentWhFilter || '',
                    title: 'Produits sélectionnés'
                })
            });
            if(!res.ok){
                const txt = await res.text();
                let msg = 'Erreur pendant la génération du PDF';
                try { msg = JSON.parse(txt)?.error || msg; } catch(e){}
                throw new Error(msg);
            }
            const blob = await res.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'produits_selection_' + new Date().toISOString().slice(0,10) + '.pdf';
            document.body.appendChild(a);
            a.click();
            a.remove();
            setTimeout(() => URL.revokeObjectURL(url), 3000);
            toast('PDF des produits sélectionnés généré', 'success');
        } catch(e){
            toast(e.message || 'Erreur PDF', 'error');
        } finally {
            if(btn){ btn.disabled = false; btn.innerHTML = oldText || 'Imprimer'; }
            this._updateBulkCount();
        }
    },

    async _applyBulkEdit(){
        if(!this._bulkSelected.size){
            toast('Aucun produit sélectionné', 'warning');
            return;
        }
        const cat   = document.getElementById('bulkCat')?.value   || '';
        const brand = document.getElementById('bulkBrand')?.value || '';
        const whId  = document.getElementById('bulkWh')?.value    || '';
        const locV  = document.getElementById('bulkLoc')?.value   || '';

        if(!cat && !brand && !whId && !locV){
            toast('Choisissez au moins un champ à modifier', 'warning');
            return;
        }
        // Emplacement nécessite un magasin (stock par magasin)
        if(locV && !whId){
            toast('Choisissez d’abord un magasin pour modifier l’emplacement', 'warning');
            return;
        }

        const ids = [...this._bulkSelected];
        let ok = 0, ko = 0;

        for(const pid of ids){
            const prod = (this._shown||[]).find(p=>String(p.id)===String(pid));
            if(!prod) continue;

            const fields = {};
            if(cat)   fields.category = cat;
            if(brand) fields.brand    = brand;
            if(whId){
                // Remplacer (pas ajouter) le magasin
                fields.allowedWarehouses = [parseInt(whId)];
            }

            try {
                // prodEdit nécessite l'objet produit complet ou au minimum id + champs
                // On envoie uniquement les champs à modifier + l'id
                await Api.prodEdit({ id: pid, ...fields });

                // Emplacement (stock) : uniquement si demandé
                if(locV !== ''){
                    const locationId = (String(locV) === '0') ? null : parseInt(locV, 10);
                    await Api.stockSet({
                        productId: parseInt(pid, 10),
                        warehouseId: parseInt(whId, 10),
                        locationId: locationId
                    });
                }

                ok++;
            } catch(e){
                ko++;
                console.error('Bulk update error:', e, pid);
            }
        }

        toast(`${ok} produit(s) mis à jour${ko>0?' — '+ko+' erreur(s)':''}`, ok>0?'success':'error');

        // Reset + reload
        this._bulkSelected.clear();
        this._updateBulkCount();
        document.querySelectorAll('.prod-chk').forEach(cb=>cb.checked=false);
        const chkAll = document.getElementById('checkAll'); if(chkAll) chkAll.checked=false;
        await this.load();
    },

    _buildWarehouseBadges(productId, whFilter){
        const stock = this.stock || [];
        const whs = this.whs || [];
        const prods = this._shown || [];
        
        // Trouver le produit pour récupérer allowedWarehouses
        const product = prods.find(p => String(p.id) === String(productId));
        
        // Stock par magasin
        const byWh = new Map();
        for(const s of stock){
            if(String(s.productId) !== String(productId)) continue;
            const wid = String(s.warehouseId||'');
            const qty = parseFloat(s.quantity)||0;
            byWh.set(wid, (byWh.get(wid)||0) + qty);
        }
        
        // Ajouter aussi les magasins autorisés même sans stock
        if(product && product.allowedWarehouses) {
            let allowed = product.allowedWarehouses;
            if(typeof allowed === 'string') {
                try { allowed = JSON.parse(allowed || '[]'); } catch(e) { allowed = []; }
            }
            if(Array.isArray(allowed)) {
                for(const wid of allowed) {
                    const widStr = String(wid);
                    if(!byWh.has(widStr)) {
                        byWh.set(widStr, 0);
                    }
                }
            }
        }

        const entries = Array.from(byWh.entries()).map(([wid,qty])=>{
            const w = whs.find(x=>String(x.id)===String(wid));
            return { wid, qty, name: w ? w.name : ('Magasin '+wid) };
        }).sort((a,b)=>b.qty-a.qty);

        if(whFilter){
            const e = entries.find(x=>String(x.wid)===String(whFilter));
            if(!e) return '';
            const badgeClass = e.qty > 0 ? 'badge-blue' : 'badge-gray';
            return `<span class="badge ${badgeClass}" style="font-size:10px">${e.name} · ${e.qty}u</span>`;
        }

        if(!entries.length) return '';
        const top = entries.slice(0,3).map(e=>{
            const badgeClass = e.qty > 0 ? 'badge-blue' : 'badge-gray';
            return `<span class="badge ${badgeClass}" style="font-size:10px">${e.name} · ${e.qty}u</span>`;
        }).join(' ');
        const more = entries.length>3 ? `<span class="badge badge-gray" style="font-size:10px">+${entries.length-3}</span>` : '';
        return `<div class="prod-wh-badges" style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap">${top} ${more}</div>`;
    },

    // Version sans unité dans les badges magasin (unité déjà affichée sous la barre de stock)
    _buildWarehouseBadgesNoUnit(productId, whFilter){
        const stock = this.stock || [];
        const whs   = this.whs   || [];
        const byWh  = new Map();
        for(const s of stock){
            if(String(s.productId)!==String(productId)) continue;
            const wid = String(s.warehouseId||'');
            byWh.set(wid, (byWh.get(wid)||0) + (parseFloat(s.quantity)||0));
        }
        const entries = Array.from(byWh.entries()).map(([wid,qty])=>{
            const w = whs.find(x=>String(x.id)===String(wid));
            return { wid, qty, name: w ? w.name : ('Mag.'+wid) };
        }).sort((a,b)=>b.qty-a.qty);

        if(whFilter){
            const e = entries.find(x=>String(x.wid)===String(whFilter));
            if(!e) return '';
            const cls = e.qty > 0 ? 'badge-blue' : 'badge-muted';
            return `<span class="badge ${cls} prod-badge-wh">${e.name}</span>`;
        }
        if(!entries.length) return '';
        const top = entries.slice(0,2).map(e=>{
            const cls = e.qty > 0 ? 'badge-blue' : 'badge-muted';
            return `<span class="badge ${cls} prod-badge-wh">${e.name}</span>`;
        }).join('');
        const more = entries.length>2 ? `<span class="badge badge-muted prod-badge-wh">+${entries.length-2}</span>` : '';
        return top + more;
    },

renderGrid(prods, whFilter){
        const grid = document.getElementById('prodGrid');
        if(!prods.length){ grid.innerHTML='<div class="tbl-empty" style="grid-column:1/-1;padding:40px">Aucun produit trouvé</div>'; return; }
        this._ensureCardStyles();
        grid.innerHTML = this._buildGridHTML(prods, whFilter);
        this._attachImageClick();
    },
    
    _renderGridItems(prods, whFilter, append = false){
        const grid = document.getElementById('prodGrid');
        if(!grid) return;
        if(!prods.length && !append){ 
            grid.innerHTML='<div class="tbl-empty" style="grid-column:1/-1;padding:40px">Aucun produit trouvé</div>'; 
            return; 
        }
        this._ensureCardStyles();
        const html = this._buildGridHTML(prods, whFilter);
        if(append){
            grid.insertAdjacentHTML('beforeend', html);
        } else {
            grid.innerHTML = html;
        }
        this._attachImageClick();
    },

    _ensureCardStyles(){
        if(document.getElementById('pc2-card-css')) return;
        const s = document.createElement('style');
        s.id = 'pc2-card-css';
        s.textContent = `
/* ═══ PRODUCT CARDS v3 — Injection JS — Priorité absolue ═══ */
#prodGrid.products-grid,#prodGrid.prod-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))!important;gap:16px!important}
#prodGrid .pc2{background:var(--bg-card,#fff)!important;border:2px solid var(--border,#e2e8f0)!important;border-radius:14px!important;cursor:pointer!important;position:relative!important;display:flex!important;flex-direction:column!important;overflow:hidden!important;padding:0!important;gap:0!important;transition:transform .22s,box-shadow .22s,border-color .22s!important}
#prodGrid .pc2:hover{transform:translateY(-4px)!important;border-color:var(--blue,#3b82f6)!important;box-shadow:0 14px 32px rgba(0,0,0,.10),0 0 0 1px var(--blue,#3b82f6)!important}
#prodGrid .pc2:active{transform:translateY(-1px)!important}
#prodGrid .pc2-hero{position:relative!important;width:100%!important;aspect-ratio:1/1!important;background:var(--bg-elevated,#f8fafc)!important;display:flex!important;align-items:center!important;justify-content:center!important;overflow:hidden!important;border-bottom:1px solid var(--border-light,rgba(0,0,0,.04))!important;border-radius:0!important;flex-shrink:0!important;min-height:160px!important}
#prodGrid .pc2-hero-img{width:100%!important;height:100%!important;object-fit:contain!important;padding:14px!important;transition:transform .3s!important;display:block!important}
#prodGrid .pc2-hero--empty{background:var(--bg-elevated,#f8fafc)!important}
#prodGrid .pc2-hero-svg{width:52px!important;height:52px!important;fill:none!important;stroke:var(--text-muted,#94a3b8)!important;stroke-width:1!important;opacity:.18!important}
#prodGrid .pc2-qa{position:absolute!important;top:8px!important;right:8px!important;z-index:2!important;width:34px!important;height:34px!important;border-radius:10px!important;background:var(--bg-card,#fff)!important;border:1px solid var(--border,#e2e8f0)!important;display:flex!important;align-items:center!important;justify-content:center!important;color:var(--text-muted,#94a3b8)!important;cursor:pointer!important;opacity:0!important;transition:all .15s!important;box-shadow:0 2px 8px rgba(0,0,0,.08)!important;padding:0!important}
#prodGrid .pc2:hover .pc2-qa,#prodGrid .pc2:focus-within .pc2-qa{opacity:1!important}
#prodGrid .pc2-qa:hover{background:var(--blue-pale,#dbeafe)!important;border-color:var(--blue)!important;color:var(--blue)!important}
#prodGrid .pc2-qa svg{width:16px!important;height:16px!important}
#prodGrid .pc2-flag{position:absolute!important;top:8px!important;left:8px!important;z-index:2!important;padding:3px 10px!important;border-radius:6px!important;font-size:10px!important;font-weight:700!important;letter-spacing:.5px!important;text-transform:uppercase!important}
#prodGrid .pc2-flag--crit{background:var(--red,#ef4444)!important;color:#fff!important}
#prodGrid .pc2-flag--low{background:var(--orange,#f59e0b)!important;color:#fff!important}
#prodGrid .pc2-identity{padding:12px 14px 4px!important}
#prodGrid .pc2-name{font-size:14px!important;font-weight:700!important;color:var(--text-primary,#0f172a)!important;line-height:1.25!important;margin:0 0 3px!important;padding:0!important;display:-webkit-box!important;-webkit-line-clamp:2!important;-webkit-box-orient:vertical!important;overflow:hidden!important}
#prodGrid .pc2-sku{font-family:var(--font-mono,'DM Mono',monospace)!important;font-size:11px!important;color:var(--text-muted,#94a3b8)!important;letter-spacing:.3px!important}
#prodGrid .pc2-chips{display:flex!important;flex-wrap:wrap!important;gap:4px!important;padding:6px 14px 2px!important}
#prodGrid .pc2-chip{display:inline-flex!important;align-items:center!important;padding:2px 8px!important;border-radius:6px!important;font-size:10.5px!important;font-weight:600!important;white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important;max-width:140px!important;line-height:1.5!important}
#prodGrid .pc2-chip--brand{background:var(--blue-pale,#dbeafe)!important;color:var(--blue,#3b82f6)!important}
#prodGrid .pc2-chip--cat{background:var(--bg-elevated,#f1f5f9)!important;color:var(--text-secondary,#64748b)!important;border:1px solid var(--border-light,rgba(0,0,0,.06))!important}
#prodGrid .pc2-chip--loc{background:var(--green-pale,#dcfce7)!important;color:var(--green,#16a34a)!important}
#prodGrid .pc2-wh{display:flex!important;flex-wrap:wrap!important;gap:4px!important;padding:4px 14px 0!important}
#prodGrid .pc2-bottom{padding:10px 14px 12px!important;margin-top:auto!important}
#prodGrid .pc2-bar{height:5px!important;background:var(--bg-elevated,#f1f5f9)!important;border-radius:99px!important;overflow:hidden!important;margin-bottom:8px!important}
#prodGrid .pc2-bar-fill{height:100%!important;border-radius:99px!important;transition:width .3s!important}
#prodGrid .pc2-bar--ok{background:linear-gradient(90deg,#22c55e,#86efac)!important}
#prodGrid .pc2-bar--low{background:linear-gradient(90deg,#f59e0b,#fcd34d)!important}
#prodGrid .pc2-bar--crit{background:linear-gradient(90deg,#ef4444,#fca5a5)!important}
#prodGrid .pc2-stats{display:flex!important;align-items:baseline!important;justify-content:space-between!important;gap:6px!important}
#prodGrid .pc2-qty{font-size:16px!important;font-weight:800!important;letter-spacing:-.5px!important}
#prodGrid .pc2-qty--crit{color:var(--red,#ef4444)!important}
#prodGrid .pc2-qty--low{color:var(--orange,#f59e0b)!important}
#prodGrid .pc2-qty--ok{color:var(--text-primary,#0f172a)!important}
#prodGrid .pc2-val{font-size:12px!important;color:var(--text-muted,#94a3b8)!important;font-weight:500!important}
#prodGrid .pc2-docs{display:flex!important;gap:6px!important;padding:0 14px 10px!important;border-top:1px solid var(--border-light,rgba(0,0,0,.04))!important;padding-top:8px!important}
#prodGrid .pc2-doc{font-size:10.5px!important;padding:3px 8px!important;border-radius:6px!important;background:var(--bg-elevated,#f1f5f9)!important;color:var(--text-secondary,#64748b)!important;text-decoration:none!important;font-weight:500!important;border:1px solid transparent!important;transition:all .12s!important}
#prodGrid .pc2-doc:hover{background:var(--blue-pale)!important;border-color:var(--blue)!important;color:var(--blue)!important}
@media(max-width:1024px){#prodGrid.products-grid,#prodGrid.prod-grid,.products-grid,.prod-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))!important;gap:12px!important}#prodGrid .pc2-hero{aspect-ratio:1/1!important}#prodGrid .pc2-hero-img{padding:10px!important}#prodGrid .pc2-identity{padding:10px 12px 3px!important}#prodGrid .pc2-name{font-size:13px!important}#prodGrid .pc2-chips{padding:4px 12px 2px!important}#prodGrid .pc2-wh{padding:3px 12px 0!important}#prodGrid .pc2-bottom{padding:8px 12px 10px!important}#prodGrid .pc2-qty{font-size:14px!important}}
@media(max-width:640px){#prodGrid.products-grid,#prodGrid.prod-grid,.products-grid,.prod-grid{grid-template-columns:repeat(2,1fr)!important;gap:8px!important}#prodGrid .pc2{border-radius:12px!important;border-width:1px!important}#prodGrid .pc2-hero{min-height:120px!important}#prodGrid .pc2-hero-img{padding:6px!important}#prodGrid .pc2-hero-svg{width:36px!important;height:36px!important}#prodGrid .pc2-identity{padding:8px 10px 2px!important}#prodGrid .pc2-name{font-size:12px!important}#prodGrid .pc2-sku{font-size:9.5px!important}#prodGrid .pc2-chips{padding:4px 10px 0!important;gap:3px!important}#prodGrid .pc2-chip{font-size:9px!important;padding:1px 5px!important;max-width:110px!important}#prodGrid .pc2-wh{padding:2px 10px 0!important}#prodGrid .pc2-bottom{padding:6px 10px 8px!important}#prodGrid .pc2-bar{height:4px!important;margin-bottom:6px!important}#prodGrid .pc2-qty{font-size:13px!important}#prodGrid .pc2-val{font-size:10px!important}#prodGrid .pc2-docs{padding:0 10px 8px!important;padding-top:6px!important}#prodGrid .pc2-qa{opacity:1!important;width:28px!important;height:28px!important;top:5px!important;right:5px!important;border-radius:8px!important}#prodGrid .pc2-qa svg{width:14px!important;height:14px!important}#prodGrid .pc2-flag{font-size:8px!important;padding:2px 6px!important;top:5px!important;left:5px!important}}
@media(max-width:380px){#prodGrid.products-grid,#prodGrid.prod-grid,.products-grid,.prod-grid{grid-template-columns:1fr!important}#prodGrid .pc2-hero{aspect-ratio:16/10!important}}
`;
        document.head.appendChild(s);
    },
    
    _resolvePublicUrl(url){
        const raw = String(url || '').trim();
        if(!raw) return '';
        if(typeof Api !== 'undefined' && typeof Api.imageUrl === 'function') return Api.imageUrl(raw);
        if(typeof Api !== 'undefined' && typeof Api.publicUrl === 'function') return Api.publicUrl(raw);
        if(/^(?:https?:)?\/\//i.test(raw) || /^(?:data|blob):/i.test(raw)) return raw;
        const runtime = window.MageStockRuntime || {};
        const base = String(runtime.basePath || '').replace(/\/+$/, '');
        let path = raw.replace(/^\.\//, '').replace(/^\/+/, '');
        const cleanBase = base.replace(/^\/+/, '');
        if(cleanBase && path.startsWith(cleanBase + '/')) path = path.slice(cleanBase.length + 1);
        return (base ? base : '') + '/' + path;
    },

    _productImageSrc(product){
        if(!product) return '';
        return this._resolvePublicUrl(product.image || product.imageUrl || product.photo || product.picture || '');
    },

    _productImageFallbackHtml(cls){
        return '<span class="'+(cls || 'prod-img-fallback')+'" aria-hidden="true">📦</span>';
    },

    /**
     * Construit le HTML pour les cartes produits
     */
    _buildGridHTML(prods, whFilter){
        const isAdmin = (window.SM_ROLE === 'admin');
        return prods.map(p=>{
            const entries = (this.stock||[]).filter(s=>String(s.productId)===String(p.id));
            const scopedEntries = whFilter ? entries.filter(s=>String(s.warehouseId)===String(whFilter)) : entries;
            const totalQty = scopedEntries.reduce((sum,s)=>sum+(parseInt(s.quantity)||0),0);
            let locLabel = '';
            if (whFilter) {
                const e = entries.find(s=>String(s.warehouseId)===String(whFilter));
                const locId = e ? (e.locationId ?? '') : '';
                if (locId && this._locationsById && this._locationsById[String(locId)]) {
                    const l = this._locationsById[String(locId)];
                    locLabel = (l.code || '') + (l.label ? ' · ' + l.label : '');
                } else if (e && e.locationLabel) {
                    locLabel = String(e.locationLabel);
                }
            }
            const baseCost = getBaseCost(p);
            const totalVal = totalQty * baseCost;
            const baseUnit = this._getBaseUnit(p);
            const unitLabel = baseUnit === 'piece' ? 'u' : baseUnit;
            const minStock = scopedEntries.some(s=>s.minStock!=null) ? Math.min(...scopedEntries.filter(s=>s.minStock!=null).map(s=>parseInt(s.minStock))) : null;
            const maxAuth = scopedEntries.some(s=>s.maxAuthorized!=null && s.maxAuthorized<9990) ? Math.max(...scopedEntries.filter(s=>s.maxAuthorized!=null && s.maxAuthorized<9990).map(s=>parseInt(s.maxAuthorized))) : null;
            const barRef = maxAuth || (minStock ? minStock * 3 : null);
            const barPct = barRef && barRef > 0 ? Math.min(100, Math.round((totalQty / barRef) * 100)) : (totalQty > 0 ? 60 : 0);
            const stockStatus = this._stockStatus(totalQty, minStock);
            const barCls = stockStatus.barClass;

            const imgSrc = this._productImageSrc(p);
            const hasImg = !!imgSrc;
            const supplier = p.supplier ? (this.suppliers||[]).find(s=>String(s.id)===String(p.supplier)) : null;
            const whBadges = ProdPage._buildWarehouseBadgesNoUnit(p.id, whFilter);
            const qaBtn = isAdmin ? '<button type="button" class="pc2-qa" data-no-image-zoom="true" onclick="event.preventDefault();event.stopPropagation();ProdPage.quickAction(\''+p.id+'\')" title="Mouvement rapide" aria-label="Mouvement rapide">'+Icons.move+'</button>' : '';

            let statusHtml = '';
            if(barCls === 'crit') statusHtml = '<span class="pc2-flag pc2-flag--crit">CRITIQUE</span>';
            else if(barCls === 'low') statusHtml = '<span class="pc2-flag pc2-flag--low">STOCK BAS</span>';

            let html = '<div class="pc2" onclick="ProdPage.openModal(\''+p.id+'\')">';

            // ── HERO IMAGE (60% de la carte) ──
            html += '<div class="pc2-hero prod-thumb-zoomable'+(hasImg?'':' pc2-hero--empty')+'" '+(hasImg?'role="button" tabindex="0" aria-label="Agrandir l\'image du produit '+String(p.name||'').replace(/"/g,'&quot;')+'" title="Agrandir l\'image"':'')+'>';
            if(hasImg) html += '<img src="'+this._esc(imgSrc)+'" alt="'+this._esc(p.name||'Produit')+'" class="pc2-hero-img" onerror="this.parentElement.classList.add(\'pc2-hero--empty\');this.remove()">';
            else html += '<svg viewBox="0 0 24 24" class="pc2-hero-svg"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/></svg>';
            html += qaBtn + statusHtml + '</div>';

            // ── NOM + SKU ──
            html += '<div class="pc2-identity">';
            html += '<h4 class="pc2-name">'+p.name+'</h4>';
            html += '<span class="pc2-sku">'+(p.sku||'')+'</span>';
            const variantText = this._getVariantAttributeText(p);
            if(p.variantLabel || variantText){
                html += '<span class="pc2-sku" style="color:var(--blue);font-weight:700">'+this._esc(p.variantLabel || variantText)+'</span>';
            }
            html += '</div>';

            // ── BADGES : marque + catégorie + emplacement ──
            const chips = [];
            if(p.brand) chips.push('<span class="pc2-chip pc2-chip--brand">'+p.brand+'</span>');
            if(p.category) chips.push('<span class="pc2-chip pc2-chip--cat">'+p.category+'</span>');
            if(p.size) chips.push('<span class="pc2-chip pc2-chip--size">👕 '+p.size+'</span>');
            if(locLabel) chips.push('<span class="pc2-chip pc2-chip--loc">\u{1F4CD} '+locLabel+'</span>');
            if(chips.length) html += '<div class="pc2-chips">'+chips.join('')+'</div>';

            // Enfants (déclinaisons) : afficher les tailles ou variantes génériques sous forme de mini-badges
            if(!p.parentId && ((p.sizeType && p.sizeType !== 'none') || p.isVariantParent || p.variantFamily)){
                const children = (this._shown||[]).filter(c=>c.parentId && String(c.parentId)===String(p.id));
                if(children.length){
                    html += '<div class="pc2-sizes-row">' + children.slice(0,8).map(c=>{
                        const cStock = (this.stock||[]).filter(s=>String(s.productId)===String(c.id)).reduce((s,x)=>s+(parseInt(x.quantity)||0),0);
                        const label = c.variantLabel || c.size || this._buildVariantLabel((this.variantFamilies||[]).find(f=>f.id===c.variantFamily), this._parseMaybeJson(c.variantAttributes, {}) || {}) || 'Var.';
                        return '<span class="pc2-size-mini" title="'+this._esc(c.name)+' — Stock: '+cStock+'">'+this._esc(label)+' ('+cStock+')</span>';
                    }).join('') + (children.length>8 ? '<span class="pc2-size-mini">+'+(children.length-8)+'</span>' : '') + '</div>';
                }
            }

            // ── MAGASINS + LOCALISATION PLAN ──
            if(whBadges) html += '<div class="pc2-wh">'+whBadges+'</div>';
            html += '<div class="pc2-location-actions">' + this._planButtonHtml(p.id, false) + '</div>';

            // ── STOCK BAR + QUANTITÉ + VALEUR ──
            html += '<div class="pc2-bottom">';
            html += '<div class="pc2-bar"><div class="pc2-bar-fill pc2-bar--'+barCls+'" style="width:'+barPct+'%"></div></div>';
            html += '<div class="pc2-stats">';
            html += '<span class="pc2-qty pc2-qty--'+barCls+'">'+totalQty+' '+unitLabel+'</span>';
            if(totalVal > 0) html += '<span class="pc2-val">'+Locale.formatMoney(totalVal)+'</span>';
            html += '</div>';
            html += '</div>';

            // ── DOCUMENTS ──
            if(p.datasheetUrl || p.msdsUrl){
                html += '<div class="pc2-docs">';
                if(p.datasheetUrl) html += '<a href="'+p.datasheetUrl+'" target="_blank" onclick="event.stopPropagation()" class="pc2-doc">\u{1F4C4} FT</a>';
                if(p.msdsUrl) html += '<a href="'+p.msdsUrl+'" target="_blank" onclick="event.stopPropagation()" class="pc2-doc">\u26A0\uFE0F FDS</a>';
                html += '</div>';
            }

            html += '</div>';
            return html;
        }).join('');
    },
    async quickAction(pid){
        const p = await Api.products().then(all=>all.find(x=>String(x.id)===String(pid)));
        if(!p){ toast('Produit introuvable','warning'); return; }

        const [stockAll] = await Promise.all([Api.stock(pid)]);
        const whs = this.whs;
        const baseUnit = this._getBaseUnit(p);
        const needsLot = !!p.trackLot;
        const needsExp = !!p.trackExpiry;
        const unitOpts = this._buildUnitOptions(p);
        const unitCostPrefill = this._getUnitCostPrefill(p, baseUnit);

        // Stock par magasin
        const stockMap = {};
        (stockAll||[]).forEach(s => { stockMap[String(s.warehouseId)] = Number(s.quantity)||0; });

        // Filtrer les magasins par allowedWarehouses du produit
        let allowed = p.allowedWarehouses;
        if (typeof allowed === 'string') try { allowed = JSON.parse(allowed||'[]'); } catch(e) { allowed = []; }
        const allowedStrs = Array.isArray(allowed) ? allowed.map(String) : [];
        const eligibleWhs = (!allowedStrs.length)
            ? whs
            : whs.filter(w => allowedStrs.includes(String(w.id)));

        // Options magasin avec stock visible
        const whOpts = eligibleWhs.map(w => {
            const qty = stockMap[String(w.id)] || 0;
            const lbl = qty > 0 ? ` — ${qty} ${baseUnit}` : ' — aucun stock';
            return `<option value="${w.id}">${w.name}${lbl}</option>`;
        }).join('');

        const traceBlock = (needsLot || needsExp) ? `
            <div class="sq-sep"></div>
            <div class="sq-section-title">📋 Traçabilité</div>
            <div class="sq-field-group">
                <label class="lbl">Numéro de lot${needsLot?' *':''}</label>
                <input type="text" class="inp" id="qaLot" placeholder="Ex: LOT-2026-001">
            </div>
            <div class="sq-field-group">
                <label class="lbl">DLUO / Péremption${needsExp?' *':''}</label>
                <input type="date" class="inp" id="qaExp">
            </div>` : '';

        Modal.show('Mouvement rapide — '+p.name, `
<div class="sq-form">

  <!-- Produit banner -->
  <div class="sq-product-banner">
    <div class="sq-product-icon">📦</div>
    <div class="sq-product-info">
      <div class="sq-product-name">${p.name||''}</div>
      <div class="sq-product-sku">${p.sku||p.barcode||''}</div>
    </div>
  </div>

  <!-- ① Magasin -->
  <div class="sq-step" id="qsStepWh">
    <div class="sq-step-header">
      <span class="sq-step-num">1</span>
      <span class="sq-step-label">Magasin *</span>
      <span class="sq-stock-badge" id="qsStockBadge" style="display:none"></span>
    </div>
    <select class="sel" id="qaProdWh">
      <option value="">— Choisir un magasin —</option>
      ${whOpts}
    </select>
  </div>

  <!-- ② Mouvement (activé après magasin) -->
  <div class="sq-step sq-step-locked" id="qsStepAction">
    <div class="sq-step-header">
      <span class="sq-step-num">2</span>
      <span class="sq-step-label">Type de mouvement</span>
    </div>
    <div style="display:flex;gap:8px">
      <button class="btn btn-success" style="flex:1" id="qsBtnAdd" disabled
        onclick="ProdPage._qaOpenType('add')">＋ Ajout de stock</button>
      <button class="btn btn-danger" style="flex:1" id="qsBtnRem" disabled
        onclick="ProdPage._qaOpenType('remove')">− Sortie de stock</button>
    </div>
  </div>

  <!-- ③ Quantité (activé après choix du type) -->
  <div class="sq-step sq-step-locked" id="qsStepQty">
    <div class="sq-step-header">
      <span class="sq-step-num">3</span>
      <span class="sq-step-label">Quantité & Conditionnement</span>
    </div>
    <div class="sq-qty-row">
      <div>
        <label class="lbl">Unité</label>
        <select class="sel" id="qaUnit" disabled>${unitOpts}</select>
      </div>
      <div>
        <label class="lbl">Quantité *</label>
        <input type="number" class="inp" id="qaQty" min="0.01" step="0.01" value="1" disabled>
      </div>
    </div>
    <small class="sq-conv-hint">Conversion automatique vers l'unité de base (${baseUnit}).</small>
    <div class="sq-field-group" id="qsCostWrap">
      <label class="lbl">Prix d'achat (optionnel)</label>
      <input type="number" class="inp" id="qaUnitCost" min="0" step="0.0001" value="${unitCostPrefill}" disabled>
    </div>
  </div>

  <!-- ④ Détails -->
  <div class="sq-step sq-step-locked" id="qsStepDetails">
    <div class="sq-step-header">
      <span class="sq-step-num">4</span>
      <span class="sq-step-label">Détails</span>
    </div>
    ${traceBlock}
    <div class="sq-field-group">
      <label class="lbl">Raison / Chantier</label>
      <input type="text" class="inp" id="qaSite" placeholder="Chantier / lieu…" disabled>
    </div>
    <div class="sq-field-group">
      <label class="lbl">Note</label>
      <input type="text" class="inp" id="qaNote" disabled>
    </div>
    <div style="margin-top:14px;display:flex;gap:8px">
      <button class="btn btn-success" style="flex:1;display:none" id="qaValAdd"
        onclick="ProdPage._qaSubmit('${pid}','add')">✓ Valider l'ajout</button>
      <button class="btn btn-danger" style="flex:1;display:none" id="qaValRem"
        onclick="ProdPage._qaSubmit('${pid}','remove')">✓ Valider la sortie</button>
    </div>
  </div>

</div>
        `, [{label:'Fermer',cls:'btn-secondary',onclick:()=>Modal.close()}], 'info');

        // CSS (partagé avec scanner.js quickMove)
        // ── Icône personnalisée mouvement rapide ────────────────────
        const mqIcon = document.getElementById('modalIcon');
        if(mqIcon){
            mqIcon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>';
            const hdr = mqIcon.parentElement;
            if(hdr) hdr.style.background = 'var(--green-pale, #dcfce7)';
            const svg = mqIcon.querySelector('svg');
            if(svg) svg.style.stroke = 'var(--green, #22c55e)';
        }

        // CSS (partagé avec scanner.js quickMove)
        if(!document.getElementById('sq-modal-css')){
            const s=document.createElement('style'); s.id='sq-modal-css';
            s.textContent=`.sq-product-banner{display:flex;align-items:center;gap:12px;padding:12px 14px;background:var(--bg-elevated);border-radius:var(--r-lg,10px);border:1px solid var(--border);margin-bottom:2px}.sq-product-icon{width:36px;height:36px;border-radius:50%;background:var(--blue-pale,#eff6ff);color:var(--blue,#3b82f6);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;flex-shrink:0}.sq-product-name{font-weight:600;font-size:15px;color:var(--text-primary)}.sq-product-sku{font-size:12px;color:var(--text-muted);font-family:monospace}.sq-form{display:flex;flex-direction:column;gap:6px}.sq-step{border:1px solid var(--border);border-radius:var(--r-lg,10px);padding:14px 16px;background:var(--bg-card);transition:opacity .2s,background .2s}.sq-step-locked{opacity:.42;pointer-events:none;background:var(--bg-elevated)}.sq-step-header{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}.sq-step-num{width:24px;height:24px;border-radius:50%;background:var(--blue,#3b82f6);color:#fff;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}.sq-step-locked .sq-step-num{background:var(--text-muted,#94a3b8)}.sq-step-label{font-size:14px;font-weight:600;color:var(--text-primary);flex:1}.sq-stock-badge{font-size:12px;font-weight:600;padding:3px 10px;border-radius:999px}.sq-stock-ok{background:var(--green-pale,#dcfce7);color:var(--green,#16a34a)}.sq-stock-low{background:var(--yellow-pale,#fef9c3);color:#b45309}.sq-stock-zero{background:var(--red-pale,#fee2e2);color:var(--red,#dc2626)}.sq-qty-row{display:grid;grid-template-columns:1fr 2fr;gap:12px}@media(max-width:400px){.sq-qty-row{grid-template-columns:1fr}}.sq-conv-hint{display:block;font-size:12px;color:var(--text-muted);margin-top:6px;margin-bottom:4px}.sq-sep{height:1px;background:var(--border);margin:10px 0 4px}.sq-section-title{font-size:13px;font-weight:700;color:var(--text-secondary);margin-bottom:8px}.sq-field-group{margin-top:10px}.sq-field-group .lbl{display:block;margin-bottom:4px}`;
            document.head.appendChild(s);
        }

        // Stocker le contexte pour _qaOpenType
        this._qaCtx = { p, pid, baseUnit, needsLot, needsExp, stockMap };

        // Cascade magasin → débloquer étape 2
        const whSel = document.getElementById('qaProdWh');
        const badge = document.getElementById('qsStockBadge');
        const stepAction = document.getElementById('qsStepAction');
        const btnAdd = document.getElementById('qsBtnAdd');
        const btnRem = document.getElementById('qsBtnRem');

        if(whSel) whSel.addEventListener('change', () => {
            const wid = whSel.value;
            if(!wid) return;
            const qty = stockMap[String(wid)] || 0;

            if(badge){
                badge.style.display='';
                badge.className = `sq-stock-badge ${qty<=0?'sq-stock-zero':qty<(p.minStock||5)?'sq-stock-low':'sq-stock-ok'}`;
                badge.textContent = `Stock : ${qty} ${baseUnit}`;
            }
            if(stepAction) stepAction.classList.remove('sq-step-locked');
            if(btnAdd){ btnAdd.disabled=false; }
            if(btnRem){ btnRem.disabled = qty <= 0; }
        });

        // Unité prefill on change
        const unitSel = document.getElementById('qaUnit');
        const costInp = document.getElementById('qaUnitCost');
        if(unitSel) unitSel.onchange = () => { if(costInp) costInp.value = String(this._getUnitCostPrefill(p, unitSel.value)||0); };

        // Stocker eligibleWhs dans le contexte
        this._qaCtx.eligibleWhs = eligibleWhs;

        // Si 1 seul magasin : présélectionner
        if(eligibleWhs.length === 1){
            whSel.value = eligibleWhs[0].id;
            whSel.dispatchEvent(new Event('change'));
        }
    },

    _qaOpenType(type){
        const ctx = this._qaCtx;
        if(!ctx) return;
        const baseUnit = ctx.baseUnit;

        // Débloquer étapes 3 et 4
        const stepQty = document.getElementById('qsStepQty');
        const stepDetails = document.getElementById('qsStepDetails');
        if(stepQty) stepQty.classList.remove('sq-step-locked');
        if(stepDetails) stepDetails.classList.remove('sq-step-locked');

        ['qaUnit','qaQty','qaUnitCost','qaSite','qaNote','qaLot','qaExp'].forEach(id=>{
            const el=document.getElementById(id);
            if(el && el.disabled!==undefined) el.disabled=false;
        });

        // Unité par défaut
        const unitSel = document.getElementById('qaUnit');
        if(unitSel && [...unitSel.options].some(o=>o.value===baseUnit)) unitSel.value=baseUnit;

        // Masquer/afficher le bloc coût selon type
        const costWrap = document.getElementById('qsCostWrap');
        if(costWrap) costWrap.style.display = type==='add' ? '' : 'none';

        // Afficher le bon bouton Valider
        const vadd = document.getElementById('qaValAdd');
        const vrem = document.getElementById('qaValRem');
        if(vadd) vadd.style.display = type==='add' ? '' : 'none';
        if(vrem) vrem.style.display = type==='remove' ? '' : 'none';

        // Focus quantité
        setTimeout(()=>{ const q=document.getElementById('qaQty'); if(q) q.focus(); }, 60);
    },
    async _qaSubmit(pid, type){
        const wid = (document.getElementById('qaProdWh')||{}).value;
        const qty = parseFloat((document.getElementById('qaQty')||{}).value)||0;
        const unit = document.getElementById('qaUnit')?.value || 'piece';
        const lotNumber  = (document.getElementById('qaLot')?.value||'').trim();
        const expiryDate = document.getElementById('qaExp')?.value || '';
        const unitCost   = parseFloat(document.getElementById('qaUnitCost')?.value)||0;

        if(!wid)  { toast('Choisissez un magasin','error'); return; }
        if(qty<=0){ toast('Quantité > 0','error'); return; }

        // Validation traçabilité
        const ctx = this._qaCtx || {};
        if(ctx.needsLot && !lotNumber)  { toast('Numéro de lot obligatoire','error'); return; }
        if(ctx.needsExp && !expiryDate) { toast('Date de péremption obligatoire','error'); return; }

        try {
            await Api.mvAdd({
                productId:   parseInt(pid),
                warehouseId: parseInt(wid),
                type,
                quantity: qty,
                unit,
                lotNumber:   lotNumber  || null,
                expiryDate:  expiryDate || null,
                unitCost: type==='add' ? unitCost : 0,
                reason:'',
                site:     (document.getElementById('qaSite')||{}).value || '',
                operator: '',
                note:     (document.getElementById('qaNote')||{}).value || ''
            });
            toast(type==='add' ? 'Ajouté !' : 'Sortie enregistrée', 'success');
            Modal.close();
            this.load();
        } catch(e){ toast(e.message,'error'); }
    },


    _getSupplierLinkOptions(selectedId=''){
        return (this.suppliers || []).map(s => `<option value="${s.id}" ${String(selectedId)===String(s.id)?'selected':''}>${this._esc ? this._esc(s.name) : s.name}</option>`).join('');
    },

    _normalizeSupplierLinks(product){
        const raw = Array.isArray(product?.supplierLinks) ? product.supplierLinks : [];
        const rows = raw.map((row, idx) => ({
            _id: row._id || ('sl_' + idx + '_' + Date.now()),
            supplierId: String(row?.supplierId || ''),
            supplierSku: String(row?.supplierSku || ''),
            purchasePrice: Number(row?.purchasePrice || 0),
            packagingLabel: String(row?.packagingLabel || ''),
            packagingQty: Number(row?.packagingQty || 0),
            minOrderQty: Number(row?.minOrderQty || 0),
            leadTimeDays: Number(row?.leadTimeDays || 0),
            discountRate: Number(row?.discountRate || 0),
            preferred: !!row?.preferred,
            allowedWarehouses: Array.isArray(row?.allowedWarehouses) ? row.allowedWarehouses.map(String) : []
        })).filter(row => row.supplierId || row.supplierSku || row.purchasePrice > 0 || row.packagingLabel);

        if(!rows.length && product?.supplier){
            rows.push({
                _id: 'sl_legacy_' + Date.now(),
                supplierId: String(product.supplier || ''),
                supplierSku: '',
                purchasePrice: Number(product?.cost || product?.cump || 0),
                packagingLabel: String(product?.unitType || product?.packBaseUnit || ''),
                packagingQty: 1,
                minOrderQty: 0,
                leadTimeDays: 0,
                discountRate: 0,
                preferred: true,
                allowedWarehouses: Array.isArray(product?.allowedWarehouses) ? product.allowedWarehouses.map(String) : []
            });
        }
        return rows;
    },

    _supplierPackagingLabels(){
        return [
            { value:'', label:'— Unité de base —' },
            { value:'piece', label:'pièce(s)' },
            { value:'carton', label:'carton(s)' },
            { value:'palette', label:'palette(s)' },
            { value:'bidon', label:'bidon(s)' },
            { value:'fût', label:'fût(s)' },
            { value:'aérosol', label:'aérosol(s)' },
            { value:'sac', label:'sac(s)' },
            { value:'seau', label:'seau(x)' },
            { value:'rouleau', label:'rouleau(x)' },
            { value:'bobine', label:'bobine(s)' },
            { value:'tube', label:'tube(s)' },
            { value:'boîte', label:'boîte(s)' },
            { value:'paquet', label:'paquet(s)' },
        ];
    },

    _renderSupplierLinksEditor(product={}){
        const host = document.getElementById('pmSupplierLinksList');
        if(!host) return;
        const rows = this._normalizeSupplierLinks(product);
        host.innerHTML = '';
        if(!rows.length){
            host.innerHTML = `<div class="empty-state" style="padding:12px;border:1px dashed var(--border);border-radius:10px;color:var(--text-muted)">Aucune offre fournisseur configurée. Ajoutez au moins une offre pour remonter automatiquement le prix, la référence fournisseur et le conditionnement dans les bons de commande.</div>`;
            this._syncPreferredSupplierFromLinks();
            return;
        }
        const packagingOptions = this._supplierPackagingLabels().map(opt => `<option value="${opt.value}">${opt.label}</option>`).join('');
        const whOptions = (this.whs || []).map(w => `<option value="${w.id}">${this._esc ? this._esc(w.name) : w.name}</option>`).join('');
        rows.forEach((row, idx) => {
            const item = document.createElement('div');
            item.className = 'pm-supplier-link-row';
            item.dataset.rowId = row._id;
            item.style.cssText = 'border:1px solid var(--border);border-radius:12px;padding:12px;margin-bottom:10px;background:var(--bg-card)';
            item.innerHTML = `
                <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap">
                    <strong>Offre fournisseur ${idx + 1}</strong>
                    <button type="button" class="btn btn-sm btn-ghost pm-sl-remove">Retirer</button>
                </div>
                <div style="display:grid;grid-template-columns:1.2fr 1fr 1fr;gap:10px">
                    <div>
                        <label class="lbl">Fournisseur</label>
                        <select class="sel pm-sl-supplier"><option value="">— Choisir —</option>${this._getSupplierLinkOptions(row.supplierId)}</select>
                    </div>
                    <div>
                        <label class="lbl">Réf. fournisseur</label>
                        <input type="text" class="inp pm-sl-sku" value="${this._esc ? this._esc(row.supplierSku) : row.supplierSku}" placeholder="Référence article">
                    </div>
                    <div>
                        <label class="lbl">Prix d'achat (${Locale.currencyCode()})</label>
                        <input type="number" class="inp pm-sl-price" min="0" step="0.01" value="${row.purchasePrice || ''}" placeholder="0.00">
                    </div>
                </div>
                <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px;margin-top:10px">
                    <div>
                        <label class="lbl">Conditionnement</label>
                        <select class="sel pm-sl-packaging">${packagingOptions}</select>
                    </div>
                    <div>
                        <label class="lbl">Qté / cond.</label>
                        <input type="number" class="inp pm-sl-packqty" min="0" step="0.01" value="${row.packagingQty || ''}" placeholder="1">
                    </div>
                    <div>
                        <label class="lbl">Qté mini</label>
                        <input type="number" class="inp pm-sl-minqty" min="0" step="0.01" value="${row.minOrderQty || ''}" placeholder="0">
                    </div>
                    <div>
                        <label class="lbl">Délai (jours)</label>
                        <input type="number" class="inp pm-sl-lead" min="0" step="1" value="${row.leadTimeDays || ''}" placeholder="0">
                    </div>
                </div>
                <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px">
                    <div>
                        <label class="lbl">Remise (%)</label>
                        <input type="number" class="inp pm-sl-discount" min="0" step="0.01" value="${row.discountRate || ''}" placeholder="0">
                    </div>
                    <div>
                        <label class="lbl">Magasins autorisés (optionnel)</label>
                        <select class="sel pm-sl-warehouses" multiple size="3">${whOptions}</select>
                    </div>
                </div>
                <div style="display:flex;align-items:center;gap:8px;margin-top:10px">
                    <input type="radio" name="pmPreferredSupplierLink" class="pm-sl-preferred" ${row.preferred ? 'checked' : ''}>
                    <label style="margin:0">Fournisseur préféré pour les commandes</label>
                </div>
                <div class="pm-sl-summary" style="margin-top:10px;font-size:12px;color:var(--text-muted)"></div>
            `;
            host.appendChild(item);
            item.querySelector('.pm-sl-packaging').value = row.packagingLabel || '';
            const msel = item.querySelector('.pm-sl-warehouses');
            Array.from(msel.options).forEach(opt => { opt.selected = row.allowedWarehouses.includes(String(opt.value)); });
            item.querySelectorAll('input,select').forEach(el => {
                el.addEventListener('input', () => this._refreshSupplierLinkRow(item));
                el.addEventListener('change', () => this._refreshSupplierLinkRow(item));
            });
            item.querySelector('.pm-sl-remove')?.addEventListener('click', () => {
                item.remove();
                if(!host.children.length) this._renderSupplierLinksEditor({});
                else this._syncPreferredSupplierFromLinks();
            });
            this._refreshSupplierLinkRow(item);
        });
        this._syncPreferredSupplierFromLinks();
    },

    _refreshSupplierLinkRow(rowEl){
        if(!rowEl) return;
        const supplierId = rowEl.querySelector('.pm-sl-supplier')?.value || '';
        const sku = rowEl.querySelector('.pm-sl-sku')?.value?.trim() || '';
        const price = parseFloat(rowEl.querySelector('.pm-sl-price')?.value || 0) || 0;
        const packagingLabel = rowEl.querySelector('.pm-sl-packaging')?.value || '';
        const packagingQty = parseFloat(rowEl.querySelector('.pm-sl-packqty')?.value || 0) || 0;
        const minQty = parseFloat(rowEl.querySelector('.pm-sl-minqty')?.value || 0) || 0;
        const lead = parseInt(rowEl.querySelector('.pm-sl-lead')?.value || 0, 10) || 0;
        const supplier = (this.suppliers || []).find(s => String(s.id) === String(supplierId));
        const summary = [];
        if(supplier) summary.push(`<strong>${this._esc ? this._esc(supplier.name) : supplier.name}</strong>`);
        if(sku) summary.push(`réf. ${this._esc ? this._esc(sku) : sku}`);
        if(price > 0) summary.push(`${Locale.formatMoney(price)} HT`);
        if(packagingLabel) summary.push(`conditionnement : 1 ${packagingLabel}${packagingQty > 0 ? ' = ' + packagingQty + ' unité(s)' : ''}`);
        if(minQty > 0) summary.push(`mini ${minQty}`);
        if(lead > 0) summary.push(`délai ${lead} j`);
        rowEl.querySelector('.pm-sl-summary').innerHTML = summary.length ? summary.join(' · ') : 'Complétez l’offre fournisseur.';
        this._syncPreferredSupplierFromLinks();
    },

    _syncPreferredSupplierFromLinks(){
        const rows = Array.from(document.querySelectorAll('#pmSupplierLinksList .pm-supplier-link-row'));
        if(!rows.length) return;
        let preferredRow = rows.find(row => row.querySelector('.pm-sl-preferred')?.checked) || rows[0];
        if(preferredRow && !preferredRow.querySelector('.pm-sl-preferred')?.checked){
            preferredRow.querySelector('.pm-sl-preferred').checked = true;
        }
        const mainSupplier = document.getElementById('pmSupp');
        if(mainSupplier){
            const supplierId = preferredRow.querySelector('.pm-sl-supplier')?.value || '';
            if(supplierId) mainSupplier.value = supplierId;
        }
    },

    _collectSupplierLinks(){
        const rows = Array.from(document.querySelectorAll('#pmSupplierLinksList .pm-supplier-link-row'));
        return rows.map(row => ({
            supplierId: row.querySelector('.pm-sl-supplier')?.value || '',
            supplierSku: row.querySelector('.pm-sl-sku')?.value?.trim() || '',
            purchasePrice: parseFloat(row.querySelector('.pm-sl-price')?.value || 0) || 0,
            packagingLabel: row.querySelector('.pm-sl-packaging')?.value || '',
            packagingQty: parseFloat(row.querySelector('.pm-sl-packqty')?.value || 0) || 0,
            minOrderQty: parseFloat(row.querySelector('.pm-sl-minqty')?.value || 0) || 0,
            leadTimeDays: parseInt(row.querySelector('.pm-sl-lead')?.value || 0, 10) || 0,
            discountRate: parseFloat(row.querySelector('.pm-sl-discount')?.value || 0) || 0,
            preferred: !!row.querySelector('.pm-sl-preferred')?.checked,
            allowedWarehouses: Array.from(row.querySelector('.pm-sl-warehouses')?.selectedOptions || []).map(opt => String(opt.value))
        })).filter(row => row.supplierId || row.supplierSku || row.purchasePrice > 0 || row.packagingLabel);
    },

    _appendEmptySupplierLink(){
        const current = { supplierLinks: this._collectSupplierLinks() };
        current.supplier = document.getElementById('pmSupp')?.value || '';
        current.supplierLinks.push({
            supplierId: current.supplier || '',
            supplierSku: '',
            purchasePrice: 0,
            packagingLabel: '',
            packagingQty: 1,
            minOrderQty: 0,
            leadTimeDays: 0,
            discountRate: 0,
            preferred: current.supplierLinks.length === 0,
            allowedWarehouses: []
        });
        this._renderSupplierLinksEditor(current);
    },

    async openModal(pid){
        const isAdmin = (window.SM_ROLE === 'admin');
        
        // ══════════════════════════════════════════════════════════════
        // MODE CONSULTATION (non-admin) - Affichage lecture seule
        // ══════════════════════════════════════════════════════════════
        if(!isAdmin && pid){
            return this._openViewModal(pid);
        }
        
        // ══════════════════════════════════════════════════════════════
        // MODE EDITION (admin) - Formulaire complet
        // ══════════════════════════════════════════════════════════════
        const whs = this.whs;
        const suppliers = this.suppliers;
        const whOpts = whs.map(w=>`<option value="${w.id}">${w.name}</option>`).join('');
        const catOpts = this.cats.map(c=>`<option value="${c}">${c}</option>`).join('');
        const suppOpts = suppliers.map(s=>`<option value="${s.id}">${s.name}</option>`).join('');
        let p = {};
        let currentStock = [];
        if(pid) { 
            const all = await Api.products(); 
            p = all.find(x=>String(x.id)===String(pid))||{}; 
            // Charger le stock actuel à la demande pour éviter l'affichage obsolète en modification.
            try {
                const freshStock = await Api.stock(pid);
                currentStock = Array.isArray(freshStock) ? freshStock : (Array.isArray(freshStock?.stock) ? freshStock.stock : []);
            } catch(e) {
                currentStock = (this.stock || []).filter(s => String(s.productId) === String(pid));
            }
        }
        const isEdit = !!pid;
        const stockBaseUnit = this._getBaseUnit(p);
        const unitLabel = stockBaseUnit === 'piece' ? 'u' : stockBaseUnit;
        
        // Construire l'affichage du stock actuel
        let stockInfoHtml = '';
        if(isEdit && currentStock.length > 0) {
            const totalQty = currentStock.reduce((sum, s) => sum + (parseFloat(s.quantity) || 0), 0);
            const baseCost = getBaseCost(p);
            const totalValue = totalQty * baseCost;
            
            const stockLines = currentStock.map(s => {
                const wh = whs.find(w => String(w.id) === String(s.warehouseId));
                const qty = parseFloat(s.quantity) || 0;
                const locTxt = [s.picking, s.location, s.rack, s.shelf].filter(Boolean).join(' · ');
                return `<div class="pm-stock-line">
                    <span>${this._esc(wh ? wh.name : 'Magasin ?')}${locTxt ? '<br><small>📍 '+this._esc(locTxt)+'</small>' : ''}</span>
                    <strong>${qty} ${unitLabel}</strong>
                </div>`;
            }).join('');
            
            stockInfoHtml = `
                <section class="pm-stock-summary" aria-labelledby="pmStockTitle">
                    <h4 id="pmStockTitle">📦 Stock actuel</h4>
                    <div class="pm-stock-card">
                        ${stockLines}
                        <div class="pm-stock-total">
                            <strong>Total</strong>
                            <strong>${totalQty} ${unitLabel} · ${Locale.formatMoney(totalValue)}</strong>
                        </div>
                    </div>
                    <p class="pm-help">💡 Pour modifier le stock, utilisez les mouvements (entrée/sortie).</p>
                </section>
            `;
        } else if(isEdit) {
            stockInfoHtml = `
                <section class="pm-stock-summary" aria-labelledby="pmStockTitle">
                    <h4 id="pmStockTitle">📦 Stock actuel</h4>
                    <div class="pm-stock-card pm-stock-card--empty">Aucun stock enregistré pour ce produit.</div>
                </section>
            `;
        }
        
        Modal.show(isEdit?'Modifier produit':'Nouveau produit', `
            ${stockInfoHtml}
            <div class="pm-form-guide" role="note">
                <strong>Création / modification produit</strong>
                <span>Complétez d'abord les champs essentiels. Les réglages techniques sont regroupés dans les options avancées.</span>
            </div>
            <div class="pm-fullgrid" role="form" aria-label="Fiche produit">
            <!-- ══ COLONNE GAUCHE ══ -->
            <div class="pm-col pm-col-left">
                <section class="pm-panel pm-panel--essential" aria-labelledby="pmIdentityTitle"><h4 id="pmIdentityTitle">🏷️ Identité produit</h4>
                <label class="lbl" for="pmName">Nom <span aria-hidden="true">*</span></label>
                <input type="text" class="inp" id="pmName" value="${p.name||''}" required aria-required="true" autocomplete="off" placeholder="Ex. Panneau danger A14">
                <label class="lbl" for="pmSKU">Référence (SKU)</label>
                <div style="position:relative">
                    <input type="text" class="inp" id="pmSKU" value="${p.sku||''}" placeholder="Auto-généré si vide" style="padding-right:36px">
                    <span id="pmSKUStatus" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:16px;display:none"></span>
                </div>
                <div id="pmSKUHint" style="font-size:11px;margin-top:3px;display:none"></div>
                <label class="lbl" for="pmBarcode">Code-barres (EAN/QR)</label>
                <input type="text" class="inp" id="pmBarcode" value="${p.barcode||''}" inputmode="numeric" autocomplete="off" placeholder="Scanner ou saisir le code">
                <label class="lbl" for="pmEanInput">Codes EAN supplémentaires</label>
                <div class="pm-inline-add"><input type="text" class="inp" id="pmEanInput" placeholder="Ajouter un EAN…" inputmode="numeric" autocomplete="off"><button type="button" class="btn btn-sm btn-secondary pm-touch-btn" id="pmEanAddBtn" aria-label="Ajouter le code EAN supplémentaire">+</button></div>
                <div class="pm-ean-tags" id="pmEanTags"></div>
                <input type="hidden" id="pmEansHidden" value='${JSON.stringify(this._sanitizeEanList(p.eans || []))}'>
                <label class="lbl" for="pmDesc">Description</label>
                <textarea class="inp" id="pmDesc" rows="3" placeholder="Description courte du produit…">${p.description||''}</textarea></section>

                <div class="pm-sep"></div>
                <section class="pm-panel" aria-labelledby="pmClassTitle"><h4 id="pmClassTitle">🏢 Classification</h4>
                <label class="lbl" for="pmWarehouse">Magasin <span aria-hidden="true">*</span></label>
                <select class="sel" id="pmWarehouse" required aria-required="true"><option value="">— Choisir un magasin —</option>${whOpts}</select>
                <label class="lbl" for="pmCat">Catégorie</label>
                <select class="sel" id="pmCat"><option value="">— Choisir d'abord un magasin —</option></select>
                <label class="lbl" for="pmSupp">Fournisseur principal</label>
                <select class="sel" id="pmSupp"><option value="">— Aucun —</option>${suppOpts}</select>
                <div id="pmBrandContainer"></div></section>

                <div class="pm-sep"></div>
                <section class="pm-panel" aria-labelledby="pmStorageTitle"><h4 id="pmStorageTitle">📍 Emplacement stock</h4>
                <label class="lbl" for="pmLocationId">Emplacement enregistré</label>
                <select class="sel" id="pmLocationId"><option value="">— Choisir un emplacement —</option></select>
                <div id="pmLocHint" class="pm-help">Choisissez d'abord un magasin.</div>
                <details class="pm-subdetails"><summary>Saisie manuelle du rangement</summary><p class="pm-help">À utiliser uniquement si aucun emplacement enregistré ne correspond.</p>
                <div class="pm-manual-grid">
                    <div><label class="lbl" for="pmPicking">Pick'in / Bac</label><input type="text" class="inp" id="pmPicking" value="" autocomplete="off"></div>
                    <div><label class="lbl" for="pmLocation">Lieu</label><input type="text" class="inp" id="pmLocation" value="" autocomplete="off"></div>
                    <div><label class="lbl" for="pmRack">Rack</label><input type="text" class="inp" id="pmRack" value="" autocomplete="off"></div>
                    <div><label class="lbl" for="pmShelfLoc">Étagère</label><input type="text" class="inp" id="pmShelfLoc" value="" autocomplete="off"></div>
                </div></details></section>

                <section class="pm-panel" aria-labelledby="pmImageTitle"><h4 id="pmImageTitle">🖼️ Image</h4>
                <div id="pmImgZone" class="pm-img-zone ${p.image ? 'pm-img-zone--has-img' : ''}">
                    <div class="pm-img-preview" id="pmImgPreview">
                        ${p.image
                            ? '<img src="'+this._esc(this._productImageSrc(p))+'" id="pmImgPreviewImg" alt="aperçu">'
                            : '<div class="pm-img-placeholder" id="pmImgPlaceholder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="40" height="40" opacity=".35"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg><span>Glissez ou collez une image ici</span></div>'
                        }
                    </div>
                    <div class="pm-img-actions">
                        <label class="pm-img-btn pm-img-btn--primary" for="pmImg" title="Parcourir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> Parcourir</label>
                        <input type="file" id="pmImg" accept="image/*" style="display:none">
                        <button type="button" class="pm-img-btn" id="pmImgUrlBtn" title="URL"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg> URL</button>
                        ${p.image ? '<button type="button" class="pm-img-btn pm-img-btn--danger" id="pmImgClear" title="Supprimer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg> Supprimer</button>' : ''}
                    </div>
                    <div class="pm-img-url-row" id="pmImgUrlRow" style="display:none"><input type="url" class="inp" id="pmImgUrl" placeholder="https://exemple.com/image.png"><button type="button" class="pm-img-btn pm-img-btn--primary" id="pmImgUrlOk">OK</button><button type="button" class="pm-img-btn" id="pmImgUrlCancel">✕</button></div>
                    <div class="pm-img-filename" id="pmImgFilename" style="display:none"></div>
                </div>
                ${p.datasheetUrl?'<a href="'+p.datasheetUrl+'" target="_blank" class="btn btn-sm btn-secondary" style="margin-top:6px">📄 Voir FT</a>':''}
                ${p.msdsUrl?'<a href="'+p.msdsUrl+'" target="_blank" class="btn btn-sm btn-secondary" style="margin-top:4px">⚠️ Voir FDS</a>':''}
                </section>
            </div>

            <!-- ══ COLONNE DROITE ══ -->
            <div class="pm-col pm-col-right">
                <section class="pm-panel pm-panel--essential" aria-labelledby="pmPackTitle"><h4 id="pmPackTitle">📦 Conditionnement</h4>
                <label class="lbl" for="pmBaseUnit">Unité de base</label>
                <select class="sel" id="pmBaseUnit">
                    <optgroup label="Unités de mesure">
                    <option value="piece">pièce</option>
                    <option value="ml">mètre linéaire (ml)</option>
                    <option value="litre">litre</option>
                    <option value="kg">kg</option>
                    <option value="m">m</option>
                    <option value="m2">m²</option>
                    <option value="m3">m³</option>
                    </optgroup>
                    <optgroup label="Conditionnements">
                    <option value="carton">carton</option>
                    <option value="palette">palette</option>
                    <option value="bidon">bidon</option>
                    <option value="fut">fût</option>
                    <option value="aerosol">aérosol</option>
                    <option value="sac">sac</option>
                    <option value="seau">seau</option>
                    <option value="rouleau">rouleau</option>
                    <option value="bobine">bobine</option>
                    <option value="tube">tube</option>
                    <option value="boite">boîte</option>
                    <option value="paquet">paquet</option>
                    </optgroup>
                </select>
                <div id="pmCondBlock"></div>
                <input type="hidden" id="pmUsePack" value="1">
                <input type="hidden" id="pmDecompSub" value="${p.decompSub?'1':'0'}">
                <input type="hidden" id="pmCartonQty" value="${p.packCartonQty??p.unitsPerBox??0}">
                <input type="hidden" id="pmPaletteQty" value="${p.packPaletteQty??0}">
                <input type="hidden" id="pmBidonQty" value="${p.packBidonQty??p.volume??0}">
                <input type="hidden" id="pmFutQty" value="${p.packFutQty??0}">
                <input type="hidden" id="pmAerosolQty" value="${p.packAerosolQty??0}">
                <input type="hidden" id="pmSacQty" value="${p.packSacQty??0}">
                <input type="hidden" id="pmSeauQty" value="${p.packSeauQty??0}">
                <input type="hidden" id="pmRouleauQty" value="${p.packRouleauQty??0}">
                <input type="hidden" id="pmBobineQty" value="${p.packBobineQty??0}">
                <input type="hidden" id="pmTubeQty" value="${p.packTubeQty??0}">
                <input type="hidden" id="pmBoiteQty" value="${p.packBoiteQty??0}">
                <input type="hidden" id="pmPaquetQty" value="${p.packPaquetQty??0}">
                <div id="pmPackFields" style="display:none"></div>

                <div class="pm-sep"></div>
                </section>

                <section class="pm-panel" aria-labelledby="pmPriceTitle"><h4 id="pmPriceTitle">💰 Prix</h4>
                <label class="lbl" for="pmCost">Prix d'achat unitaire (${Locale.currencyCode()})</label>
                <input type="number" class="inp" id="pmCost" min="0" step="0.01" value="${p.cost||0}">
                ${isEdit ? '<label class="lbl">CUMP (coût moyen pondéré)</label><div class="pm-cump-display" aria-live="polite">'+Locale.formatMoney(parseFloat(p.cump||p.cost||0), {minimumFractionDigits:4, maximumFractionDigits:4})+'</div>' : ''}</section>

                <div class="pm-sep"></div>
                <details class="pm-advanced"><summary><span>Options avancées du produit</span><small>Déclinaisons, tailles, traçabilité, seuils et documents</small></summary><div class="pm-advanced-body"><section class="pm-panel" aria-labelledby="pmVariantTitle"><h4 id="pmVariantTitle">🧬 Déclinaisons génériques</h4>
                <div style="background:var(--bg-elevated,#f8fafc);border:1px solid var(--border,#e2e8f0);border-radius:10px;padding:10px;margin-bottom:12px">
                    <label class="chk" style="margin-bottom:8px"><input type="checkbox" id="pmIsVariantParent" ${p.isVariantParent?'checked':''}> Ce produit sert de parent à des déclinaisons</label>
                    <label class="lbl">Famille de déclinaison</label>
                    <select class="sel" id="pmVariantFamily"><option value="">— Aucune —</option>${(this.variantFamilies||[]).map(f=>`<option value="${this._esc(f.id)}" ${p.variantFamily===f.id?'selected':''}>${this._esc(f.label)}${f.sourceType==='custom'?' · perso':' · officiel'}</option>`).join('')}</select>
                    <label class="lbl">Libellé de cette variante</label>
                    <input type="text" class="inp" id="pmVariantLabel" value="${this._esc(p.variantLabel||'')}" placeholder="Ex. Ø650 mm, 500 × 700 mm, Taille L">
                    <input type="hidden" id="pmVariantAttributes" value='${this._esc(JSON.stringify(p.variantAttributes||{}))}'>
                    <div id="pmVariantSummary" style="font-size:12px;color:var(--text-muted);margin-top:6px"></div>
                    ${isEdit ? '<button type="button" class="btn btn-primary btn-sm" id="pmOpenVariantWizard" style="margin-top:8px">🧬 Dupliquer avec déclinaisons</button>' : '<small style="color:var(--text-muted)">Enregistrez le produit pour générer des déclinaisons stockables.</small>'}
                </div>

                <div class="pm-sep"></div>
                </section><section class="pm-panel" aria-labelledby="pmSizeSectionTitle"><h4 id="pmSizeSectionTitle">👕 Tailles & Déclinaisons</h4>
                <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
                    <span style="font-size:12px;color:var(--text-muted)">👕 Vêtement</span>
                    <label style="position:relative;display:inline-block;width:44px;height:24px;margin:0;cursor:pointer">
                        <input type="checkbox" id="pmSizeModeToggle" style="opacity:0;width:0;height:0">
                        <span style="position:absolute;inset:0;background:var(--border);border-radius:12px;transition:.3s"></span>
                        <span id="pmSizeModeKnob" style="position:absolute;top:2px;left:2px;width:20px;height:20px;background:#fff;border-radius:50%;transition:.3s;box-shadow:0 1px 3px rgba(0,0,0,.2)"></span>
                    </label>
                    <span style="font-size:12px;color:var(--text-muted)">🦺 EPI</span>
                </div>
                <label class="lbl">Type d'article</label>
                <select class="sel" id="pmSizeType">
                    <option value="none">Aucun (produit standard)</option>
                </select>
                <div id="pmSizeNormWrap" style="display:none;margin-top:8px">
                    <label class="lbl">Norme de taille</label>
                    <select class="sel" id="pmSizeNorm">
                        <option value="eu">EU (Europe)</option>
                        <option value="uk">UK (Royaume-Uni)</option>
                        <option value="us">US (États-Unis)</option>
                    </select>
                </div>
                ${isEdit && p.size ? '<div style="margin-top:8px"><label class="lbl">Taille de ce produit</label><div style="font-weight:700;font-size:16px;color:var(--blue)">'+p.size+'</div></div>' : ''}
                <div id="pmSizesBlock" style="display:none;margin-top:10px">
                    <label class="lbl">Tailles à créer</label>
                    <div class="pm-sizes-grid" id="pmSizesGrid"></div>
                    <div id="pmSizesCount" style="font-size:12px;color:var(--text-muted);margin-top:6px"></div>
                    ${!isEdit ? '' : '<button type="button" class="btn btn-primary btn-sm" id="pmGenVariants" style="margin-top:8px">👕 Générer les déclinaisons</button>'}
                </div>

                <div class="pm-sep"></div>
                </section><section class="pm-panel" aria-labelledby="pmTraceTitle"><h4 id="pmTraceTitle">🔒 Traçabilité</h4>
                <label class="chk"><input type="checkbox" id="pmTrackLot" ${p.trackLot?'checked':''}> Numéro de lot obligatoire</label>
                <label class="chk"><input type="checkbox" id="pmTrackExp" ${p.trackExpiry?'checked':''}> Date de péremption / DLUO</label>
                <label class="lbl">Durée de vie (jours)</label>
                <input type="number" class="inp" id="pmShelf" min="0" step="1" value="${p.shelfLifeDays??''}" placeholder="Optionnel">

                <div class="pm-sep"></div>
                </section><section class="pm-panel" aria-labelledby="pmThresholdTitle"><h4 id="pmThresholdTitle">📊 Seuils</h4>
                <label class="lbl">Stock de sécurité (en unité de base)</label>
                <input type="number" class="inp" id="pmSafety" min="0" step="1" value="${p.safetyStock??''}" placeholder="Optionnel">

                <div class="pm-sep"></div>
                </section><section class="pm-panel" aria-labelledby="pmDocsTitle"><h4 id="pmDocsTitle">📄 Documents</h4>
                <label class="lbl" for="pmDatasheet">Fiche technique (URL)</label>
                <input type="url" class="inp" id="pmDatasheet" value="${p.datasheetUrl||''}" placeholder="https://...">
                <label class="lbl" for="pmMsds">Fiche de sécurité (URL)</label>
                <input type="url" class="inp" id="pmMsds" value="${p.msdsUrl||''}" placeholder="https://..."></section></div></details>

                ${!isEdit ? '<section class="pm-panel pm-panel--essential" aria-labelledby="pmInitTitle"><h4 id="pmInitTitle">📥 Stock initial</h4><p class="pm-help">Optionnel : laissez 0 si le produit sera entré plus tard par mouvement.</p><label class="lbl" for="pmInitQty">Quantité initiale</label><input type="number" class="inp" id="pmInitQty" min="0" step="0.01" value="0" inputmode="decimal"><div id="pmInitTrace" style="display:none"><label class="lbl" for="pmInitLot">Lot</label><input type="text" class="inp" id="pmInitLot" value=""><label class="lbl" for="pmInitExp">DLUO/DLC</label><input type="date" class="inp" id="pmInitExp" value=""></div></section>' : ''}
            </div>
            </div>
            <div id="pmSupplierLinksList" style="display:none"></div>
        `, [
            {label:'Annuler', cls:'btn-secondary', onclick:()=>Modal.close()},
            ...(isEdit ? [{label:'Localiser sur le plan', cls:'btn-secondary', onclick:()=>this.openProductPlan(pid)}] : []),
            {label:isEdit?'Sauvegarder':'Créer', cls:'btn-primary', onclick:()=>this.saveProduct(pid)},
            ...(isEdit ? [
                {label:'🧬 Décliner', cls:'btn-primary', onclick:()=>this.openVariantDuplicateModal(pid)},
                {label:'📋 Dupliquer', cls:'btn-ghost', onclick:()=>this.duplicateProduct(pid)},
                {label:'Supprimer', cls:'btn-danger', onclick:()=>this.delProduct(pid)}
            ] : [])
        ], isEdit?'info':'success');

        // ── Icône personnalisée du modal produit ────────────────────
        const modalIconEl = document.getElementById('modalIcon');
        if(modalIconEl){
            modalIconEl.innerHTML = isEdit
                ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'
                : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
            const hdr = modalIconEl.parentElement;
            if(hdr){
                hdr.style.background = isEdit ? 'var(--blue-pale, #dbeafe)' : 'var(--green-pale, #dcfce7)';
                const svg = modalIconEl.querySelector('svg');
                if(svg) svg.style.stroke = isEdit ? 'var(--blue, #3b82f6)' : 'var(--green, #22c55e)';
            }
        }

        // Préserver les offres fournisseurs existantes, même si le bloc avancé reste masqué.
        this._renderSupplierLinksEditor(p || {});

        // Init uploader image pro
        this._initImageUploader(p.image || '');

        // ── Vérification SKU en temps réel ──────────────────────────
        const skuInput  = document.getElementById('pmSKU');
        const skuStatus = document.getElementById('pmSKUStatus');
        const skuHint   = document.getElementById('pmSKUHint');
        if(skuInput && skuStatus && skuHint){
            let _skuTimer = null;
            skuInput.addEventListener('input', () => {
                clearTimeout(_skuTimer);
                const val = skuInput.value.trim();
                if(!val){ skuStatus.style.display='none'; skuHint.style.display='none'; skuInput.style.borderColor=''; return; }
                _skuTimer = setTimeout(async () => {
                    try {
                        const allProds = await Api.products();
                        const existing = allProds.find(x => 
                            x.sku && x.sku.toLowerCase() === val.toLowerCase() && String(x.id) !== String(pid)
                        );
                        if(existing){
                            skuStatus.textContent = '⚠️';
                            skuStatus.style.display = '';
                            skuHint.innerHTML = `<span style="color:var(--red)">Ce SKU est déjà utilisé par <strong>${existing.name}</strong></span>`;
                            skuHint.style.display = 'block';
                            skuInput.style.borderColor = 'var(--red)';
                        } else {
                            skuStatus.textContent = '✓';
                            skuStatus.style.display = '';
                            skuHint.innerHTML = '<span style="color:var(--green)">SKU disponible</span>';
                            skuHint.style.display = 'block';
                            skuInput.style.borderColor = 'var(--green)';
                            // Reset après 2s
                            setTimeout(() => { 
                                if(skuInput.value.trim() === val){ 
                                    skuHint.style.display='none'; 
                                    skuInput.style.borderColor=''; 
                                    skuStatus.style.display='none'; 
                                }
                            }, 2000);
                        }
                    } catch(e){}
                }, 800);
            });
        }

        // Inject CSS uploader image (une seule fois)
        if(!document.getElementById('pm-img-css')){
            const s = document.createElement('style');
            s.id = 'pm-img-css';
            s.textContent = `
.pm-img-zone{border:2px dashed var(--border,#e2e8f0);border-radius:12px;overflow:hidden;background:var(--bg-elevated,#f8fafc);transition:border-color .2s,background .2s;cursor:default}
.pm-img-zone--drag{border-color:var(--blue,#3b82f6);background:var(--blue-pale,#eff6ff)}
.pm-img-zone--has-img{border-style:solid;border-color:var(--border,#e2e8f0)}
.pm-img-preview{width:100%;min-height:140px;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;background:var(--bg-elevated,#f8fafc)}
.pm-img-preview img{width:100%;max-height:220px;object-fit:contain;display:block;transition:opacity .2s}
.pm-img-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 20px;color:var(--text-muted,#94a3b8);font-size:13px;text-align:center;user-select:none;width:100%;min-height:130px}
.pm-img-actions{display:flex;gap:6px;flex-wrap:wrap;padding:10px 12px;border-top:1px solid var(--border,#e2e8f0);background:var(--bg-card,#fff)}
.pm-img-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:13px;font-weight:500;border:1.5px solid var(--border,#e2e8f0);background:var(--bg-card,#fff);color:var(--text-primary,#1e293b);cursor:pointer;transition:all .15s;white-space:nowrap;line-height:1}
.pm-img-btn:hover{background:var(--bg-elevated,#f8fafc);border-color:var(--text-muted,#94a3b8)}
.pm-img-btn--primary{background:var(--blue,#3b82f6);border-color:var(--blue,#3b82f6);color:#fff}
.pm-img-btn--primary:hover{background:var(--blue-dark,#2563eb);border-color:var(--blue-dark,#2563eb)}
.pm-img-btn--danger{background:#fff;border-color:var(--danger,#ef4444);color:var(--danger,#ef4444)}
.pm-img-btn--danger:hover{background:var(--red-pale,#fee2e2)}
.pm-img-url-row{display:flex;gap:6px;align-items:center;padding:8px 12px;border-top:1px solid var(--border,#e2e8f0);background:var(--bg-card,#fff)}
.pm-img-url-row .inp{flex:1;margin:0;font-size:13px}
.pm-img-filename{font-size:11.5px;color:var(--text-muted,#64748b);padding:4px 12px 6px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;background:var(--bg-card,#fff)}
`;
            document.head.appendChild(s);
        }

        // CASCADE FIABLE : magasin → catégories → emplacements → rangement stock
        const whSelect = document.getElementById('pmWarehouse');
        const catSelect = document.getElementById('pmCat');
        const locSelect = document.getElementById('pmLocationId');

        const updateCategoriesForWarehouse = async (selectedCategory='') => {
            const selectedWhId = parseInt(whSelect?.value || '0', 10);
            if(!catSelect) return;
            if(!selectedWhId) {
                catSelect.innerHTML = '<option value="">— Choisir magasin d’abord —</option>';
                return;
            }
            try {
                const data = await Api.warehouseCategories(selectedWhId);
                const categories = Array.isArray(data) ? data : (data?.categories || []);
                if(categories.length > 0) {
                    catSelect.innerHTML = '<option value="">— Aucune —</option>' +
                        categories.map(c => `<option value="${this._esc(c)}">${this._esc(c)}</option>`).join('');
                } else {
                    catSelect.innerHTML = '<option value="">— Aucune catégorie dans ce magasin —</option>';
                }
                if(selectedCategory) catSelect.value = selectedCategory;
            } catch(e) {
                console.error('Error loading categories:', e);
                catSelect.innerHTML = '<option value="">— Erreur chargement —</option>';
            }
        };

        const loadWarehouseContext = async (warehouseId, opts={}) => {
            const wid = parseInt(warehouseId || '0', 10);
            if(whSelect && wid) whSelect.value = String(wid);
            await updateCategoriesForWarehouse(opts.category || '');
            await this._populateLocationSelect(wid, opts.locationId || '');
        };

        const primaryWarehouseId = isEdit ? this._getPrimaryWarehouseId(p) : 0;
        if(primaryWarehouseId){
            const stockRow = await this._getStockRowForWarehouse(pid, primaryWarehouseId);
            await loadWarehouseContext(primaryWarehouseId, {
                category: p.category || '',
                locationId: stockRow?.locationId || ''
            });
            if(stockRow) this._applyStockLocationToForm(stockRow);
        } else if(p.category) {
            await updateCategoriesForWarehouse(p.category);
        }

        if(locSelect) {
            locSelect.onchange = () => {
                const opt = locSelect.options[locSelect.selectedIndex];
                if(!opt || !opt.value) return;
                const code  = opt.getAttribute('data-code')  || '';
                const label = opt.getAttribute('data-label') || '';
                const zone  = opt.getAttribute('data-zone')  || '';
                const elPick = document.getElementById('pmPicking');
                const elLoc  = document.getElementById('pmLocation');
                const elRack = document.getElementById('pmRack');
                if(elPick && !elPick.value) elPick.value = code;
                if(elLoc  && !elLoc.value)  elLoc.value  = label || zone || code;
                if(elRack && !elRack.value) elRack.value = zone;
            };
        }

        if(whSelect) {
            whSelect.onchange = async () => {
                await loadWarehouseContext(whSelect.value, { category: '' });
                this._applyStockLocationToForm({});
            };
        }

        // Brand cascade logic
        const updateBrandField = async () => {
            const suppSelect = document.getElementById('pmSupp');
            const suppId = parseInt(suppSelect?.value || '0', 10);
            const container = document.getElementById('pmBrandWrap')
                || document.getElementById('pmBrandContainer')
                || (document.getElementById('pmBrand') ? document.getElementById('pmBrand').parentElement : null);
            if (!container) return;

            // Always use a select (as requested). If supplier has no linked brands, show all brands.
            const allBrands = await Api.brands();
            const supplier = (this.suppliers || []).find(s => String(s.id) === String(suppId));

            let linkedBrandIds = [];
            const raw = supplier?.linkedBrands;
            if (Array.isArray(raw)) {
                linkedBrandIds = raw.map(x => parseInt(x, 10)).filter(n => Number.isFinite(n));
            } else if (typeof raw === 'string' && raw.trim()) {
                try {
                    const parsed = JSON.parse(raw);
                    if (Array.isArray(parsed)) linkedBrandIds = parsed.map(x => parseInt(x, 10)).filter(n => Number.isFinite(n));
                } catch (e) { /* ignore */ }
            }
            const brandsToShow = linkedBrandIds.length
                ? allBrands.filter(b => linkedBrandIds.includes(b.id))
                : allBrands;

            const currentBrand = (document.getElementById('pmBrand')?.value || p.brand || '').trim();

            const h = (v) => String(v ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');

            // Build select + optional "Autre" input.
            container.innerHTML = `
                <label class="lbl">Marque</label>
                <select id="pmBrand" class="sel">
                    <option value="">— Aucune —</option>
                    ${brandsToShow.map(b => `<option value="${h(b.name)}">${h(b.name)}</option>`).join('')}
                    <option value="__other__">Autre (saisie libre)…</option>
                </select>
                <input id="pmBrandOther" class="inp" style="display:none;margin-top:8px" placeholder="Saisir une marque…">
            `;

            const brandSel = document.getElementById('pmBrand');
            const brandOther = document.getElementById('pmBrandOther');

            const applyValue = (val) => {
                const exists = [...brandSel.options].some(o => o.value === val);
                if (val && exists) {
                    brandSel.value = val;
                    brandOther.style.display = 'none';
                    brandOther.value = '';
                } else if (val) {
                    brandSel.value = '__other__';
                    brandOther.style.display = '';
                    brandOther.value = val;
                } else {
                    brandSel.value = '';
                    brandOther.style.display = 'none';
                    brandOther.value = '';
                }
            };

            brandSel.addEventListener('change', () => {
                if (brandSel.value === '__other__') {
                    brandOther.style.display = '';
                    brandOther.focus();
                } else {
                    brandOther.style.display = 'none';
                    brandOther.value = '';
                }
            });

            applyValue(currentBrand);
        };
                    
                    // Set supplier if editing
        if(p.supplier) {
            document.getElementById('pmSupp').value = p.supplier;
        }
        
        // Listen to supplier changes
        document.getElementById('pmSupp').onchange = updateBrandField;
        
        // ALWAYS trigger cascade on load
        await updateBrandField();
        
        // Defaults for packaging
        const baseUnit = (p.packBaseUnit || (p.unitType==='liter'?'litre':p.unitType) || 'piece');
        if(document.getElementById('pmBaseUnit')) document.getElementById('pmBaseUnit').value = baseUnit;

        // ── Conditionnement dynamique ──
        const _CM = {
            carton:{icon:'📦',lbl:'carton',sub:'pièces',f:'pmCartonQty'},
            palette:{icon:'🏗️',lbl:'palette',sub:'pièces',f:'pmPaletteQty'},
            bidon:{icon:'🫙',lbl:'bidon',sub:'litres',f:'pmBidonQty'},
            fut:{icon:'🛢️',lbl:'fût',sub:'litres',f:'pmFutQty'},
            aerosol:{icon:'🧴',lbl:'aérosol',sub:'ml',f:'pmAerosolQty'},
            sac:{icon:'🛍️',lbl:'sac',sub:'kg',f:'pmSacQty'},
            seau:{icon:'🪣',lbl:'seau',sub:'litres',f:'pmSeauQty'},
            rouleau:{icon:'🧻',lbl:'rouleau',sub:'mètres',f:'pmRouleauQty'},
            bobine:{icon:'🧵',lbl:'bobine',sub:'mètres',f:'pmBobineQty'},
            tube:{icon:'🧪',lbl:'tube',sub:'pièces',f:'pmTubeQty'},
            boite:{icon:'📫',lbl:'boîte',sub:'pièces',f:'pmBoiteQty'},
            paquet:{icon:'📬',lbl:'paquet',sub:'pièces',f:'pmPaquetQty'},
        };
        const _updCond = () => {
            const bu = document.getElementById('pmBaseUnit')?.value || 'piece';
            const el = document.getElementById('pmCondBlock');
            if(!el) return;
            const d = _CM[bu];
            if(!d){ el.innerHTML = ''; return; }
            const v = parseFloat(document.getElementById(d.f)?.value) || 0;
            const decomp = document.getElementById('pmDecompSub')?.value === '1';
            el.innerHTML = '<div class="pm-cond-box">'
                + '<h5>'+d.icon+' Conditionnement : '+d.lbl+'</h5>'
                + '<div class="pm-cond-row">'
                + '<div><label>Combien de '+d.sub+' dans 1 '+d.lbl+' ?</label>'
                + '<input type="number" class="inp" id="pmCondInput" min="0" step="0.01" value="'+v+'" placeholder="Ex: 10"></div>'
                + '<div><label>Prix d\'achat par '+d.lbl+' ('+Locale.currencyCode()+')</label>'
                + '<input type="number" class="inp" id="pmCondCost" min="0" step="0.01" value="0" placeholder="Optionnel"></div>'
                + '</div>'
                + '<div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--border,#e2e8f0)">'
                + '<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;font-weight:600">'
                + '<input type="checkbox" id="pmDecompSubChk" style="width:16px;height:16px" '+(decomp?'checked':'')+'>'
                + ' Décompter en '+d.sub+' dans le '+d.lbl
                + '</label>'
                + '<div style="font-size:11px;color:var(--text-muted,#64748b);margin-top:4px">'
                + 'Si coché, les mouvements de stock se font en '+d.sub+' (ex: sortir 2 '+d.sub+' d\'un '+d.lbl+'). Sinon, en '+d.lbl+'s entiers.'
                + '</div>'
                + '</div>'
                + '</div>';
            const inp = document.getElementById('pmCondInput');
            if(inp) inp.oninput = () => { const h=document.getElementById(d.f); if(h) h.value=inp.value; };
            const chk = document.getElementById('pmDecompSubChk');
            if(chk) chk.onchange = () => { const h=document.getElementById('pmDecompSub'); if(h) h.value=chk.checked?'1':'0'; };
        };
        document.getElementById('pmBaseUnit').onchange = _updCond;
        _updCond();

        // ── EAN multiples ──
        const _eanRefresh = () => {
            const tags = document.getElementById('pmEanTags');
            const hidden = document.getElementById('pmEansHidden');
            if(!tags || !hidden) return;
            const eans = this._sanitizeEanList(hidden.value || '[]');
            hidden.value = JSON.stringify(eans);
            tags.innerHTML = eans.map((e,i) => `<span class="pm-ean-tag">${this._esc(e)}<button type="button" aria-label="Retirer l'EAN ${this._esc(e)}" data-idx="${i}">×</button></span>`).join('');
            tags.querySelectorAll('button').forEach(btn => {
                btn.onclick = () => {
                    const list = this._sanitizeEanList(hidden.value || '[]');
                    list.splice(parseInt(btn.dataset.idx, 10), 1);
                    hidden.value = JSON.stringify(list);
                    _eanRefresh();
                };
            });
        };
        window._pmEanRefresh = _eanRefresh;
        const eanAddBtn = document.getElementById('pmEanAddBtn');
        const eanInput = document.getElementById('pmEanInput');
        if(eanAddBtn && eanInput) {
            const _addEan = () => {
                const v = eanInput.value.trim();
                if(!v || ['[]','{}','null','undefined'].includes(v.toLowerCase())) return;
                const hidden = document.getElementById('pmEansHidden');
                const eans = this._sanitizeEanList(hidden?.value || '[]');
                if(!eans.includes(v)){ eans.push(v); hidden.value=JSON.stringify(eans); }
                eanInput.value = '';
                _eanRefresh();
            };
            eanAddBtn.onclick = _addEan;
            eanInput.onkeydown = (e) => { if(e.key==='Enter'){ e.preventDefault(); _addEan(); } };
        }
        _eanRefresh();

        const pmOpenVariantWizard = document.getElementById('pmOpenVariantWizard');
        if(pmOpenVariantWizard && pid){
            pmOpenVariantWizard.onclick = () => this.openVariantDuplicateModal(pid);
        }
        const pmVariantFamily = document.getElementById('pmVariantFamily');
        const pmVariantSummary = document.getElementById('pmVariantSummary');
        const updateVariantSummary = () => {
            const fid = pmVariantFamily?.value || '';
            const fam = (this.variantFamilies || []).find(f => f.id === fid);
            if(!pmVariantSummary) return;
            if(!fam){ pmVariantSummary.textContent = 'Aucune famille générique sélectionnée.'; return; }
            const attrs = (fam.attributes||[]).map(a => `${a.label}${a.unit ? ' ('+a.unit+')' : ''}`).join(' · ');
            pmVariantSummary.innerHTML = `<strong>${this._esc(fam.label)}</strong><br>${this._esc(attrs || 'Attributs libres')}`;
        };
        if(pmVariantFamily) pmVariantFamily.onchange = updateVariantSummary;
        updateVariantSummary();

        // ── Tailles & Déclinaisons (toggle EPI / Vêtement) ──
        const _sizeTypeSel = document.getElementById('pmSizeType');
        const _sizeNormSel = document.getElementById('pmSizeNorm');
        const _sizeNormWrap = document.getElementById('pmSizeNormWrap');
        const _sizesBlock = document.getElementById('pmSizesBlock');
        const _sizesGrid = document.getElementById('pmSizesGrid');
        const _sizesCount = document.getElementById('pmSizesCount');
        const _sizeModeToggle = document.getElementById('pmSizeModeToggle');
        const _sizeModeKnob = document.getElementById('pmSizeModeKnob');
        const _sizeTitle = document.getElementById('pmSizeSectionTitle');
        const _selectedSizes = new Set();

        const _sizeOptions = {
            clothing: { // mode vêtement
                none:     'Aucun (produit standard)',
                clothing: 'Vêtement (XS → 4XL)',
                pants:    'Pantalon (taille 34 → 54)',
                shoes:    'Chaussures (EU / UK / US)',
                headwear: 'Coiffure / Casquette (S → XL)',
                eyewear:  'Lunettes / Accessoire',
                baby:     'Bébé / Enfant (0M → 16A)',
            },
            epi: { // mode EPI
                none:     'Aucun (produit standard)',
                clothing: 'Vêtement de travail (XS → 4XL)',
                pants:    'Pantalon de travail (taille 34 → 54)',
                shoes:    'Chaussures de sécurité (EU / UK / US)',
                gloves:   'Gants de protection (6 → 12)',
                headwear: 'Casque / Protection tête (S → XL)',
                eyewear:  'Lunettes / Visière de protection',
            }
        };

        const _rebuildSizeTypeOptions = () => {
            if(!_sizeTypeSel) return;
            const mode = (_sizeModeToggle && _sizeModeToggle.checked) ? 'epi' : 'clothing';
            const curVal = _sizeTypeSel.value;
            const opts = _sizeOptions[mode];
            _sizeTypeSel.innerHTML = Object.entries(opts).map(([k,v]) =>
                '<option value="'+k+'"'+(k===curVal?' selected':'')+'>'+v+'</option>'
            ).join('');
            // Restore value if still exists
            if(opts[curVal]) _sizeTypeSel.value = curVal;
            else _sizeTypeSel.value = 'none';
            // Update title + toggle visuals
            if(_sizeTitle) _sizeTitle.innerHTML = mode==='epi' ? '🦺 EPI & Déclinaisons' : '👕 Tailles & Déclinaisons';
            if(_sizeModeKnob) _sizeModeKnob.style.left = mode==='epi' ? '22px' : '2px';
            if(_sizeModeToggle) _sizeModeToggle.parentElement.querySelector('span:nth-child(2)').style.background = mode==='epi' ? 'var(--orange,#f59e0b)' : 'var(--border)';
            _buildSizeChips();
        };

        if(_sizeModeToggle){
            _sizeModeToggle.onchange = _rebuildSizeTypeOptions;
        }

        const _buildSizeChips = () => {
            const st = _sizeTypeSel?.value || 'none';
            const sn = _sizeNormSel?.value || 'eu';
            if(st === 'none' || !_sizesGrid){
                if(_sizesBlock) _sizesBlock.style.display = 'none';
                if(_sizeNormWrap) _sizeNormWrap.style.display = 'none';
                return;
            }
            if(_sizeNormWrap) _sizeNormWrap.style.display = (st === 'shoes' || st === 'pants') ? 'block' : 'none';
            if(_sizesBlock) _sizesBlock.style.display = 'block';

            const sizes = this._getSizesForType(st, sn);
            _selectedSizes.clear();
            _sizesGrid.innerHTML = sizes.map(sz => {
                return '<div class="pm-size-chip" data-sz="'+sz+'">'+sz+'</div>';
            }).join('');

            _sizesGrid.querySelectorAll('.pm-size-chip').forEach(chip => {
                chip._selected = false;
                chip.onclick = () => {
                    chip._selected = !chip._selected;
                    chip.classList.toggle('selected', chip._selected);
                    const sz = chip.dataset.sz;
                    if(chip._selected) _selectedSizes.add(sz);
                    else _selectedSizes.delete(sz);
                    if(_sizesCount) _sizesCount.textContent = _selectedSizes.size > 0
                        ? _selectedSizes.size + ' déclinaison(s) seront créées'
                        : '';
                };
            });
            if(_sizesCount) _sizesCount.textContent = '';
        };

        if(_sizeTypeSel){
            _sizeTypeSel.onchange = _buildSizeChips;
            // Detect initial mode from existing product sizeType
            if(p.sizeType && p.sizeType !== 'none'){
                const isEpi = !!_sizeOptions.epi[p.sizeType] && !_sizeOptions.clothing[p.sizeType];
                const isBoth = !!_sizeOptions.epi[p.sizeType] && !!_sizeOptions.clothing[p.sizeType];
                // gloves only exists in EPI mode
                if(p.sizeType === 'gloves' && _sizeModeToggle){
                    _sizeModeToggle.checked = true;
                }
                _rebuildSizeTypeOptions();
                _sizeTypeSel.value = p.sizeType;
            }
        }
        if(_sizeNormSel){
            _sizeNormSel.onchange = _buildSizeChips;
            if(p.sizeNorm) _sizeNormSel.value = p.sizeNorm;
        }
        _rebuildSizeTypeOptions();

        // Bouton générer déclinaisons (édition uniquement)
        const genBtn = document.getElementById('pmGenVariants');
        if(genBtn && pid){
            genBtn.onclick = () => {
                const sizes = [..._selectedSizes];
                if(!sizes.length){ toast('Sélectionnez au moins une taille','warning'); return; }
                if(!confirm('Créer '+sizes.length+' déclinaison(s) de ce produit ?\n\nTailles : '+sizes.join(', '))) return;
                this.generateSizeVariants(pid, sizes);
            };
        }

        // Toggle initial trace inputs based on tracking flags
        const initTrace = document.getElementById('pmInitTrace');
        const trLot = document.getElementById('pmTrackLot');
        const trExp = document.getElementById('pmTrackExp');
        const toggleInitTrace = ()=>{
            const on = (!!(trLot && trLot.checked)) || (!!(trExp && trExp.checked));
            if(initTrace) initTrace.style.display = on ? 'block' : 'none';
        };
        if(trLot) trLot.onchange = toggleInitTrace;
        if(trExp) trExp.onchange = toggleInitTrace;
        toggleInitTrace();

        // PRODUCTS MOBILE FLOW5 : le module dédié réorganise le formulaire uniquement
        // sur téléphone/tablette, après l'initialisation complète des dépendances.
        if(window.MageStockProductsMobile?.enhanceModal){
            await window.MageStockProductsMobile.enhanceModal({ pid: pid || null, isEdit, product: p });
        }

    },
    
    /**
     * Modal de consultation en lecture seule (pour utilisateurs non-admin)
     */
    async _openViewModal(pid){
        const whs = this.whs;
        const all = await Api.products();
        const p = all.find(x => String(x.id) === String(pid)) || {};
        const currentStock = (this.stock || []).filter(s => String(s.productId) === String(pid));
        
        if(!p.id){
            toast('Produit introuvable', 'error');
            return;
        }
        
        const baseUnit = this._getBaseUnit(p);
        const unitLabel = baseUnit === 'piece' ? 'u' : baseUnit;
        const baseCost = getBaseCost(p);
        
        // Construire l'affichage du stock
        let stockHtml = '';
        if(currentStock.length > 0) {
            const totalQty = currentStock.reduce((sum, s) => sum + (parseFloat(s.quantity) || 0), 0);
            const totalValue = totalQty * baseCost;
            
            const stockLines = currentStock.map(s => {
                const wh = whs.find(w => String(w.id) === String(s.warehouseId));
                const qty = parseFloat(s.quantity) || 0;
                const minStock = s.minStock != null ? parseInt(s.minStock) : null;
                const maxStock = s.maxStock != null ? parseInt(s.maxStock) : null;
                
                const stockStatus = this._stockStatus(qty, minStock);
                const statusBadge = `<span class="badge ${stockStatus.badgeClass}">${stockStatus.label}</span>`;
                
                return `
                    <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--border-light)">
                        <div>
                            <strong>${wh ? wh.name : 'Magasin ?'}</strong>
                            ${s.location ? '<br><small style="color:var(--text-muted)">📍 ' + s.location + '</small>' : ''}
                        </div>
                        <div style="text-align:right">
                            <strong style="font-size:18px">${qty} ${unitLabel}</strong>
                            ${minStock != null ? '<br><small style="color:var(--text-muted)">Min: ' + minStock + '</small>' : ''}
                        </div>
                        <div style="margin-left:10px">${statusBadge}</div>
                    </div>`;
            }).join('');
            
            stockHtml = `
                <div style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--r-lg);padding:16px;margin-bottom:16px">
                    <h4 style="margin:0 0 12px 0;display:flex;align-items:center;gap:8px">
                        📦 Stock par magasin
                    </h4>
                    ${stockLines}
                    <div style="display:flex;justify-content:space-between;padding:12px 0 0 0;margin-top:8px;border-top:2px solid var(--border);font-size:16px">
                        <strong>Total</strong>
                        <strong>${totalQty} ${unitLabel} · ${Locale.formatMoney(totalValue)}</strong>
                    </div>
                </div>`;
        } else {
            stockHtml = `
                <div style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--r-lg);padding:20px;margin-bottom:16px;text-align:center;color:var(--text-muted)">
                    <svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity:0.5;margin-bottom:8px">
                        <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/>
                    </svg>
                    <p>Aucun stock enregistré</p>
                </div>`;
        }
        
        // Informations produit
        const supplier = this.suppliers.find(s => String(s.id) === String(p.supplier));
        
        const infoHtml = `
            <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:16px">
                <div style="background:var(--bg-elevated);padding:12px;border-radius:var(--r)">
                    <small style="color:var(--text-muted)">SKU</small>
                    <div style="font-weight:600;font-family:var(--font-mono)">${p.sku || '—'}</div>
                </div>
                <div style="background:var(--bg-elevated);padding:12px;border-radius:var(--r)">
                    <small style="color:var(--text-muted)">Code-barres</small>
                    <div style="font-weight:600;font-family:var(--font-mono)">${p.barcode || '—'}</div>
                </div>
                <div style="background:var(--bg-elevated);padding:12px;border-radius:var(--r)">
                    <small style="color:var(--text-muted)">Catégorie</small>
                    <div style="font-weight:600">${p.category || '—'}</div>
                </div>
                <div style="background:var(--bg-elevated);padding:12px;border-radius:var(--r)">
                    <small style="color:var(--text-muted)">Marque</small>
                    <div style="font-weight:600">${p.brand || '—'}</div>
                </div>
                ${supplier ? `
                <div style="background:var(--bg-elevated);padding:12px;border-radius:var(--r);grid-column:span 2">
                    <small style="color:var(--text-muted)">Fournisseur</small>
                    <div style="font-weight:600">${supplier.name}</div>
                </div>` : ''}
            </div>`;
        
        // Documents
        let docsHtml = '';
        if(p.datasheetUrl || p.msdsUrl){
            docsHtml = `
                <div style="display:flex;gap:8px;margin-top:16px;padding-top:16px;border-top:1px solid var(--border)">
                    ${p.datasheetUrl ? `<a href="${p.datasheetUrl}" target="_blank" class="btn btn-secondary btn-sm">📄 Fiche technique</a>` : ''}
                    ${p.msdsUrl ? `<a href="${p.msdsUrl}" target="_blank" class="btn btn-secondary btn-sm">⚠️ Fiche de sécurité</a>` : ''}
                </div>`;
        }
        
        // Image
        const imgHtml = p.image 
            ? `<div style="text-align:center;margin-bottom:16px"><img src="${this._esc(this._productImageSrc(p))}" style="max-width:200px;max-height:150px;border-radius:var(--r-lg);object-fit:cover"></div>`
            : '';
        
        Modal.show(p.name, `
            ${imgHtml}
            ${stockHtml}
            ${infoHtml}
            ${docsHtml}
        `, [
            {label: 'Localiser sur le plan', cls: 'btn-primary', onclick: () => this.openProductPlan(pid)},
            {label: 'Fermer', cls: 'btn-secondary', onclick: () => Modal.close()}
        ], 'info');

        // ── Icône personnalisée du modal consultation ────────────────
        const mIcon = document.getElementById('modalIcon');
        if(mIcon){
            mIcon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
            const hdr = mIcon.parentElement;
            if(hdr) hdr.style.background = 'var(--blue-pale, #dbeafe)';
            const svg = mIcon.querySelector('svg');
            if(svg) svg.style.stroke = 'var(--blue, #3b82f6)';
        }
    },
    
    async _syncStockLocations(productId, warehouseIds){
    const pid = parseInt(productId||0);
    if(!pid) return;
    const whs = Array.isArray(warehouseIds) ? warehouseIds.map(x=>parseInt(x)).filter(Boolean) : [];
    if(whs.length===0) return;

    const picking    = (document.getElementById('pmPicking')?.value || '').trim();
    const location   = (document.getElementById('pmLocation')?.value || '').trim();
    const rack       = (document.getElementById('pmRack')?.value || '').trim();
    const shelf      = (document.getElementById('pmShelfLoc')?.value || '').trim();
    const locSel     = document.getElementById('pmLocationId');
    const locationId = locSel?.value ? parseInt(locSel.value) : null;
    // Si un emplacement enregistré est sélectionné, on utilise son code comme "location" (rétrocompat)
    const locOpt     = locSel ? locSel.options[locSel.selectedIndex] : null;
    const locCode    = (locationId && locOpt) ? (locOpt.getAttribute('data-code') || '') : '';

    // Upsert stock row for each selected warehouse (creates assignment effectively)
    await Promise.all(whs.map(wid => Api.stockSet({
        productId: pid,
        warehouseId: wid,
        picking,
        location:   location || locCode,
        rack,
        shelf,
        locationId: locationId || null
    })));
},

    // ─────────────────────────────────────────────────────────────
    // Uploader image pro : fichier + drag&drop + URL
    // ─────────────────────────────────────────────────────────────
    _initImageUploader(currentImageUrl){
        // État interne
        this._imgUploadFile = null;
        this._imgUploadUrl  = currentImageUrl || '';
        this._imgUploadDirty = false;

        const zone     = document.getElementById('pmImgZone');
        const fileInp  = document.getElementById('pmImg');
        const preview  = document.getElementById('pmImgPreview');
        const urlBtn   = document.getElementById('pmImgUrlBtn');
        const urlRow   = document.getElementById('pmImgUrlRow');
        const urlInp   = document.getElementById('pmImgUrl');
        const urlOk    = document.getElementById('pmImgUrlOk');
        const urlCancel= document.getElementById('pmImgUrlCancel');
        const clearBtn = document.getElementById('pmImgClear');
        const filename = document.getElementById('pmImgFilename');

        if(!zone || !fileInp) return;

        const _showPreview = (src, label) => {
            preview.innerHTML = `<img src="${src}" id="pmImgPreviewImg" alt="aperçu" onerror="this.parentElement.innerHTML='<span style=color:var(--danger)>Image invalide</span>'">`;
            zone.classList.add('pm-img-zone--has-img');
            if(filename && label){
                filename.textContent = label;
                filename.style.display = 'block';
            }
            // Montrer bouton supprimer
            if(!document.getElementById('pmImgClear')){
                const del = document.createElement('button');
                del.type = 'button';
                del.id = 'pmImgClear';
                del.className = 'pm-img-btn pm-img-btn--danger';
                del.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg> Supprimer`;
                del.onclick = _clearImg;
                document.querySelector('#pmImgZone .pm-img-actions')?.appendChild(del);
            }
        };

        const _clearImg = () => {
            this._imgUploadFile = null;
            this._imgUploadUrl  = '';
            this._imgUploadDirty = true;
            preview.innerHTML = `<div class="pm-img-placeholder" id="pmImgPlaceholder">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="40" height="40" opacity=".35">
                    <rect x="3" y="3" width="18" height="18" rx="3"/>
                    <circle cx="8.5" cy="8.5" r="1.5"/>
                    <path d="M21 15l-5-5L5 21"/>
                </svg>
                <span>Glissez ou collez une image ici</span>
            </div>`;
            zone.classList.remove('pm-img-zone--has-img');
            fileInp.value = '';
            document.getElementById('pmImgClear')?.remove();
            if(filename){ filename.textContent=''; filename.style.display='none'; }
        };

        // ── Fichier via input ────────────────────────────────────
        fileInp.onchange = () => {
            const f = fileInp.files[0];
            if(!f) return;
            this._imgUploadFile = f;
            this._imgUploadUrl  = '';
            this._imgUploadDirty = true;
            const reader = new FileReader();
            reader.onload = e => _showPreview(e.target.result, f.name);
            reader.readAsDataURL(f);
        };

        // ── Drag & Drop ──────────────────────────────────────────
        zone.addEventListener('dragover', e => {
            e.preventDefault();
            zone.classList.add('pm-img-zone--drag');
        });
        zone.addEventListener('dragleave', () => zone.classList.remove('pm-img-zone--drag'));
        zone.addEventListener('drop', e => {
            e.preventDefault();
            zone.classList.remove('pm-img-zone--drag');
            const f = e.dataTransfer?.files?.[0];
            if(f && f.type.startsWith('image/')){
                this._imgUploadFile = f;
                this._imgUploadUrl  = '';
                this._imgUploadDirty = true;
                const reader = new FileReader();
                reader.onload = ev => _showPreview(ev.target.result, f.name);
                reader.readAsDataURL(f);
            }
        });

        // ── Coller une image (Ctrl+V / Cmd+V) ───────────────────
        // Écoute globale sur le document mais ne réagit que si le modal produit est ouvert
        if(!window._pmPasteListenerBound){
            window._pmPasteListenerBound = true;
            document.addEventListener('paste', (e) => {
                // Ne réagir que si la zone d'upload existe dans le DOM (= modal ouvert)
                const z = document.getElementById('pmImgZone');
                if(!z) return;
                const items = e.clipboardData?.items;
                if(!items) return;
                for(const item of items){
                    if(item.type.startsWith('image/')){
                        e.preventDefault();
                        const f = item.getAsFile();
                        if(!f) continue;
                        ProdPage._imgUploadFile = f;
                        ProdPage._imgUploadUrl  = '';
                        ProdPage._imgUploadDirty = true;
                        const reader = new FileReader();
                        reader.onload = ev => {
                            const preview = document.getElementById('pmImgPreview');
                            const zone = document.getElementById('pmImgZone');
                            const filename = document.getElementById('pmImgFilename');
                            if(preview){
                                preview.innerHTML = `<img src="${ev.target.result}" id="pmImgPreviewImg" alt="aperçu">`;
                            }
                            if(zone) zone.classList.add('pm-img-zone--has-img');
                            if(filename){ filename.textContent = 'Image collée'; filename.style.display = 'block'; }
                            // Ajouter bouton supprimer si absent
                            if(!document.getElementById('pmImgClear')){
                                const del = document.createElement('button');
                                del.type = 'button'; del.id = 'pmImgClear';
                                del.className = 'pm-img-btn pm-img-btn--danger';
                                del.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg> Supprimer`;
                                del.onclick = () => {
                                    ProdPage._imgUploadFile = null;
                                    ProdPage._imgUploadUrl = '';
                                    ProdPage._imgUploadDirty = true;
                                    const p2 = document.getElementById('pmImgPreview');
                                    if(p2) p2.innerHTML = `<div class="pm-img-placeholder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="40" height="40" opacity=".35"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg><span>Glissez ou collez une image ici</span></div>`;
                                    const z2 = document.getElementById('pmImgZone');
                                    if(z2) z2.classList.remove('pm-img-zone--has-img');
                                    const fi = document.getElementById('pmImg');
                                    if(fi) fi.value = '';
                                    del.remove();
                                    const fn = document.getElementById('pmImgFilename');
                                    if(fn){ fn.textContent=''; fn.style.display='none'; }
                                };
                                document.querySelector('#pmImgZone .pm-img-actions')?.appendChild(del);
                            }
                        };
                        reader.readAsDataURL(f);
                        break;
                    }
                }
            });
        }

        // ── Bouton URL ───────────────────────────────────────────
        if(urlBtn) urlBtn.onclick = () => {
            urlRow.style.display = 'flex';
            urlInp.value = this._imgUploadUrl || '';
            urlInp.focus();
        };
        if(urlCancel) urlCancel.onclick = () => { urlRow.style.display = 'none'; };
        if(urlOk) urlOk.onclick = () => {
            const url = urlInp.value.trim();
            if(!url){ urlRow.style.display = 'none'; return; }
            this._imgUploadFile = null;
            this._imgUploadUrl  = url;
            this._imgUploadDirty = true;
            _showPreview(url, url.split('/').pop());
            urlRow.style.display = 'none';
        };
        // Valider URL sur Entrée
        if(urlInp) urlInp.onkeydown = e => { if(e.key==='Enter'){ e.preventDefault(); urlOk?.click(); } };

        // ── Supprimer image existante ────────────────────────────
        if(clearBtn) clearBtn.onclick = _clearImg;
    },

    async saveProduct(pid){
        const name = document.getElementById('pmName').value.trim();
        if(!name){toast('Nom obligatoire','error');return false;}

        // Get selected warehouse (single select)
        const whSelect = document.getElementById('pmWarehouse');
        const selectedWarehouseId = parseInt(whSelect.value) || 0;
        
        if(!selectedWarehouseId) {
            toast('Veuillez sélectionner un magasin', 'error');
            return false;
        }
        
        // Create array with single warehouse
        const allowedWarehouses = [selectedWarehouseId];
        
        // ================================================================
        // DEMANDE #2: AUTO-CRÉATION MARQUE
        // ================================================================
        let brandValue = '';
        const brandEl = document.getElementById('pmBrand');
        const supplierId = parseInt(document.getElementById('pmSupp').value) || null;
        
        if(brandEl) {
            if(brandEl.tagName === 'SELECT' && brandEl.value === '__other__') {
                // User selected "Autre" - get custom brand name
                const otherInput = document.getElementById('pmBrandOther');
                brandValue = otherInput ? otherInput.value.trim() : '';
                
                // Auto-create brand if new and supplier selected
                if(brandValue && supplierId) {
                    try {
                        // Check if brand already exists globally
                        const allBrands = await Api.brands();
                        const existingBrand = allBrands.find(b => 
                            b.name.toLowerCase() === brandValue.toLowerCase()
                        );
                        
                        if(!existingBrand) {
                            // Create new brand
                            const newBrand = await Api.brandAdd({ name: brandValue });
                            
                            // Link brand to supplier
                            const supplier = this.suppliers.find(s => parseInt(s.id) === supplierId);
                            if(supplier) {
                                let linkedBrands = [];
                                try { const v = supplier.linkedBrands; linkedBrands = Array.isArray(v) ? v : JSON.parse(v || '[]'); } catch(e) { linkedBrands = []; }
                                linkedBrands = (Array.isArray(linkedBrands)?linkedBrands:[]).map(x=>parseInt(x)).filter(x=>Number.isFinite(x));
                                if(!linkedBrands.includes(parseInt(newBrand.id))) {
                                    linkedBrands.push(parseInt(newBrand.id));
                                    await Api.supplierEdit({
                                        id: supplierId,
                                        linkedBrands: JSON.stringify(linkedBrands)
                                    });
                                }
                            }
                            
                            toast(`✅ Marque "${brandValue}" créée et liée au fournisseur`, 'success');
                        } else {
                            // Brand exists - link to supplier if not already
                            const supplier = this.suppliers.find(s => parseInt(s.id) === supplierId);
                            if(supplier) {
                                let linkedBrands = [];
                                try { const v = supplier.linkedBrands; linkedBrands = Array.isArray(v) ? v : JSON.parse(v || '[]'); } catch(e) { linkedBrands = []; }
                                linkedBrands = (Array.isArray(linkedBrands)?linkedBrands:[]).map(x=>parseInt(x)).filter(x=>Number.isFinite(x));
                                if(!linkedBrands.includes(parseInt(existingBrand.id))) {
                                    linkedBrands.push(parseInt(existingBrand.id));
                                    await Api.supplierEdit({
                                        id: supplierId,
                                        linkedBrands: JSON.stringify(linkedBrands)
                                    });
                                    toast(`✅ Marque "${brandValue}" liée au fournisseur`, 'info');
                                }
                            }
                        }
                    } catch(e) {
                        console.error('[Products] Error auto-creating brand:', e);
                        // Continue anyway - don't block product creation
                    }
                }
            } else {
                brandValue = brandEl.value.trim();
            }
        }
        
        const imgFile    = this._imgUploadFile || null;
        const imgUrlOnly = (!imgFile && this._imgUploadUrl) ? this._imgUploadUrl : null;
        const packEnabled = true;
        const baseUnit = document.getElementById('pmBaseUnit').value;
        const packCartonQty = parseInt(document.getElementById('pmCartonQty').value)||0;
        const packPaletteQty = parseInt(document.getElementById('pmPaletteQty').value)||0;
        const packBidonQty = parseFloat(document.getElementById('pmBidonQty').value)||0;
        const packFutQty = parseFloat(document.getElementById('pmFutQty').value)||0;
        const packAerosolQty = parseFloat(document.getElementById('pmAerosolQty')?.value)||0;
        const packSacQty = parseFloat(document.getElementById('pmSacQty')?.value)||0;
        const packSeauQty = parseFloat(document.getElementById('pmSeauQty')?.value)||0;
        const packRouleauQty = parseFloat(document.getElementById('pmRouleauQty')?.value)||0;
        const packBobineQty = parseFloat(document.getElementById('pmBobineQty')?.value)||0;
        const packTubeQty = parseFloat(document.getElementById('pmTubeQty')?.value)||0;
        const packBoiteQty = parseFloat(document.getElementById('pmBoiteQty')?.value)||0;
        const packPaquetQty = parseFloat(document.getElementById('pmPaquetQty')?.value)||0;

        const initQty = (!pid) ? (parseFloat(document.getElementById('pmInitQty')?.value)||0) : 0;
        const initLot = (!pid) ? (document.getElementById('pmInitLot')?.value||'').trim() : '';
        const initExp = (!pid) ? (document.getElementById('pmInitExp')?.value||'').trim() : '';
        const supplierLinks = this._collectSupplierLinks ? this._collectSupplierLinks() : [];
        let eans = [];
        eans = this._sanitizeEanList(document.getElementById('pmEansHidden')?.value || '[]');
        const decompSub = document.getElementById('pmDecompSub')?.value === '1';
        const sizeType = document.getElementById('pmSizeType')?.value || 'none';
        const sizeNorm = document.getElementById('pmSizeNorm')?.value || 'eu';
        const isVariantParent = !!document.getElementById('pmIsVariantParent')?.checked;
        const variantFamily = document.getElementById('pmVariantFamily')?.value || '';
        const variantLabel = (document.getElementById('pmVariantLabel')?.value || '').trim();
        let variantAttributes = {};
        try { variantAttributes = JSON.parse(document.getElementById('pmVariantAttributes')?.value || '{}'); } catch(e){ variantAttributes = {}; }

        try {
            if(imgFile && !pid){
                const fd = new FormData();
                fd.append('name', name);
                fd.append('sku', document.getElementById('pmSKU').value);
                fd.append('barcode', document.getElementById('pmBarcode').value);
                fd.append('brand', brandValue);
                fd.append('category', document.getElementById('pmCat').value);
                fd.append('description', document.getElementById('pmDesc').value);
                fd.append('cost', document.getElementById('pmCost').value);
                fd.append('supplier', document.getElementById('pmSupp').value);
                fd.append('supplierLinks', JSON.stringify(supplierLinks));
                fd.append('allowedWarehouses', JSON.stringify(allowedWarehouses));
                fd.append('defaultWarehouse', String(selectedWarehouseId));
                fd.append('packBaseUnit', baseUnit);
                fd.append('unitType', baseUnit);
                fd.append('packCartonQty', String(packCartonQty));
                fd.append('packPaletteQty', String(packPaletteQty));
                fd.append('packBidonQty', String(packBidonQty));
                fd.append('packFutQty', String(packFutQty));
                fd.append('packAerosolQty', String(packAerosolQty));
                fd.append('packSacQty', String(packSacQty));
                fd.append('packSeauQty', String(packSeauQty));
                fd.append('packRouleauQty', String(packRouleauQty));
                fd.append('packBobineQty', String(packBobineQty));
                fd.append('packTubeQty', String(packTubeQty));
                fd.append('packBoiteQty', String(packBoiteQty));
                fd.append('packPaquetQty', String(packPaquetQty));
                fd.append('safetyStock', document.getElementById('pmSafety').value);
                fd.append('trackLot', document.getElementById('pmTrackLot').checked);
                fd.append('trackExpiry', document.getElementById('pmTrackExp').checked);
                fd.append('shelfLifeDays', document.getElementById('pmShelf').value);
                fd.append('datasheetUrl', document.getElementById('pmDatasheet').value);
                fd.append('msdsUrl', document.getElementById('pmMsds').value);
                fd.append('image', imgFile);
                fd.append('eans', JSON.stringify(eans));
                fd.append('decompSub', decompSub ? '1' : '0');
                fd.append('sizeType', sizeType);
                fd.append('sizeNorm', sizeNorm);
                fd.append('isVariantParent', isVariantParent ? '1' : '0');
                fd.append('variantFamily', variantFamily);
                fd.append('variantLabel', variantLabel);
                fd.append('variantAttributes', JSON.stringify(variantAttributes));
                const created = await Api.prodAddImg(fd);
                await this._maybeInitStock(created?.id, initQty, baseUnit, initLot, initExp);
                await this._syncStockLocations(created?.id, allowedWarehouses);
            } else if(!pid){
                const created = await Api.prodAdd({
                    name, sku:document.getElementById('pmSKU').value, barcode:document.getElementById('pmBarcode').value,
                    brand:brandValue, category:document.getElementById('pmCat').value,
                    description:document.getElementById('pmDesc').value, cost:parseFloat(document.getElementById('pmCost').value)||0,
                    supplier:document.getElementById('pmSupp').value,
                    supplierLinks:supplierLinks,
                    allowedWarehouses:JSON.stringify(allowedWarehouses),
                    defaultWarehouse:selectedWarehouseId,
                    unitType:baseUnit,
                    packBaseUnit:baseUnit,
                    packCartonQty:packCartonQty,
                    packPaletteQty:packPaletteQty,
                    packBidonQty:packBidonQty,
                    packFutQty:packFutQty,
                    packPaquetQty:packPaquetQty,
                    packBoiteQty:packBoiteQty,
                    packTubeQty:packTubeQty,
                    packBobineQty:packBobineQty,
                    packRouleauQty:packRouleauQty,
                    packSeauQty:packSeauQty,
                    packSacQty:packSacQty,
                    packAerosolQty:packAerosolQty,
                    safetyStock:document.getElementById('pmSafety').value===''?null:parseInt(document.getElementById('pmSafety').value),
                    trackLot:document.getElementById('pmTrackLot').checked,
                    trackExpiry:document.getElementById('pmTrackExp').checked,
                    shelfLifeDays:document.getElementById('pmShelf').value===''?null:parseInt(document.getElementById('pmShelf').value),
                    datasheetUrl:document.getElementById('pmDatasheet').value,
                    msdsUrl:document.getElementById('pmMsds').value,
                    image: imgUrlOnly || '',
                    eans: eans,
                    decompSub: decompSub,
                    sizeType: sizeType,
                    sizeNorm: sizeNorm,
                    isVariantParent: isVariantParent,
                    variantFamily: variantFamily,
                    variantLabel: variantLabel,
                    variantAttributes: variantAttributes
                });
                await this._maybeInitStock(created?.id, initQty, baseUnit, initLot, initExp);
                await this._syncStockLocations(created?.id, allowedWarehouses);

            } else {
                // Edit product (JSON fields)
                await Api.prodEdit({
                    id:parseInt(pid), name, sku:document.getElementById('pmSKU').value, barcode:document.getElementById('pmBarcode').value,
                    brand:brandValue, category:document.getElementById('pmCat').value,
                    description:document.getElementById('pmDesc').value, cost:parseFloat(document.getElementById('pmCost').value)||0,
                    supplier:document.getElementById('pmSupp').value,
                    allowedWarehouses:JSON.stringify(allowedWarehouses),
                    defaultWarehouse:selectedWarehouseId,
                    unitType:baseUnit,
                    packBaseUnit:baseUnit,
                    packCartonQty:packCartonQty,
                    packPaletteQty:packPaletteQty,
                    packBidonQty:packBidonQty,
                    packFutQty:packFutQty,
                    packPaquetQty:packPaquetQty,
                    packBoiteQty:packBoiteQty,
                    packTubeQty:packTubeQty,
                    packBobineQty:packBobineQty,
                    packRouleauQty:packRouleauQty,
                    packSeauQty:packSeauQty,
                    packSacQty:packSacQty,
                    packAerosolQty:packAerosolQty,
                    safetyStock:document.getElementById('pmSafety').value===''?null:parseInt(document.getElementById('pmSafety').value),
                    trackLot:document.getElementById('pmTrackLot').checked,
                    trackExpiry:document.getElementById('pmTrackExp').checked,
                    shelfLifeDays:document.getElementById('pmShelf').value===''?null:parseInt(document.getElementById('pmShelf').value),
                    datasheetUrl:document.getElementById('pmDatasheet').value,
                    msdsUrl:document.getElementById('pmMsds').value,
                    supplierLinks:supplierLinks,
                    eans: eans,
                    decompSub: decompSub,
                    sizeType: sizeType,
                    sizeNorm: sizeNorm,
                    isVariantParent: isVariantParent,
                    variantFamily: variantFamily,
                    variantLabel: variantLabel,
                    variantAttributes: variantAttributes
                });

                // If a new image was selected during edit, upload it and update the stored URL.
                if(this._imgUploadDirty){
                    if(imgFile){
                        const fd = new FormData();
                        fd.append('id', pid);
                        fd.append('image', imgFile);
                        await Api.prodEditImg(fd);
                    } else if(imgUrlOnly !== null){
                        // URL externe : mettre à jour directement le champ image
                        await Api.prodEdit({ id: parseInt(pid), image: imgUrlOnly });
                    } else if(this._imgUploadUrl === ''){
                        // Image supprimée
                        await Api.prodEdit({ id: parseInt(pid), image: '' });
                    }
                }
                await this._syncStockLocations(pid, allowedWarehouses);
            }
            toast(pid?'Produit mis à jour':'Produit créé','success');
            Modal.close();
            await this.load();
            return true;
        } catch(e){
            toast(e.message,'error');
            return false;
        }
    },
    async delProduct(pid){
        if(!confirm('Supprimer ce produit ? (irréversible)')) return;
        try { await Api.prodDel(pid); toast('Produit supprimé','success'); Modal.close(); this.load(); }
        catch(e){toast(e.message,'error');}
    },
    // ════════════════════════════════════════════════════════════════
    // DUPLIQUER UN PRODUIT
    // ════════════════════════════════════════════════════════════════
    async duplicateProduct(pid){
        try {
            const all = await Api.products();
            const src = all.find(x=>String(x.id)===String(pid));
            if(!src){ toast('Produit introuvable','error'); return; }
            Modal.close();
            // Ouvrir le formulaire nouveau produit pré-rempli
            await this.openModal(null);
            // Attendre que le DOM soit prêt
            await new Promise(r=>setTimeout(r,200));
            // Pré-remplir tous les champs
            const _v = (id,v) => { const e=document.getElementById(id); if(e) e.value=v||''; };
            const _c = (id,v) => { const e=document.getElementById(id); if(e) e.checked=!!v; };
            _v('pmName', (src.name||'') + ' (copie)');
            _v('pmSKU', (src.sku||'') + '-CPY');
            _v('pmBarcode', src.barcode||'');
            _v('pmDesc', src.description||'');
            _v('pmCost', src.cost||0);
            _v('pmSafety', src.safetyStock??'');
            _v('pmShelf', src.shelfLifeDays??'');
            _v('pmDatasheet', src.datasheetUrl||'');
            _v('pmMsds', src.msdsUrl||'');
            _c('pmTrackLot', src.trackLot);
            _c('pmTrackExp', src.trackExpiry);
            // Unité de base
            _v('pmBaseUnit', src.packBaseUnit||src.unitType||'piece');
            // Hiddens conditionnements
            _v('pmCartonQty', src.packCartonQty||0);
            _v('pmPaletteQty', src.packPaletteQty||0);
            _v('pmBidonQty', src.packBidonQty||0);
            _v('pmFutQty', src.packFutQty||0);
            _v('pmAerosolQty', src.packAerosolQty||0);
            _v('pmSacQty', src.packSacQty||0);
            _v('pmSeauQty', src.packSeauQty||0);
            _v('pmRouleauQty', src.packRouleauQty||0);
            _v('pmBobineQty', src.packBobineQty||0);
            _v('pmTubeQty', src.packTubeQty||0);
            _v('pmBoiteQty', src.packBoiteQty||0);
            _v('pmPaquetQty', src.packPaquetQty||0);
            _v('pmDecompSub', src.decompSub?'1':'0');
            // EANs
            _v('pmEansHidden', JSON.stringify(this._sanitizeEanList(src.eans || [])));
            // Déclencher les cascades
            const buSel = document.getElementById('pmBaseUnit');
            if(buSel) buSel.dispatchEvent(new Event('change'));
            // Magasin
            let aw = src.allowedWarehouses;
            if(typeof aw==='string') try{aw=JSON.parse(aw||'[]')}catch(e){aw=[];}
            if(Array.isArray(aw)&&aw.length) {
                _v('pmWarehouse', aw[0]);
                const whSel = document.getElementById('pmWarehouse');
                if(whSel) whSel.dispatchEvent(new Event('change'));
                await new Promise(r=>setTimeout(r,300));
            }
            // Catégorie + fournisseur + marque
            _v('pmCat', src.category||'');
            _v('pmSupp', src.supplier||'');
            const suppSel = document.getElementById('pmSupp');
            if(suppSel) suppSel.dispatchEvent(new Event('change'));
            await new Promise(r=>setTimeout(r,200));
            const brandSel = document.getElementById('pmBrand');
            if(brandSel) brandSel.value = src.brand||'';
            // EPI
            _v('pmSizeType', src.sizeType||'none');
            const stSel = document.getElementById('pmSizeType');
            if(stSel) stSel.dispatchEvent(new Event('change'));
            // EAN refresh
            const eanRefreshBtn = document.getElementById('pmEanTags');
            if(eanRefreshBtn && typeof window._pmEanRefresh === 'function') window._pmEanRefresh();
            // Focus le nom
            const nameEl = document.getElementById('pmName');
            if(nameEl){ nameEl.focus(); nameEl.select(); }
            toast('Produit dupliqué — modifiez et créez','info');
        } catch(e){ toast(e.message,'error'); }
    },

    // ════════════════════════════════════════════════════════════════
    // DUPLIQUER AVEC DÉCLINAISONS GÉNÉRIQUES
    // ════════════════════════════════════════════════════════════════
    async openVariantDuplicateModal(pid){
        try {
            await this._loadVariantFamilies();
            const all = await Api.products();
            const parent = all.find(x => String(x.id) === String(pid));
            if(!parent){ toast('Produit introuvable','error'); return; }
            this._variantDuplicateParent = parent;

            const families = this.variantFamilies || [];
            const imgHtml = parent.image
                ? `<img src="${this._esc(this._productImageSrc(parent))}" alt="${this._esc(parent.name||'Produit')}" style="width:64px;height:64px;object-fit:contain;border-radius:10px;border:1px solid var(--border);background:#fff">`
                : `<div style="width:64px;height:64px;border-radius:10px;border:1px solid var(--border);display:flex;align-items:center;justify-content:center;color:var(--text-muted);background:var(--bg-alt)">📦</div>`;

            Modal.show('Dupliquer avec déclinaisons', `
                <div class="variant-wizard" style="display:grid;gap:14px;max-width:980px">
                    <div class="variant-parent-card" style="display:flex;gap:12px;align-items:center;border:1px solid var(--border);border-radius:12px;padding:12px">
                        ${imgHtml}
                        <div style="min-width:0">
                            <div style="font-weight:800;font-size:16px">${this._esc(parent.name||'Produit')}</div>
                            <div class="variant-muted" style="font-size:12px">${this._esc(parent.sku||'Sans SKU')} · ${this._esc(parent.category||'Sans catégorie')}</div>
                            <div class="variant-muted" style="font-size:12px;margin-top:3px">Les déclinaisons créées seront des produits stockables indépendants liés à ce parent.</div>
                        </div>
                    </div>

                    <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px" class="variant-wizard-grid">
                        <div>
                            <label class="lbl">Modèle de déclinaison</label>
                            <select class="sel" id="vdFamily">
                                <option value="">— Choisir un modèle —</option>
                                ${families.map(f=>`<option value="${this._esc(f.id)}">${this._esc(f.label)}${f.sourceType==='custom'?' · perso':' · officiel'}</option>`).join('')}
                            </select>
                            <small class="variant-muted">Utilisez un modèle officiel ou un modèle personnalisé déjà créé.</small>
                        </div>
                        <div>
                            <label class="lbl">Options de reprise</label>
                            <label class="chk"><input type="checkbox" id="vdCopyImage" ${parent.image?'checked':''}> Reprendre l'image du produit parent</label>
                            <label class="chk"><input type="checkbox" id="vdCopyMeta" checked> Reprendre catégorie, marque, fournisseur, documents, traçabilité et conditionnement</label>
                            <label class="chk"><input type="checkbox" id="vdCopyCost" checked> Reprendre le prix d'achat</label>
                        </div>
                    </div>

                    <div class="variant-values-panel" style="border:1px solid var(--border);border-radius:12px;padding:12px">
                        <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px">
                            <strong>Valeurs à générer</strong>
                            <button type="button" class="btn btn-sm btn-secondary" id="vdPreviewBtn">Prévisualiser</button>
                        </div>
                        <div id="vdAttributes" class="variant-muted" style="display:grid;gap:10px">Choisissez un modèle pour afficher ses attributs.</div>
                    </div>

                    <div class="variant-preview-panel" style="border:1px solid var(--border);border-radius:12px;padding:12px">
                        <strong>Prévisualisation</strong>
                        <div id="vdPreview" class="variant-muted" style="margin-top:8px">Aucune déclinaison préparée.</div>
                    </div>
                </div>
            `, [
                {label:'Annuler', cls:'btn-secondary', onclick:()=>Modal.close()},
                {label:'Créer les déclinaisons', cls:'btn-primary', onclick:()=>this.createVariantDuplicates(pid)}
            ], 'info');

            const familySel = document.getElementById('vdFamily');
            const attrBox = document.getElementById('vdAttributes');
            const previewBtn = document.getElementById('vdPreviewBtn');
            const renderAttributes = () => {
                const fam = families.find(f => f.id === familySel.value);
                if(!fam){ attrBox.innerHTML = 'Choisissez un modèle pour afficher ses attributs.'; this.previewVariantDuplicates(pid); return; }
                const attrs = Array.isArray(fam.attributes) ? fam.attributes : [];
                if(!attrs.length){ attrBox.innerHTML = '<em>Ce modèle ne contient pas encore d’attribut.</em>'; return; }
                attrBox.innerHTML = attrs.map(a => {
                    const key = this._esc(a.key||'');
                    const label = this._esc(a.label||a.key||'Attribut');
                    const unit = a.unit ? ` (${this._esc(a.unit)})` : '';
                    const presets = Array.isArray(a.presetValues) ? a.presetValues : [];
                    const chips = presets.length ? `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">${presets.map(v=>`<label class="variant-chip" style="display:inline-flex;align-items:center;gap:5px;border:1px solid var(--border);border-radius:999px;padding:5px 8px;cursor:pointer"><input type="checkbox" class="vdPreset" data-key="${key}" value="${this._esc(String(v))}"> ${this._esc(String(v))}${unit}</label>`).join('')}</div>` : '';
                    return `<div class="vd-attr" data-key="${key}">
                        <label class="lbl">${label}${unit}</label>
                        ${chips}
                        <input type="text" class="inp vdCustom" data-key="${key}" placeholder="Valeurs personnalisées séparées par virgules — ex. 450, 650, 850">
                    </div>`;
                }).join('');
                this.previewVariantDuplicates(pid);
            };
            if(familySel) familySel.onchange = renderAttributes;
            if(previewBtn) previewBtn.onclick = () => this.previewVariantDuplicates(pid);
            attrBox?.addEventListener('change', () => this.previewVariantDuplicates(pid));
            attrBox?.addEventListener('input', () => this.previewVariantDuplicates(pid));
            renderAttributes();
        } catch(e){
            console.error('openVariantDuplicateModal error:', e);
            toast(e.message || 'Erreur ouverture déclinaisons','error');
        }
    },

    _collectVariantDuplicateRows(parentId){
        const familyId = document.getElementById('vdFamily')?.value || '';
        const family = (this.variantFamilies || []).find(f => f.id === familyId);
        if(!family) return {family:null, rows:[], error:'Choisissez un modèle de déclinaison.'};
        const attrs = Array.isArray(family.attributes) ? family.attributes : [];
        const groups = [];
        for(const a of attrs){
            const key = a.key || '';
            if(!key) continue;
            const vals = [];
            document.querySelectorAll(`.vdPreset[data-key="${CSS.escape(key)}"]:checked`).forEach(el => vals.push(String(el.value).trim()));
            const custom = document.querySelector(`.vdCustom[data-key="${CSS.escape(key)}"]`)?.value || '';
            custom.split(/[,;\n]+/).map(v=>v.trim()).filter(Boolean).forEach(v => vals.push(v));
            const unique = [...new Set(vals.filter(Boolean))];
            if(unique.length) groups.push({attr:a, key, values:unique});
        }
        if(!groups.length) return {family, rows:[], error:'Sélectionnez au moins une valeur officielle ou personnalisée.'};
        let combos = [{}];
        for(const g of groups){
            const next = [];
            for(const combo of combos){
                for(const value of g.values){
                    next.push({...combo, [g.key]: value});
                }
            }
            combos = next;
        }
        if(combos.length > 80) return {family, rows:[], error:'Trop de déclinaisons à créer en une seule fois (maximum 80). Réduisez les valeurs.'};
        const rows = combos.map(attrsObj => {
            const label = this._buildVariantLabel(family, attrsObj) || Object.values(attrsObj).join(' · ');
            const skuSuffix = String(label || 'VAR').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-zA-Z0-9]+/g,'-').replace(/^-+|-+$/g,'').toUpperCase().slice(0, 32) || 'VAR';
            return {label, skuSuffix, attrs: attrsObj};
        });
        return {family, rows, error:''};
    },

    previewVariantDuplicates(parentId){
        const box = document.getElementById('vdPreview');
        if(!box) return;
        const parent = this._variantDuplicateParent || {};
        const {rows, error} = this._collectVariantDuplicateRows(parentId);
        if(error){ box.innerHTML = `<span class="variant-muted">${this._esc(error)}</span>`; return; }
        const sample = rows.slice(0, 12).map(r => `<div style="display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--border);padding:5px 0"><span>${this._esc((parent?.name||'Produit') + ' — ' + r.label)}</span><code>${this._esc((parent?.sku||'SKU') + '-' + r.skuSuffix)}</code></div>`).join('');
        box.innerHTML = `${sample}${rows.length>12?`<div style="margin-top:6px;color:var(--text-muted)">+ ${rows.length-12} autre(s)</div>`:''}<div style="margin-top:8px;font-weight:700">${rows.length} déclinaison(s) prête(s) à créer.</div>`;
    },

    async createVariantDuplicates(parentId){
        try {
            const all = await Api.products();
            const parent = all.find(x => String(x.id) === String(parentId));
            if(!parent){ toast('Produit parent introuvable','error'); return; }
            const {family, rows, error} = this._collectVariantDuplicateRows(parentId);
            if(error){ toast(error,'warning'); return; }
            const copyImage = !!document.getElementById('vdCopyImage')?.checked;
            const copyMeta = !!document.getElementById('vdCopyMeta')?.checked;
            const copyCost = !!document.getElementById('vdCopyCost')?.checked;
            let created = 0, skipped = 0, errors = 0;
            for(const row of rows){
                const exists = all.find(x => String(x.parentId||'') === String(parentId) && String(x.variantLabel||'') === String(row.label));
                if(exists){ skipped++; continue; }
                try {
                    await Api.prodAdd({
                        name: `${parent.name || 'Produit'} — ${row.label}`,
                        sku: `${parent.sku || 'SKU'+parent.id}-${row.skuSuffix}`,
                        barcode: '',
                        brand: copyMeta ? (parent.brand||'') : '',
                        category: copyMeta ? (parent.category||'') : '',
                        description: parent.description||'',
                        cost: copyCost ? (parseFloat(parent.cost)||0) : 0,
                        supplier: copyMeta ? (parent.supplier||'') : '',
                        supplierLinks: [],
                        allowedWarehouses: JSON.stringify(parent.allowedWarehouses||[]),
                        defaultWarehouse: Array.isArray(parent.allowedWarehouses) && parent.allowedWarehouses.length ? parent.allowedWarehouses[0] : (parent.defaultWarehouse||null),
                        unitType: copyMeta ? (parent.packBaseUnit||parent.unitType||'piece') : 'piece',
                        packBaseUnit: copyMeta ? (parent.packBaseUnit||parent.unitType||'piece') : 'piece',
                        packCartonQty: copyMeta ? (parent.packCartonQty||0) : 0,
                        packPaletteQty: copyMeta ? (parent.packPaletteQty||0) : 0,
                        packBidonQty: copyMeta ? (parent.packBidonQty||0) : 0,
                        packFutQty: copyMeta ? (parent.packFutQty||0) : 0,
                        packAerosolQty: copyMeta ? (parent.packAerosolQty||0) : 0,
                        packSacQty: copyMeta ? (parent.packSacQty||0) : 0,
                        packSeauQty: copyMeta ? (parent.packSeauQty||0) : 0,
                        packRouleauQty: copyMeta ? (parent.packRouleauQty||0) : 0,
                        packBobineQty: copyMeta ? (parent.packBobineQty||0) : 0,
                        packTubeQty: copyMeta ? (parent.packTubeQty||0) : 0,
                        packBoiteQty: copyMeta ? (parent.packBoiteQty||0) : 0,
                        packPaquetQty: copyMeta ? (parent.packPaquetQty||0) : 0,
                        safetyStock: copyMeta ? parent.safetyStock : null,
                        trackLot: copyMeta ? !!parent.trackLot : false,
                        trackExpiry: copyMeta ? !!parent.trackExpiry : false,
                        shelfLifeDays: copyMeta ? parent.shelfLifeDays : null,
                        datasheetUrl: copyMeta ? (parent.datasheetUrl||'') : '',
                        msdsUrl: copyMeta ? (parent.msdsUrl||'') : '',
                        eans: [],
                        decompSub: copyMeta ? !!parent.decompSub : false,
                        sizeType: parent.sizeType||'none',
                        sizeNorm: parent.sizeNorm||'eu',
                        size: row.attrs.size || '',
                        parentId: parseInt(parentId),
                        isVariantParent: false,
                        variantFamily: family.id,
                        variantLabel: row.label,
                        variantAttributes: row.attrs,
                        variantSort: created,
                        image: copyImage ? (parent.image||'') : '',
                    });
                    created++;
                } catch(e){
                    errors++;
                    console.error('Variant duplicate create error:', e, row);
                }
            }
            if(created > 0){
                try { await Api.prodEdit({ id: parseInt(parentId), isVariantParent: true, variantFamily: family.id }); } catch(e){}
                toast(`${created} déclinaison(s) créée(s)` + (skipped ? ` · ${skipped} déjà existante(s)` : '') + (errors ? ` · ${errors} erreur(s)` : ''), 'success');
                Modal.close();
                await this.load();
                return;
            }
            toast(skipped ? 'Aucune nouvelle déclinaison créée : elles existent déjà.' : 'Aucune déclinaison créée.', skipped ? 'warning' : 'error');
        } catch(e){
            console.error('createVariantDuplicates error:', e);
            toast(e.message || 'Erreur création déclinaisons','error');
        }
    },

    // ════════════════════════════════════════════════════════════════
    // GÉNÉRER LES DÉCLINAISONS PAR TAILLE (EPI)
    // ════════════════════════════════════════════════════════════════
    async generateSizeVariants(parentId, sizes){
        if(!sizes || !sizes.length){ toast('Aucune taille sélectionnée','warning'); return; }
        const all = await Api.products();
        const parent = all.find(x=>String(x.id)===String(parentId));
        if(!parent){ toast('Produit parent introuvable','error'); return; }

        let created = 0, errors = 0;
        for(const sz of sizes){
            const suffix = '-' + sz.replace(/[^a-zA-Z0-9.]/g,'').toUpperCase();
            const childName = parent.name + ' — ' + sz;
            const childSku = (parent.sku||'') + suffix;
            // Vérifier si cette déclinaison existe déjà
            const exists = all.find(x => x.parentId && String(x.parentId)===String(parentId) && x.size===sz);
            if(exists){ continue; } // skip sans compter comme erreur
            try {
                await Api.prodAdd({
                    name: childName,
                    sku: childSku,
                    barcode: (parent.barcode||'') ? (parent.barcode + suffix) : '',
                    brand: parent.brand||'',
                    category: parent.category||'',
                    description: parent.description||'',
                    cost: parseFloat(parent.cost)||0,
                    supplier: parent.supplier||'',
                    supplierLinks: [],
                    allowedWarehouses: JSON.stringify(parent.allowedWarehouses||[]),
                    defaultWarehouse: (Array.isArray(parent.allowedWarehouses)&&parent.allowedWarehouses.length) ? parent.allowedWarehouses[0] : null,
                    unitType: parent.packBaseUnit||parent.unitType||'piece',
                    packBaseUnit: parent.packBaseUnit||parent.unitType||'piece',
                    packCartonQty: parent.packCartonQty||0,
                    packPaletteQty: parent.packPaletteQty||0,
                    packBidonQty: parent.packBidonQty||0,
                    packFutQty: parent.packFutQty||0,
                    packAerosolQty: parent.packAerosolQty||0,
                    packSacQty: parent.packSacQty||0,
                    packSeauQty: parent.packSeauQty||0,
                    packRouleauQty: parent.packRouleauQty||0,
                    packBobineQty: parent.packBobineQty||0,
                    packTubeQty: parent.packTubeQty||0,
                    packBoiteQty: parent.packBoiteQty||0,
                    packPaquetQty: parent.packPaquetQty||0,
                    safetyStock: parent.safetyStock,
                    trackLot: parent.trackLot||false,
                    trackExpiry: parent.trackExpiry||false,
                    shelfLifeDays: parent.shelfLifeDays,
                    datasheetUrl: parent.datasheetUrl||'',
                    msdsUrl: parent.msdsUrl||'',
                    eans: [],
                    decompSub: parent.decompSub||false,
                    sizeType: parent.sizeType||'none',
                    sizeNorm: parent.sizeNorm||'eu',
                    size: sz,
                    parentId: parseInt(parentId),
                    image: parent.image||'',
                });
                created++;
            } catch(e){ errors++; console.error('Variant create error:', e, sz); }
        }
        toast(created + ' déclinaison(s) créée(s)' + (errors>0 ? ' — '+errors+' erreur(s)':''), created>0?'success':'error');
        Modal.close();
        this.load();
    },

    // ════════════════════════════════════════════════════════════════
    // HELPERS EPI/TAILLES
    // ════════════════════════════════════════════════════════════════
    _getSizesForType(sizeType, sizeNorm){
        const map = {
            clothing: 'XS,S,M,L,XL,XXL,3XL,4XL',
            pants: { eu:'34,36,38,40,42,44,46,48,50,52,54', uk:'24,26,28,30,32,34,36,38,40,42', us:'24,26,28,30,32,34,36,38,40,42' },
            shoes: { eu:'36,37,38,39,40,41,42,43,44,45,46,47,48', uk:'3,3.5,4,4.5,5,5.5,6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12,13', us:'4,4.5,5,5.5,6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12,12.5,13,14' },
            gloves: '6,7,8,9,10,11,12',
            headwear: 'S,M,L,XL,Unique',
            helmet: 'S,M,L,XL,Unique',
            eyewear: 'Unique,S,M,L',
            baby: '0M,3M,6M,9M,12M,18M,24M,2A,3A,4A,5A,6A,8A,10A,12A,14A,16A',
        };
        const v = map[sizeType];
        if(!v) return [];
        if(typeof v === 'string') return v.split(',');
        return (v[sizeNorm||'eu']||v.eu||'').split(',');
    },

    // ── CSV Import ──────────────────────────────────────────────────────────
    openImportModal() {
        if (!window.Modal || typeof Modal.show !== 'function') {
            toast('La fenêtre d’import est indisponible. Rechargez la page.', 'error');
            return;
        }

        const body = `
            <div data-modal-no-autofocus="1" class="csv-import-dialog">
                <div style="background:var(--bg2,var(--bg-elevated));border:1px solid var(--border);border-radius:8px;padding:1rem;margin-bottom:1rem;font-size:.85rem">
                    <strong>Format (séparateur ; ou ,) :</strong><br>
                    <code>name;sku;barcode;brand;category;description;cost;unitType;safetyStock</code><br>
                    Colonne obligatoire : <strong>name</strong> — Les catégories inexistantes sont créées automatiquement lors de l’import.
                </div>
                <div style="margin-bottom:1rem">
                    <label class="form-label" for="csvFileInput">Fichier CSV *</label>
                    <input type="file" id="csvFileInput" class="form-control" accept=".csv,.txt,text/csv,text/plain">
                </div>
                <div id="csvPreviewResult" role="status" aria-live="polite" style="max-height:280px;overflow:auto"></div>
            </div>`;

        Modal.show('Import produits CSV', body, [
            { label:'Vérifier le fichier', cls:'btn-secondary', onclick:() => ProdPage.runImport('dryrun') },
            { label:'Importer', cls:'btn-primary', onclick:() => ProdPage.runImport('import') },
            { label:'Annuler', cls:'btn-secondary', onclick:() => Modal.close() },
        ], 'info');

        const footerButtons = Array.from(document.querySelectorAll('#modalFooter button'));
        this._csvImportButton = footerButtons.find(button => button.textContent.trim() === 'Importer') || null;
        if (this._csvImportButton) {
            this._csvImportButton.hidden = true;
            this._csvImportButton.disabled = true;
        }
        const fileInput = document.getElementById('csvFileInput');
        if (fileInput) {
            fileInput.addEventListener('change', () => {
                const preview = document.getElementById('csvPreviewResult');
                if (preview) preview.innerHTML = '';
                if (this._csvImportButton) {
                    this._csvImportButton.hidden = true;
                    this._csvImportButton.disabled = true;
                }
            });
        }
    },

    async runImport(mode) {
        const file = document.getElementById('csvFileInput')?.files?.[0];
        if (!file) { toast('Sélectionnez un fichier CSV', 'error'); return; }
        const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
        const fd = new FormData();
        fd.append('csv', file);
        fd.append('mode', mode);
        const previewEl = document.getElementById('csvPreviewResult');
        previewEl.innerHTML = '<p>Traitement…</p>';
        try {
            const resp = await fetch(Api.url('api/import.php'), { method:'POST', headers:{'X-CSRF-TOKEN':csrfToken}, credentials:'same-origin', body:fd });
            const raw = await resp.text();
            let data = null;
            try { data = JSON.parse(raw); }
            catch (_) { throw new Error('Réponse serveur invalide pendant l’import CSV.'); }
            if (!resp.ok) throw new Error(data.error || 'Erreur pendant l’import CSV.');
            const summary = `<div style="display:flex;gap:1rem;margin-bottom:.5rem;flex-wrap:wrap;font-size:.85rem">
                <span style="color:var(--green)">✅ Créés : <strong>${data.created}</strong></span>
                <span style="color:var(--orange)">⏭ Ignorés : <strong>${data.skipped}</strong></span>
                <span style="color:var(--red)">❌ Erreurs : <strong>${data.errors}</strong></span>
            </div>`;
            const rows = (data.results||[]).slice(0,50).map(r=>{
                const c=r.status==='ok'?'var(--green)':r.status==='skip'?'var(--orange)':'var(--red)';
                const i=r.status==='ok'?'✅':r.status==='skip'?'⏭':'❌';
                return `<div style="color:${c};font-size:.8rem">L.${r.line} ${i} ${r.name} — ${r.message}</div>`;
            }).join('');
            previewEl.innerHTML = summary + rows;
            if (mode==='dryrun') {
                if (this._csvImportButton) {
                    this._csvImportButton.hidden = !(data.created > 0);
                    this._csvImportButton.disabled = !(data.created > 0);
                }
            } else {
                toast(data.created+' produit(s) importé(s)','success');
                Modal.close();
                this._csvImportButton = null;
                await this.load();
            }
        } catch(e) { previewEl.innerHTML = `<p style="color:var(--red)">${e.message}</p>`; }
    },

    // ──────────────────────────────────────────────────────────────
    // Boutons d'effacement des recherches : utile PC, tablette et mobile.
    _initSearchClearButtons(){
        const root = document.getElementById('pgProducts') || document;
        const searchInputs = root.querySelectorAll('.search-box input[type="text"], .search-box input[type="search"], input[data-clearable-search="true"]');
        searchInputs.forEach(input=>{
            const box = input.closest('.search-box') || input.parentElement;
            if(!box || input._searchClearBound) return;
            input._searchClearBound = true;
            let btn = box.querySelector('.search-clear-btn');
            if(!btn){
                btn = document.createElement('button');
                btn.type = 'button';
                btn.className = 'search-clear-btn';
                btn.setAttribute('aria-label', 'Effacer la recherche');
                btn.setAttribute('title', 'Effacer la recherche');
                btn.textContent = '×';
                box.appendChild(btn);
            }
            btn.addEventListener('click', (e)=>{
                e.preventDefault();
                input.value = '';
                input.dispatchEvent(new Event('input', { bubbles:true }));
                input.focus();
            });
            input.addEventListener('input', ()=>this._syncSearchClearButtons());
        });
        this._syncSearchClearButtons();
    },

    _syncSearchClearButtons(){
        const root = document.getElementById('pgProducts') || document;
        root.querySelectorAll('.search-box input[type="text"], .search-box input[type="search"], input[data-clearable-search="true"]').forEach(input=>{
            const box = input.closest('.search-box') || input.parentElement;
            if(box) box.classList.toggle('has-value', !!String(input.value||'').trim());
        });
    },

    // ──────────────────────────────────────────────────────────────
    // Barre de filtres intelligente : compacte au scroll du vrai conteneur (#appContent)
    _initSmartProductFilters(){
        const bar = document.querySelector('#pgProducts .prod-filters-bar');
        if(!bar || bar._smartFiltersBound) return;
        bar._smartFiltersBound = true;

        const page = document.getElementById('pgProducts');
        const toggle = document.getElementById('prodFilterToggle');
        const search = document.getElementById('prodSearch');
        const contentScroller = document.getElementById('appContent') || document.querySelector('.main-content');
        const scrollTargets = [contentScroller, window].filter(Boolean);
        let lastY = 0;
        let isCompact = false;
        let ticking = false;
        let expandedHeight = Math.ceil(bar.getBoundingClientRect().height);
        let interactionLockUntil = 0;

        const isMobileFilters = ()=>{
            try { return window.matchMedia && window.matchMedia('(max-width: 760px)').matches; }
            catch(_) { return (window.innerWidth || 0) <= 760; }
        };

        const getScrollY = ()=>{
            if(contentScroller && contentScroller !== window) return contentScroller.scrollTop || 0;
            return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
        };

        const syncFlowCompensation = ()=>{
            if(!isCompact){
                bar.style.setProperty('--prod-filter-flow-compensation', '0px');
                return;
            }
            const currentHeight = Math.ceil(bar.getBoundingClientRect().height);
            bar.style.setProperty('--prod-filter-flow-compensation', `${Math.max(0, expandedHeight - currentHeight)}px`);
        };

        const setExpanded = (expanded)=>{
            interactionLockUntil = Date.now() + 500;
            bar.classList.toggle('is-expanded', !!expanded);
            if(toggle) toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
            window.requestAnimationFrame(syncFlowCompensation);
        };

        const setCompact = (compact)=>{
            if(compact === isCompact) return;
            if(!isCompact) expandedHeight = Math.max(expandedHeight, Math.ceil(bar.getBoundingClientRect().height));
            isCompact = compact;
            bar.classList.toggle('is-compact', isCompact);
            if(page) page.classList.toggle('is-prod-filter-compact', isCompact);
            if(!isCompact) setExpanded(false);
            window.requestAnimationFrame(syncFlowCompensation);
        };

        const update = ()=>{
            ticking = false;
            if(isMobileFilters()){
                setCompact(false);
                lastY = getScrollY();
                return;
            }

            const y = getScrollY();
            const shouldCompact = isCompact ? y > 20 : y > 72;
            setCompact(shouldCompact);

            const interacting = Date.now() < interactionLockUntil || bar.contains(document.activeElement);
            if(isCompact && !interacting && y > lastY + 4) setExpanded(false);
            lastY = y;
        };

        const requestUpdate = ()=>{
            if(!ticking){
                window.requestAnimationFrame(update);
                ticking = true;
            }
        };

        scrollTargets.forEach(target=>target.addEventListener('scroll', requestUpdate, { passive:true }));
        window.addEventListener('resize', ()=>{
            if(!isCompact) expandedHeight = Math.ceil(bar.getBoundingClientRect().height);
            requestUpdate();
        }, { passive:true });

        bar.addEventListener('pointerdown', ()=>{ interactionLockUntil = Date.now() + 700; }, { passive:true });

        if(toggle){
            toggle.addEventListener('click', (e)=>{
                e.preventDefault();
                e.stopPropagation();
                setExpanded(!bar.classList.contains('is-expanded'));
            });
        }

        if(search){
            search.addEventListener('focus', ()=>{ interactionLockUntil = Date.now() + 1200; });
            search.addEventListener('input', ()=>{ interactionLockUntil = Date.now() + 350; });
        }

        bar.querySelectorAll('select').forEach(select=>{
            select.addEventListener('change', ()=>{
                if(bar.classList.contains('is-compact')) setExpanded(false);
            });
        });

        bar.addEventListener('focusin', (e)=>{
            interactionLockUntil = Date.now() + 900;
            if(bar.classList.contains('is-compact') && !e.target.closest('.search-box') && !e.target.closest('.prod-filter-toggle')) setExpanded(true);
        });

        update();
        setTimeout(update, 150);
    },

    // ──────────────────────────────────────────────────────────────
    // Zoom image : uniquement clic/tap/clavier. Plus aucun déclenchement au survol.
    _initImageZoom(){
        if(!document.getElementById('imgZoomOverlay')){
            const ov = document.createElement('div');
            ov.id = 'imgZoomOverlay';
            ov.setAttribute('aria-hidden', 'true');
            ov.innerHTML = `
                <div id="imgZoomBg" role="presentation"></div>
                <img id="imgZoomImg" src="" alt="">
                <button id="imgZoomClose" type="button" aria-label="Fermer l'image agrandie" title="Fermer">✕</button>
            `;
            document.body.appendChild(ov);
            document.getElementById('imgZoomClose').addEventListener('click', ()=>ProdPage._closeZoom());
            document.getElementById('imgZoomBg').addEventListener('click', ()=>ProdPage._closeZoom());
            document.addEventListener('keydown', e=>{ if(e.key==='Escape') ProdPage._closeZoom(); });
        }

        const pg = document.getElementById('pgProducts');
        if(!pg || pg._imageClickDelegated) return;
        pg._imageClickDelegated = true;

        const openFromThumb = (thumb, event)=>{
            const img = thumb.querySelector('img');
            if(!img) return;
            if(event){
                event.preventDefault();
                event.stopPropagation();
            }
            ProdPage._openZoom(img.currentSrc || img.src, img.alt);
        };

        pg.addEventListener('click', e=>{
            // Les actions superposées aux cartes produits (mouvement rapide, duplication,
            // cases à cocher, menus) ne doivent jamais ouvrir le zoom de l'image.
            if(e.target.closest('button,a,input,select,textarea,label,[data-no-image-zoom],.pc2-qa,.rowacts,.prod-card-select')) return;
            const thumb = e.target.closest('.prod-thumb-zoomable, .prod-list-img-trigger');
            if(!thumb || !pg.contains(thumb)) return;
            openFromThumb(thumb, e);
        }, true);

        pg.addEventListener('keydown', e=>{
            if(e.key !== 'Enter' && e.key !== ' ') return;
            if(e.target.closest('button,a,input,select,textarea,label,[data-no-image-zoom],.pc2-qa,.rowacts,.prod-card-select')) return;
            const thumb = e.target.closest('.prod-thumb-zoomable, .prod-list-img-trigger');
            if(!thumb || !pg.contains(thumb)) return;
            openFromThumb(thumb, e);
        }, true);
    },

    _openZoom(src, alt){
        if(!src || src.endsWith('undefined') || src === window.location.href) return;
        const ov  = document.getElementById('imgZoomOverlay');
        const img = document.getElementById('imgZoomImg');
        if(!ov || !img) return;
        img.src = src;
        img.alt = alt || 'Image produit';
        ov.classList.add('visible');
        ov.setAttribute('aria-hidden', 'false');
        document.body.classList.add('prod-img-zoom-open');
        const close = document.getElementById('imgZoomClose');
        if(close) close.focus({ preventScroll:true });
    },

    _closeZoom(){
        const ov = document.getElementById('imgZoomOverlay');
        if(ov){
            ov.classList.remove('visible');
            ov.setAttribute('aria-hidden', 'true');
        }
        document.body.classList.remove('prod-img-zoom-open');
    },

    // Appelé après chaque rendu pour garantir l'accessibilité des miniatures.
    _attachImageClick(){
        document.querySelectorAll('#pgProducts .prod-thumb-zoomable, #pgProducts .prod-list-img-trigger').forEach(thumb=>{
            const img = thumb.querySelector('img');
            if(!img) return;
            if(!thumb.hasAttribute('role')) thumb.setAttribute('role', 'button');
            if(!thumb.hasAttribute('tabindex')) thumb.setAttribute('tabindex', '0');
            if(!thumb.hasAttribute('title')) thumb.setAttribute('title', 'Agrandir l\'image');
            if(!thumb.hasAttribute('aria-label')) thumb.setAttribute('aria-label', 'Agrandir l\'image du produit '+(img.alt || ''));
        });
    },

};
