// Construit et imaginé par Jean-François Avart
(() => {
  // ============================================================
  // DÉTECTION MODE DEMO
  // ============================================================
  // Le mode DEMO est activé via une meta tag dans index.html:
  // <meta name="csi-mode" content="demo">
  const DEMO_MODE = (() => {
    const meta = document.querySelector('meta[name="csi-mode"]');
    return meta && meta.content === 'demo';
  })();

  // URL du site pour télécharger l'application complète
  const DEMO_DOWNLOAD_URL = 'https://cyber-assistant.com/client_web.zip';
  const DEMO_WEBSITE_URL = 'https://cyber-assistant.com';
  const DEMO_CONTACT_EMAIL = 'info@cyber-assistant.com';

  // Configuration mode DEMO
  const DEMO_AUTO_RELOAD_INTERVAL = 60 * 60 * 1000; // 1 heure en millisecondes
  let demoAutoReloadTimer = null;

  // Log mode DEMO si activé
  if (DEMO_MODE) {
    void 0;
    void 0;
  }

  /**
   * Affiche un message de blocage pour les fonctions désactivées en mode démo
   * @param {string} featureName - Nom de la fonctionnalité bloquée
   */
  function showDemoBlockedMessage(featureName = t('demo.thisFeature', 'Cette fonction')) {
    if (typeof showToast2 === 'function') {
      showToast2(`${featureName} ${t('demo.disabledInDemo', 'est désactivée en mode démonstration.')}`, 'warning');
    } else {
      void 0;
    }
  }

  /**
   * Démarre le timer de rechargement automatique des données démo
   */
  function startDemoAutoReload() {
    if (!DEMO_MODE) return;

    // Arrêter tout timer existant
    if (demoAutoReloadTimer) {
      clearInterval(demoAutoReloadTimer);
    }

    // Démarrer le rechargement automatique toutes les heures
    demoAutoReloadTimer = setInterval(() => {
      void 0;
      if (typeof loadDemoDataSilent === 'function') {
        loadDemoDataSilent();
        if (typeof showToast2 === 'function') {
          showToast2(t('toasts.demoDataReloaded'), 'info');
        }
      }
    }, DEMO_AUTO_RELOAD_INTERVAL);

    void 0;
  }

  /**
   * Arrête le timer de rechargement automatique
   */
  function stopDemoAutoReload() {
    if (demoAutoReloadTimer) {
      clearInterval(demoAutoReloadTimer);
      demoAutoReloadTimer = null;
      void 0;
    }
  }

  // ============================================================
  // PROTECTIONS ANTI-ABUS (MODE DEMO)
  // ============================================================

  // Rate limiting pour les actions fréquentes
  const demoRateLimiter = {
    actions: new Map(),
    maxActions: 100,        // Max 100 actions par fenêtre
    windowMs: 60000,        // Fenêtre de 1 minute
    cooldownMs: 5000,       // Pause de 5 secondes si dépassé

    /**
     * Vérifie si une action est autorisée (rate limiting)
     * @param {string} actionType - Type d'action (save, create, update, etc.)
     * @returns {boolean} - true si l'action est autorisée
     */
    checkAction(actionType = 'default') {
      if (!DEMO_MODE) return true;

      const now = Date.now();
      const key = actionType;

      if (!this.actions.has(key)) {
        this.actions.set(key, { count: 0, windowStart: now, blocked: false });
      }

      const action = this.actions.get(key);

      // Réinitialiser la fenêtre si expirée
      if (now - action.windowStart > this.windowMs) {
        action.count = 0;
        action.windowStart = now;
        action.blocked = false;
      }

      // Vérifier le cooldown
      if (action.blocked && now - action.blockedAt < this.cooldownMs) {
        return false;
      } else if (action.blocked) {
        action.blocked = false;
      }

      // Vérifier le rate limit
      if (action.count >= this.maxActions) {
        action.blocked = true;
        action.blockedAt = now;
        void 0;
        return false;
      }

      action.count++;
      return true;
    },

    /**
     * Affiche un message de rate limit
     */
    showRateLimitMessage() {
      if (typeof showToast2 === 'function') {
        showToast2(t('toasts.tooManyActions'), 'warning');
      }
    }
  };

  // Limite de taille des données en mode démo (protection mémoire)
  const DEMO_MAX_ITEMS = {
    controls: 200,
    actions: 500,
    risks: 200,
    threats: 200,
    nonconformities: 100,
    audits: 50,
    documents: 100,
    stakeholders: 100,
    criticalAssets: 100,
    projectRisks: 100
  };

  /**
   * Vérifie les limites de données en mode démo
   * @param {string} dataType - Type de données
   * @param {number} currentCount - Nombre actuel d'éléments
   * @returns {boolean} - true si on peut ajouter
   */
  function checkDemoDataLimit(dataType, currentCount) {
    if (!DEMO_MODE) return true;

    const limit = DEMO_MAX_ITEMS[dataType];
    if (!limit) return true;

    if (currentCount >= limit) {
      showDemoBlockedMessage(t('demo.limitReached', 'Limite atteinte') + ` (${limit} ${dataType} ` + t('demo.maxInDemo', 'maximum en mode démo') + `)`);
      return false;
    }
    return true;
  }

  /**
   * Protection contre le localStorage flooding
   * @returns {boolean} - true si on peut sauvegarder
   */
  function checkDemoStorageLimit() {
    if (!DEMO_MODE) return true;

    try {
      // Estimer la taille actuelle du localStorage
      let totalSize = 0;
      for (let key in localStorage) {
        if (localStorage.hasOwnProperty(key)) {
          totalSize += localStorage[key].length * 2; // UTF-16 = 2 bytes par char
        }
      }

      // Limite à 4MB en mode démo (sur les 5MB typiques)
      const MAX_DEMO_STORAGE = 4 * 1024 * 1024;
      if (totalSize > MAX_DEMO_STORAGE) {
        void 0;
        showDemoBlockedMessage(t('demo.storageLimitReached', 'Limite de stockage atteinte en mode démo'));
        return false;
      }
      return true;
    } catch (e) {
      return true; // En cas d'erreur, autoriser
    }
  }

  // Canonical domain values (used for data storage - do NOT translate)
  const DOMAIN_BY_PREFIX = {
    '5': 'Organisationnel',
    '6': 'Humain',
    '7': 'Physique',
    '8': 'Technologique'
  };
  // Translated domain display labels
  const DOMAIN_DISPLAY_KEYS = {
    'Organisationnel': 'domains.organisationnel',
    'Humain': 'domains.humain',
    'Physique': 'domains.physique',
    'Technologique': 'domains.technologique',
    'Non classé': 'domains.nonClasse'
  };
  function getTranslatedDomain(canonicalValue) {
    const key = DOMAIN_DISPLAY_KEYS[canonicalValue];
    return key ? t(key) : canonicalValue;
  }
  // Resolve ISO 27002 control title/description based on current language
  function isoCtrlTitle(control) {
    var lang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : 'fr';
    return (lang !== 'fr' && control.titleEn) ? control.titleEn : (control.title || '');
  }
  function isoCtrlDesc(control) {
    var lang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : 'fr';
    return (lang !== 'fr' && control.descriptionEn) ? control.descriptionEn : (control.measure || control.description || '');
  }
  // Resolve ISO 27002 category label based on current language
  function isoCatLabel(cat) {
    var lang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : 'fr';
    return (lang !== 'fr' && cat.labelEn) ? cat.labelEn : cat.label;
  }
  const normalizeCapabilityKey = (value) =>
    value
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .toLowerCase()
      .replace(/[\s'']+/g, "_")
      .replace(/_+/g, "_");
  const getIso27002DomainFromValue = (value) => {
    const text = String(value || "").trim();
    if (!text) return "";
    const match = text.match(/(^|[^0-9])([5-8])\s*(?:\.|$)/);
    if (!match) return "";
    return DOMAIN_BY_PREFIX[match[2]] || "";
  };
  const inferIso27002Category = (control = {}) => {
    const fromNumber = getIso27002DomainFromValue(control.numero);
    if (fromNumber) return fromNumber;
    const fromReference = getIso27002DomainFromValue(control.reference);
    if (fromReference) return fromReference;
    const title = String(control.title || "").trim();
    if (title && typeof iso27002_data_default !== "undefined") {
      const match = iso27002_data_default.find((item) => item.numero === control.numero || item.titre === title);
      if (match) {
        const prefix = String(match.numero || "").split(".")[0];
        return DOMAIN_BY_PREFIX[prefix] || "";
      }
    }
    return "Non classé";
  };

  // Génération d'ID unique avec crypto.randomUUID() et fallback
  const generateId = (prefix = '') => {
    let uuid;
    if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
      uuid = crypto.randomUUID();
    } else {
      // Fallback pour navigateurs anciens
      uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    }
    return prefix ? `${prefix}-${uuid}` : uuid;
  };

  // Accès sécurisé aux éléments DOM
  const safeSetText = (id, text) => {
    const el = document.getElementById(id);
    if (el) el.textContent = text;
  };


  const safeSetHTML = (id, html) => {
    const el = document.getElementById(id);
    if (el) el.textContent = html;
  };

  const safeAddClass = (id, className) => {
    const el = document.getElementById(id);
    if (el) el.classList.add(className);
  };

  const safeRemoveClass = (id, className) => {
    const el = document.getElementById(id);
    if (el) el.classList.remove(className);
  };

  const CAPABILITY_I18N_KEYS = [
    { raw: "Gouvernance", i18n: "capabilities.gouvernance" },
    { raw: "Gestion_des_actifs", i18n: "capabilities.gestionActifs" },
    { raw: "Protection_des_informations", i18n: "capabilities.protectionInformations" },
    { raw: "Sécurité_des_ressources_humaines", i18n: "capabilities.securiteRH" },
    { raw: "Sécurité_physique", i18n: "capabilities.securitePhysique" },
    { raw: "Sécurité_système_et_réseau", i18n: "capabilities.securiteSystemeReseau" },
    { raw: "Sécurité_des_applications", i18n: "capabilities.securiteApplications" },
    { raw: "Configuration_sécurisée", i18n: "capabilities.configurationSecurisee" },
    { raw: "Gestion_des_identités_et_des_accès", i18n: "capabilities.gestionIdentitesAcces" },
    { raw: "Gestion_des_menaces_et_des_vulnérabilités", i18n: "capabilities.gestionMenacesVulnerabilites" },
    { raw: "Continuité", i18n: "capabilities.continuite" },
    { raw: "Sécurité_des_relations_fournisseurs", i18n: "capabilities.securiteRelationsFournisseurs" },
    { raw: "Réglementation_et_conformité", i18n: "capabilities.reglementationConformite" },
    { raw: "Gestion_des_événements_de_sécurité_de_l'information", i18n: "capabilities.gestionEvenementsSI" },
    { raw: "Assurance_de_sécurité_de_l'information", i18n: "capabilities.assuranceSI" }
  ];
  function getOperationalCapabilities() {
    return CAPABILITY_I18N_KEYS.map(item => ({
      key: normalizeCapabilityKey(item.raw),
      label: t(item.i18n)
    }));
  }
  let OPERATIONAL_CAPABILITIES = getOperationalCapabilities();
  const NIS2_CATEGORIES = [
    'ID.AM', 'ID.BE', 'ID.GV', 'ID.RA', 'ID.RM', 'ID.SC',
    'PR.AC', 'PR.AT', 'PR.DS', 'PR.IP', 'PR.MA', 'PR.PT',
    'DE.AE', 'DE.CM', 'DE.DP',
    'RS.RP', 'RS.CO', 'RS.AN', 'RS.MI', 'RS.IM',
    'RC.RP', 'RC.IM', 'RC.CO'
  ];
  let currentTheme = "poudre";
  const THEME_STORAGE_KEY = "smsi_theme";
  function getThemeOptions() {
    return [
      { value: "light", label: t('settings.theme_clair', 'Clair') },
      { value: "dark", label: t('settings.theme_sombre', 'Sombre') },
      { value: "poudre", label: t('settings.theme_poudre', 'Poudre') },
      { value: "bleu", label: t('settings.theme_bleu', 'Bleu') }
    ];
  }
  const THEME_OPTIONS = getThemeOptions();
  // Lire le thème par défaut depuis la balise meta (pour branding) ou utiliser poudre
  const getDefaultTheme = () => {
    const metaTheme = document.querySelector('meta[name="csi-default-theme"]');
    if (metaTheme && metaTheme.content) {
      return metaTheme.content;
    }
    return "poudre";
  };
  const DEFAULT_THEME = getDefaultTheme();
  const normalizeTheme = (value) => {
    if (!value) return DEFAULT_THEME;
    return THEME_OPTIONS.some((opt) => opt.value === value) ? value : DEFAULT_THEME;
  };
  // ============================================================
  // DOCUMENT_REVIEW_ITEMS: loaded from iso27001 or open-security-management framework
  // Builds the same structure as the previously hardcoded array for backward compatibility.
  // Reference format: "4.1 Title" (composite string used as data key in appData.documentReview)
  // ============================================================
  function _getActiveManagementFramework() {
    var fws = window._csiFrameworks || {};
    if (fws['iso27001'] && _isFrameworkAllowed('iso27001')) return fws['iso27001'];
    if (fws['open-security-management'] && _isFrameworkAllowed('open-security-management')) return fws['open-security-management'];
    return null;
  }
  function _getActiveManagementFrameworkId() {
    var fws = window._csiFrameworks || {};
    if (fws['iso27001'] && _isFrameworkAllowed('iso27001')) return 'iso27001';
    if (fws['open-security-management'] && _isFrameworkAllowed('open-security-management')) return 'open-security-management';
    return null;
  }
  function _isFrameworkAllowed(id) {
    var ed = window._csiEdition;
    if (ed && ed.forcedFrameworks && Array.isArray(ed.frameworks)) {
      return ed.frameworks.indexOf(id) !== -1;
    }
    return true;
  }
  function _getActiveControlsFramework() {
    var fws = window._csiFrameworks || {};
    if (fws['iso27002'] && _isFrameworkAllowed('iso27002')) return fws['iso27002'];
    if (fws['open-security-controls'] && _isFrameworkAllowed('open-security-controls')) return fws['open-security-controls'];
    return null;
  }
  function _getActiveControlsFrameworkId() {
    var fws = window._csiFrameworks || {};
    if (fws['iso27002'] && _isFrameworkAllowed('iso27002')) return 'iso27002';
    if (fws['open-security-controls'] && _isFrameworkAllowed('open-security-controls')) return 'open-security-controls';
    return null;
  }
  /** Returns the correct default enabledFrameworks list based on loaded edition */
  function _defaultEnabledFrameworks() {
    var ed = window._csiEdition;
    if (ed && ed.forcedFrameworks && Array.isArray(ed.frameworks)) {
      // Edition avec frameworks forcés → utiliser uniquement la liste de l'édition
      var list = [];
      var fwReg = window._csiFrameworks || {};
      ed.frameworks.forEach(function(fwId) {
        if (fwReg[fwId]) list.push(fwId);
      });
      return list;
    }
    return [_getActiveControlsFrameworkId() || 'iso27002', 'nis2'];
  }
  const DOCUMENT_REVIEW_ITEMS = (function() {
    var mgmtFw = _getActiveManagementFramework();
    if (!mgmtFw || !Array.isArray(mgmtFw.clauses)) return [];
    var chapters = mgmtFw.chapters || [];
    return mgmtFw.clauses.map(function(clause) {
      if (clause.type === 'chapter') {
        var ch = chapters.find(function(c) { return c.id === clause.chapterId; });
        return { type: 'chapter', title: ch ? ch.title : '' };
      }
      return {
        reference: clause.reference + ' ' + clause.title,
        description: clause.description
      };
    });
  })();
  const createDefaultDocumentReview = (source = []) => {
    const src = Array.isArray(source) ? source : [];
    // B5 garde-fou : si le référentiel de management n'est pas chargé (échec de
    // script, package incomplet), DOCUMENT_REVIEW_ITEMS est vide → ne JAMAIS
    // écraser des données existantes par une liste vide (perte totale sinon).
    if (!DOCUMENT_REVIEW_ITEMS.some(it => it.reference)) return src;
    const map = new Map(src.map(item => [item.reference, item]));
    // B5 repli : correspondance par numéro de clause (préfixe avant le 1er espace)
    // pour rescaper les données saisies si le libellé d'une clause change.
    const byNum = new Map();
    src.forEach(item => {
      const num = String(item.reference || "").split(" ")[0];
      if (num && !byNum.has(num)) byNum.set(num, item);
    });
    return DOCUMENT_REVIEW_ITEMS.filter(it => it.reference).map(it => {
      const num = String(it.reference).split(" ")[0];
      const existing = map.get(it.reference) || byNum.get(num) || {};
      // B1 : spread d'abord pour préserver `comments` et tout champ inconnu
      // (patron FIX-CONTROL-FIELD-LOSS), puis on fige les champs connus + la clé.
      return {
        ...existing,
        reference: it.reference,
        justification: existing.justification || "",
        tool: existing.tool || "",
        status: existing.status || "",
        responsible: existing.responsible || "",
        lastReview: existing.lastReview || "",
        nextReview: existing.nextReview || "",
        documentLink: existing.documentLink || ""
      };
    });
  };
  function parseCssColor(value) {
    const raw = (value || "").trim();
    if (!raw) return null;
    if (raw.startsWith("#")) {
      const hex = raw.slice(1);
      if (hex.length === 3) {
        const r = parseInt(hex[0] + hex[0], 16);
        const g = parseInt(hex[1] + hex[1], 16);
        const b = parseInt(hex[2] + hex[2], 16);
        return { r, g, b };
      }
      if (hex.length === 6) {
        const r = parseInt(hex.slice(0, 2), 16);
        const g = parseInt(hex.slice(2, 4), 16);
        const b = parseInt(hex.slice(4, 6), 16);
        return { r, g, b };
      }
      return null;
    }
    const match = raw.match(/rgba?\(([^)]+)\)/i);
    if (!match) return null;
    const parts = match[1].split(",").map((p) => parseFloat(p.trim()));
    if (parts.length < 3 || parts.some((p) => Number.isNaN(p))) return null;
    return { r: parts[0], g: parts[1], b: parts[2] };
  }
  function withAlpha(color, alpha) {
    const rgb = parseCssColor(color);
    if (!rgb) return color;
    return `rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${alpha})`;
  }
  function getThemeVars() {
    const target = document.body || document.documentElement;
    const styles = getComputedStyle(target);
    const readVar = (name, fallback) => styles.getPropertyValue(name).trim() || fallback;
    const textColor = readVar("--color-text", "#2d3e4a");
    const textSecondary = readVar("--color-text-secondary", "#758592");
    const borderColor = readVar("--color-border", "#e9ecef");
    const surface = readVar("--color-surface", "#ffffff");
    const primary = readVar("--color-primary", "#1fb8cd");
    const secondary = readVar("--color-secondary", "#5d878f");
    const success = readVar("--color-success", "#32c086");
    const warning = readVar("--color-warning", "#ffc185");
    const danger = readVar("--color-danger", "#db4545");
    const info = readVar("--color-info", "#5d878f");
    return {
      textColor,
      textSecondary,
      borderColor,
      surface,
      primary,
      secondary,
      success,
      warning,
      danger,
      info
    };
  }
  function safeT(key, fallback) {
    if (typeof t === 'function') {
      var val = t(key);
      if (val && val !== key) return val;
    }
    return fallback;
  }
  // Refonte header 2026-07 : le bouton thème est un item du menu utilisateur.
  // Libellé dynamique « Thème : <nom> » + icône propre à chacun des 4 thèmes réels.
  const THEME_MENU_ICONS = { light: "fa-sun", dark: "fa-moon", poudre: "fa-feather", bleu: "fa-droplet" };
  const THEME_LABEL_KEYS = { light: "settings.theme_clair", dark: "settings.theme_sombre", poudre: "settings.theme_poudre", bleu: "settings.theme_bleu" };
  function updateThemeToggleUi() {
    const btn = document.getElementById("themeToggleBtn");
    if (!btn) return;
    const iconEl = btn.querySelector("i");
    if (iconEl) iconEl.className = "fas " + (THEME_MENU_ICONS[currentTheme] || "fa-circle-half-stroke");
    const fallbackNames = { light: "Clair", dark: "Sombre", poudre: "Poudre", bleu: "Bleu" };
    const themeName = safeT(THEME_LABEL_KEYS[currentTheme] || "", fallbackNames[currentTheme] || currentTheme);
    // La ponctuation (espace insécable FR / deux-points EN) vit dans la clé i18n
    const label = safeT("header.themeLabel", "Thème :") + " " + themeName;
    const labelEl = btn.querySelector(".csi-usermenu__theme-label");
    if (labelEl) labelEl.textContent = label;
    else btn.textContent = label;
    btn.title = safeT("header.themeToggleTitle", "Changer de thème");
  }
  function setTheme(mode) {
    currentTheme = normalizeTheme(mode);
    document.body.classList.toggle("theme-dark", currentTheme === "dark");
    document.body.classList.toggle("theme-light", currentTheme === "light");
    document.body.classList.toggle("theme-poudre", currentTheme === "poudre");
    document.body.classList.toggle("theme-bleu", currentTheme === "bleu");
    // Set data-theme for CSS selectors and prefers-color-scheme compatibility
    document.documentElement.setAttribute('data-theme', currentTheme);
    updateThemeToggleUi();
    try {
      if (typeof Chart !== "undefined") {
        const { textColor } = getThemeVars();
        Chart.defaults.color = textColor;
      }
    } catch (e) {
      void 0;
    }
    if (typeof updateCharts === "function") {
      updateCharts();
    }
    try {
      localStorage.setItem(THEME_STORAGE_KEY, currentTheme);
    } catch (e) {
      void 0;
    }
  }
  function toggleTheme() {
    const themes = THEME_OPTIONS.map(function(o) { return o.value; });
    const idx = themes.indexOf(currentTheme);
    setTheme(themes[(idx + 1) % themes.length]);
  }
  function downloadJsonBackup(content, name = "smsi_backup.json") {
    try {
      const blob = new Blob([content], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = name;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (e) {
      void 0;
    }
  }
  function initThemeToggle() {
    // Lire le thème par défaut depuis la balise meta (pour branding/packaging)
    const metaTheme = document.querySelector('meta[name="csi-default-theme"]');
    const brandDefaultTheme = metaTheme && metaTheme.content ? metaTheme.content : null;

    const saved = (() => {
      try {
        return localStorage.getItem(THEME_STORAGE_KEY);
      } catch (e) {
        return null;
      }
    })();

    // Si un thème de marque est défini et pas de thème sauvegardé, utiliser le thème de marque
    const defaultToUse = brandDefaultTheme || DEFAULT_THEME;
    const initial = normalizeTheme(saved || defaultToUse);
    setTheme(initial);
    // Note: click listener added in setupEventListeners(), not here, to avoid double-binding
  }
  const DEFAULT_NIS2_PHASES = [
    { id: "phase-1", label: "Phase 1 (2026)", order: 1 },
    { id: "phase-2", label: "Phase 2 (2027)", order: 2 },
    { id: "phase-3", label: "Phase 3 (2028)", order: 3 },
    { id: "phase-unplanned", label: t('templates.outOfPhase'), order: 99, fallback: true }
  ];
  // DEFAULT_NIS2_MATURITY is now dynamic - initialized in constants.js, rebuilt by setCyfunAssuranceLevel()
  // On first load, rebuild categories from framework data if available
  if (typeof window.rebuildCyfunCategories === 'function') {
    window.rebuildCyfunCategories('basic'); // default level until user data is loaded
  }
  const DEFAULT_SOCLE_PILLARS = [
    {
      id: "socle-pillar-1",
      title: t('templates.pillar1Title', 'Gouverner (GV)'),
      subtitle: t('templates.pillar1Subtitle', 'Gouvernance et politique cyber'),
      items: [
        "Formalisation de la gouvernance NIS2",
        "Politique de s\u00e9curit\u00e9 de l'information",
        "R\u00f4les et responsabilit\u00e9s SSI",
        "Strat\u00e9gie de gestion des risques",
        "Gestion des risques cha\u00eene d'approvisionnement"
      ],
      owner: t('templates.defaultOwner'),
      icon: "fa-landmark"
    },
    {
      id: "socle-pillar-2",
      title: t('templates.pillar2Title', 'Identifier (ID)'),
      subtitle: t('templates.pillar2Subtitle', 'Actifs, risques et amélioration'),
      items: [
        "Cartographie des actifs critiques",
        "\u00c9valuation des risques cyber",
        "Programme d'am\u00e9lioration continue",
        "Inventaire des syst\u00e8mes et donn\u00e9es"
      ],
      owner: t('templates.defaultOwner'),
      icon: "fa-magnifying-glass"
    },
    {
      id: "socle-pillar-3",
      title: t('templates.pillar3Title', 'Protéger (PR)'),
      subtitle: t('templates.pillar3Subtitle', 'Accès, données et infrastructure'),
      items: [
        "MFA et gestion des acc\u00e8s \u00e0 privil\u00e8ges",
        "Sensibilisation et formation cyber",
        "Chiffrement et s\u00e9curit\u00e9 des donn\u00e9es",
        "Durcissement des plateformes",
        "R\u00e9silience de l'infrastructure"
      ],
      owner: t('templates.defaultOwner'),
      icon: "fa-shield-halved"
    },
    {
      id: "socle-pillar-4",
      title: t('templates.pillar4Title', 'Détecter (DE)'),
      subtitle: t('templates.pillar4Subtitle', 'Surveillance et analyse'),
      items: [
        "SIEM centralis\u00e9 et surveillance continue",
        "Analyse des \u00e9v\u00e9nements ind\u00e9sirables",
        "D\u00e9tection des anomalies et menaces",
        "Anti-malware et journalisation"
      ],
      owner: t('templates.defaultOwner'),
      icon: "fa-chart-line"
    },
    {
      id: "socle-pillar-5",
      title: t('templates.pillar5Title', 'Répondre (RS)'),
      subtitle: t('templates.pillar5Subtitle', 'Gestion et communication des incidents'),
      items: [
        "Processus de gestion des incidents",
        "Analyse et att\u00e9nuation des incidents",
        "Circuit de notification NIS2",
        "Communication avec les autorit\u00e9s"
      ],
      owner: t('templates.defaultOwner'),
      icon: "fa-bolt"
    },
    {
      id: "socle-pillar-6",
      title: t('templates.pillar6Title', 'Rétablir (RC)'),
      subtitle: t('templates.pillar6Subtitle', 'Reprise et communication de crise'),
      items: [
        "Plan de reprise d'activit\u00e9 (DRP)",
        "RTO/RPO d\u00e9finis et test\u00e9s",
        "Communication de reprise",
        "Retour d'exp\u00e9rience post-incident"
      ],
      owner: t('templates.defaultOwner'),
      icon: "fa-rotate-right"
    }
  ];
  const buildNis2Maturity = (source = {}) => {
    const result = {};
    CYFUN_CATEGORIES.forEach(cat => {
      result[cat] = 0;
    });
    // Apply values from source
    Object.keys(source).forEach(key => {
      if (typeof source[key] === 'number' && result.hasOwnProperty(key)) {
        result[key] = source[key];
      }
    });
    // Legacy compat: try to map pilier1-5 keys
    if (typeof source.pilier1 === 'number') {
      // Old pilier format, skip - not directly mappable to CyFun categories
    }
    return result;
  };
  const cloneDefaultSoclePillars = () => DEFAULT_SOCLE_PILLARS.map((pillar) => ({
    id: pillar.id,
    title: pillar.title,
    subtitle: pillar.subtitle,
    items: [...pillar.items],
    owner: pillar.owner,
    icon: pillar.icon
  }));
  const normalizeSoclePillars = (pillars = []) => {
    const source = Array.isArray(pillars) ? pillars : [];
    return DEFAULT_SOCLE_PILLARS.map((fallback, idx) => {
      const current = source[idx] || {};
      return {
        id: typeof current.id === "string" ? current.id : fallback.id,
        title: typeof current.title === "string" ? current.title : fallback.title,
        subtitle: typeof current.subtitle === "string" ? current.subtitle : fallback.subtitle,
        items: Array.isArray(current.items) ? current.items.filter(Boolean) : [...fallback.items],
        owner: typeof current.owner === "string" ? current.owner : fallback.owner,
        icon: typeof current.icon === "string" ? current.icon : fallback.icon
      };
    });
  };
  const DEFAULT_NIS2_PLAN = {
    phases: [...DEFAULT_NIS2_PHASES],
    maturity: {
      labels: CYFUN_CATEGORIES.length > 0 ? CYFUN_CATEGORIES.slice() : ['GV.OC','GV.RM','GV.RR','GV.PO','ID.AM','ID.RA','ID.IM','PR.AA','PR.AT','PR.DS','PR.PS','PR.IR','DE.CM','DE.AE','RS.MA','RS.CO','RC.RP'],
      current: Array(CYFUN_CATEGORIES.length || 17).fill(0),
      currentDoc: Array(CYFUN_CATEGORIES.length || 17).fill(0),
      currentImpl: Array(CYFUN_CATEGORIES.length || 17).fill(0),
      target2027: 0,
      target2028: 0
    },
    statuses: [],
    budgetBlocks: [
      { id: "nis2-budget-1", title: t('templates.budgetTotal', 'Budget total mise en conformité') },
      { id: "nis2-budget-2", title: t('templates.budget2026', 'Budget 2026') },
      { id: "nis2-budget-3", title: t('templates.budget2027', 'Budget 2027') },
      { id: "nis2-budget-4", title: t('templates.budget2028', 'Budget 2028') }
    ],
    customBlocks: [
      { id: "nis2-highlight-1", title: t('templates.budgetEngaged'), value: 0, format: "currency" },
      { id: "nis2-highlight-2", title: t('templates.globalProgress'), value: 0, format: "percent" },
      { id: "nis2-highlight-3", title: t('templates.completedActions'), value: 0, format: "percent" }
    ],
    domains: [
      {
        title: t('templates.domainGovernance', 'Gouvernance & responsabilité'),
        objective: "",
        pillar: 1,
        functionId: "GV",
        urgency: "standard",
        actions: [
          { text: "Formaliser la gouvernance NIS2 et le comit\u00e9 de pilotage.", phase: 1, status: "in_progress" },
          { text: "D\u00e9finir la cartographie des r\u00f4les et responsabilit\u00e9s SSI.", phase: 1, status: "todo" },
          { text: "Mettre en place un reporting trimestriel des risques majeurs.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainRiskMgmt', 'Gestion des risques (SMSI)'),
        objective: "",
        pillar: 1,
        functionId: "GV",
        urgency: "standard",
        actions: [
          { text: "Actualiser la m\u00e9thodologie d'analyse des risques.", phase: 1, status: "in_progress" },
          { text: "Lancer l'\u00e9valuation des risques m\u00e9tiers critiques.", phase: 1, status: "todo" },
          { text: "Prioriser le plan de traitement.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainSupplyChain', 'Sécurité de la chaîne d\'approvisionnement'),
        objective: "",
        pillar: 1,
        functionId: "GV",
        urgency: "high",
        actions: [
          { text: "Classifier les fournisseurs critiques.", phase: 1, status: "todo" },
          { text: "D\u00e9ployer un questionnaire de s\u00e9curit\u00e9 tiers.", phase: 1, status: "todo" },
          { text: "Planifier des audits p\u00e9riodiques.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainAssetRisk', 'Actifs et évaluation des risques'),
        objective: "",
        pillar: 2,
        functionId: "ID",
        urgency: "high",
        actions: [
          { text: "Cartographier les actifs critiques et d\u00e9pendances.", phase: 1, status: "in_progress" },
          { text: "R\u00e9aliser l'\u00e9valuation des risques selon NIS2.", phase: 1, status: "todo" },
          { text: "Mettre en place le programme d'am\u00e9lioration continue.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainAccessMgmt', 'Gestion des accès au SI'),
        objective: "",
        pillar: 3,
        functionId: "PR",
        urgency: "high",
        actions: [
          { text: "D\u00e9ployer le MFA sur les comptes sensibles.", phase: 1, status: "todo" },
          { text: "Lancer le programme de sensibilisation.", phase: 1, status: "in_progress" },
          { text: "Appliquer le moindre privil\u00e8ge.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainDataInfra', 'Sécurité données et infrastructure'),
        objective: "",
        pillar: 3,
        functionId: "PR",
        urgency: "high",
        actions: [
          { text: "Chiffrer les donn\u00e9es sensibles au repos et en transit.", phase: 1, status: "todo" },
          { text: "Durcir les configurations des plateformes.", phase: 1, status: "todo" },
          { text: "Renforcer la r\u00e9silience de l'infrastructure.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainDevSec', 'Sécurité dans l\'acquisition, le développement & la maintenance'),
        objective: "",
        pillar: 3,
        functionId: "PR",
        urgency: "high",
        actions: [
          { text: "\u00c9tablir le calendrier des scans et correctifs.", phase: 1, status: "in_progress" },
          { text: "Suivre les vuln\u00e9rabilit\u00e9s des d\u00e9pendances.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainTraceability', 'Traçabilité et auditabilité'),
        objective: "",
        pillar: 4,
        functionId: "DE",
        urgency: "medium",
        actions: [
          { text: "Mettre en place la surveillance continue.", phase: 1, status: "todo" },
          { text: "D\u00e9finir les r\u00e8gles d'analyse des \u00e9v\u00e9nements.", phase: 2, status: "todo" },
          { text: "D\u00e9ployer les alertes et corr\u00e9lations prioritaires.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainIncident', 'Gestion des incidents & notification'),
        objective: "",
        pillar: 5,
        functionId: "RS",
        urgency: "high",
        actions: [
          { text: "Formaliser le processus de gestion des incidents.", phase: 1, status: "todo" },
          { text: "Mettre en place le circuit de notification NIS2.", phase: 1, status: "todo" },
          { text: "Organiser un exercice de gestion d'incident.", phase: 2, status: "todo" }
        ]
      },
      {
        title: t('templates.domainBcpDrp', 'Continuité d\'activité & gestion de crise (BCP/DRP)'),
        objective: "",
        pillar: 6,
        functionId: "RC",
        urgency: "high",
        actions: [
          { text: "\u00c9laborer le plan de reprise d'incident.", phase: 1, status: "in_progress" },
          { text: "D\u00e9finir les RTO/RPO cibles et tester.", phase: 2, status: "todo" },
          { text: "D\u00e9finir le plan de communication de reprise.", phase: 2, status: "todo" }
        ]
      }
    ]
  };
  const clampPercent = (value, fallback = 0) => {
    const num = parseFloat(value);
    if (Number.isFinite(num)) return Math.max(0, Math.min(100, Math.round(num)));
    return Math.max(0, Math.min(100, fallback || 0));
  };
  const parseLooseNumber = (value) => {
    if (typeof value === "number") return value;
    if (value === null || value === void 0) return NaN;
    const str = String(value).replace(/\s/g, "").replace(",", ".").replace(/[^0-9.\-]/g, "");
    const num = parseFloat(str);
    return Number.isFinite(num) ? num : NaN;
  };
  const cloneDefaultNis2Plan = (options = {}) => {
    const includeActions = options.includeActions === true;
    const clamp = typeof clampPercent === "function"
      ? clampPercent
      : (value, fallback = 0) => {
          const num = parseFloat(value);
          if (Number.isFinite(num)) return Math.max(0, Math.min(100, Math.round(num)));
          return Math.max(0, Math.min(100, fallback || 0));
        };
    const parseLoose = typeof parseLooseNumber === "function"
      ? parseLooseNumber
      : (value) => {
          if (typeof value === "number") return value;
          if (value === null || value === void 0) return NaN;
          const str = String(value).replace(/\s/g, "").replace(",", ".").replace(/[^0-9.\-]/g, "");
          const num = parseFloat(str);
          return Number.isFinite(num) ? num : NaN;
        };
    return {
      phases: [...DEFAULT_NIS2_PLAN.phases],
      maturity: {
        labels: [...DEFAULT_NIS2_PLAN.maturity.labels],
        current: [...DEFAULT_NIS2_PLAN.maturity.current],
        target2027: DEFAULT_NIS2_PLAN.maturity.target2027,
        target2028: DEFAULT_NIS2_PLAN.maturity.target2028
      },
      statuses: DEFAULT_NIS2_PLAN.statuses.map((s, idx) => ({
        id: s.id || `nis2-status-${idx + 1}`,
        text: s.text,
        status: s.status
      })),
      budgetBlocks: (DEFAULT_NIS2_PLAN.budgetBlocks || []).map((b, idx) => ({
        id: b.id || `nis2-budget-${idx + 1}`,
        title: b.title || `Budget ${idx + 1}`
      })),
      customBlocks: (DEFAULT_NIS2_PLAN.customBlocks || []).map((b, idx) => {
        const format = (b.format || "currency").toLowerCase();
        const rawValue = typeof b.value !== "undefined" ? b.value : b.percent;
        const value = format === "currency"
          ? (() => {
              const num = parseLoose(rawValue);
              return Number.isFinite(num) ? num : 0;
            })()
          : clamp(rawValue, 0);
        return {
          id: b.id || `nis2-highlight-${idx + 1}`,
          title: b.title,
          value,
          format
        };
      }),
      domains: DEFAULT_NIS2_PLAN.domains.map((d, dIndex) => {
        const actions = includeActions ? (d.actions || []).map((a, aIndex) => ({
          id: a.id || `nis2-act-${dIndex + 1}-${aIndex + 1}`,
          text: a.text,
          phase: typeof a.phase === "number" ? a.phase : parseInt(a.phase, 10) || 1,
          status: a.status || "todo",
          owner: a.owner || "",
          workType: a.workType || "initiative",
          budget: typeof a.budget === "number" ? String(a.budget) : a.budget || "",
          budget2026: typeof a.budget2026 === "number" ? String(a.budget2026) : a.budget2026 || "",
          budget2027: typeof a.budget2027 === "number" ? String(a.budget2027) : a.budget2027 || "",
          budget2028: typeof a.budget2028 === "number" ? String(a.budget2028) : a.budget2028 || "",
          budgetCategory: a.budgetCategory || "divers",
          progress: typeof a.progress === "number" ? Math.max(0, Math.min(100, a.progress)) : a.status === "done" ? 100 : 0
        })) : [];
        return {
          id: d.id || `nis2-domain-${dIndex + 1}`,
          title: d.title,
          objective: d.objective,
          pillar: d.pillar,
          functionId: d.functionId || "",
          urgency: d.urgency || "standard",
          actions
        };
      })
    };
  };
  const getEmptyNis2Plan = () => {
    const plan = cloneDefaultNis2Plan();
    const labels = Array.isArray(plan.maturity?.labels) ? plan.maturity.labels : [];
    plan.maturity = {
      labels,
      current: labels.map(() => 0),
      target2027: 0,
      target2028: 0
    };
    plan.statuses = [];
    plan.budgetBlocks = [];
    plan.customBlocks = [];
    plan.domains = (plan.domains || []).map((domain) => ({
      ...domain,
      actions: []
    }));
    return plan;
  };
  // ===== Budget Cybersécurité — modèle de lignes budgétaires autonomes =====
  // Statuts budgétaires génériques (indépendants de NIS2).
  var BUDGET_STATUSES = ['planifie', 'engage', 'realise'];
  function normalizeBudgetStatus(s) {
    s = (s == null ? '' : String(s)).toLowerCase();
    if (s === 'engage' || s === 'engagé' || s === 'committed' || s === 'in_progress') return 'engage';
    if (s === 'realise' || s === 'réalisé' || s === 'spent' || s === 'done') return 'realise';
    return 'planifie';
  }
  function defaultBudgetYears() {
    var y;
    try { y = (new Date()).getFullYear(); } catch (e) { y = 2026; }
    if (!Number.isFinite(y)) y = 2026;
    return [String(y), String(y + 1), String(y + 2)];
  }
  // Migration one-shot, non destructive : convertit les budgets portés par les
  // actions des domaines NIS2 en lignes budgétaires autonomes (zéro perte de budget).
  // Seules les actions effectivement budgétées deviennent des lignes.
  // Les champs budget portés par les actions legacy ont des noms FIGÉS
  // budget2026 / budget2027 / budget2028 (jamais dérivés de l'année courante).
  var LEGACY_BUDGET_YEARS = ['2026', '2027', '2028'];
  function migrateActionsToBudgetLines(domains, years, parseLoose) {
    var lines = [];
    var counter = 0;
    var yrs = (Array.isArray(years) && years.length) ? years : defaultBudgetYears();
    (domains || []).forEach(function(d) {
      ((d && d.actions) || []).forEach(function(a) {
        if (!a) return;
        var amounts = {};
        var hasAny = false;
        yrs.forEach(function(yr) { amounts[yr] = 0; });
        // Mapping POSITIONNEL des champs legacy figés (budget2026/27/28) vers les
        // années cibles yrs[0..2]. Indispensable : sans cela, dès 2027 la lecture
        // a['budget'+yr] (yr = année système) viserait budget2027/28/29 et perdrait
        // silencieusement les montants saisis sur budget2026.
        LEGACY_BUDGET_YEARS.forEach(function(ly, i) {
          if (i >= yrs.length) return;
          var n = parseLoose(a['budget' + ly]);
          if (Number.isFinite(n) && n !== 0) { amounts[yrs[i]] += n; hasAny = true; }
        });
        // Champ legacy `budget` = total unique (utilisé seulement en l'absence de
        // ventilation annuelle, comme l'ancien getActionBudgetTotal).
        if (!hasAny) {
          var legacy = parseLoose(a.budget);
          if (Number.isFinite(legacy) && legacy !== 0) {
            amounts[yrs[0]] = legacy;
            hasAny = true;
          }
        }
        if (!hasAny) return;
        counter++;
        lines.push({
          id: a.id ? ('bl-' + a.id) : ('bl-mig-' + counter),
          label: a.text || ('Ligne ' + counter),
          category: a.budgetCategory || 'divers',
          owner: a.owner || '',
          status: normalizeBudgetStatus(a.status),
          amounts: amounts
        });
      });
    });
    return lines;
  }
  const normalizeNis2Plan = (plan) => {
    const defaults = cloneDefaultNis2Plan();
    const src = plan && typeof plan === "object" ? plan : {};
    const clamp = typeof clampPercent === "function"
      ? clampPercent
      : (value, fallback = 0) => {
          const num = parseFloat(value);
          if (Number.isFinite(num)) return Math.max(0, Math.min(100, Math.round(num)));
          return Math.max(0, Math.min(100, fallback || 0));
        };
    const parseLoose = typeof parseLooseNumber === "function"
      ? parseLooseNumber
      : (value) => {
          if (typeof value === "number") return value;
          if (value === null || value === void 0) return NaN;
          const str = String(value).replace(/\s/g, "").replace(",", ".").replace(/[^0-9.\-]/g, "");
          const num = parseFloat(str);
          return Number.isFinite(num) ? num : NaN;
        };
    const phases = Array.isArray(src.phases) && src.phases.length ? src.phases.map((p, idx) => ({
      id: p.id || `phase-${idx + 1}`,
      label: p.label || p.name || `Phase ${idx + 1}`,
      order: typeof p.order === "number" ? p.order : parseInt(p.order, 10) || idx + 1,
      fallback: !!p.fallback
    })) : defaults.phases;
    appData.nis2Plan = appData.nis2Plan || {};
    appData.nis2Plan.phases = phases;
    const normalizedPhases = ensureNis2Phases();
    const maturity = {
      labels: Array.isArray(src?.maturity?.labels) && src.maturity.labels.length ? src.maturity.labels : defaults.maturity.labels,
      current: Array.isArray(src?.maturity?.current) && src.maturity.current.length ? src.maturity.current : defaults.maturity.current,
      target2027: typeof src?.maturity?.target2027 === "number" ? src.maturity.target2027 : defaults.maturity.target2027,
      target2028: typeof src?.maturity?.target2028 === "number" ? src.maturity.target2028 : defaults.maturity.target2028
    };
    let statuses = Array.isArray(src.statuses) ? src.statuses.map((s, idx) => {
      const fallbackText = defaults.statuses.length
        ? defaults.statuses[idx % defaults.statuses.length].text
        : `Statut ${idx + 1}`;
      return {
        id: s.id || `nis2-status-${idx + 1}`,
        text: s.text || fallbackText,
        status: s.status || "todo"
      };
    }) : defaults.statuses;
    const budgetBlocks = (Array.isArray(src.budgetBlocks) && src.budgetBlocks.length ? src.budgetBlocks : defaults.budgetBlocks || []).map((b, idx) => ({
      id: b.id || `nis2-budget-${idx + 1}`,
      title: b.title || defaults.budgetBlocks?.[idx % (defaults.budgetBlocks.length || 1)]?.title || `Budget ${idx + 1}`
    }));
    const sourceBlocks = Array.isArray(src.customBlocks) && src.customBlocks.length ? src.customBlocks : defaults.customBlocks;
    const customBlocks = sourceBlocks.map((b, idx) => {
      const fallback = defaults.customBlocks[idx % defaults.customBlocks.length] || defaults.customBlocks[0] || {};
      let format = (b.format || fallback.format || "currency").toLowerCase();
      if (idx === 0 && format !== "currency") format = "currency";
      const rawValue = typeof b.value !== "undefined" ? b.value : (typeof b.percent !== "undefined" ? b.percent : (typeof fallback.value !== "undefined" ? fallback.value : fallback.percent));
      const fallbackValue = typeof fallback.value !== "undefined" ? fallback.value : fallback.percent;
      const value = format === "currency"
        ? (() => {
            const num = parseLoose(rawValue);
            const fb = parseLoose(fallbackValue);
            return Number.isFinite(num) ? num : (Number.isFinite(fb) ? fb : 0);
          })()
        : clamp(rawValue ?? fallbackValue ?? 0, clamp(fallbackValue ?? 0, 0));
      return {
        id: b.id || `nis2-highlight-${idx + 1}`,
        title: b.title || fallback.title || `Bloc ${idx + 1}`,
        value,
        format
      };
    });
    const domainsSource = Array.isArray(src.domains) ? src.domains : defaults.domains;
    const domains = domainsSource.map((d, dIndex) => {
      const templateDomain = defaults.domains[dIndex % defaults.domains.length];
      const templateActions = templateDomain.actions || [];
      return {
        id: d.id || `nis2-domain-${dIndex + 1}`,
        title: d.title || templateDomain.title,
        objective: d.objective || templateDomain.objective,
        pillar: d.pillar || templateDomain.pillar,
        functionId: d.functionId || templateDomain.functionId || "",
        urgency: d.urgency || "standard",
        actions: Array.isArray(d.actions) ? d.actions.map((a, aIndex) => {
          const fallback = templateActions[aIndex % templateActions.length] || templateActions[0] || {};
          const phase = normalizePhaseId(typeof a.phase === "undefined" ? fallback.phase : a.phase);
          const progress = typeof a.progress === "number" ? Math.max(0, Math.min(100, a.progress)) : a.status === "done" ? 100 : 0;
          const budget = typeof a.budget === "number" ? String(a.budget) : a.budget || fallback.budget || "";
          const budget2026 = typeof a.budget2026 === "number" ? String(a.budget2026) : a.budget2026 || fallback.budget2026 || "";
          const budget2027 = typeof a.budget2027 === "number" ? String(a.budget2027) : a.budget2027 || fallback.budget2027 || "";
          const budget2028 = typeof a.budget2028 === "number" ? String(a.budget2028) : a.budget2028 || fallback.budget2028 || "";
          const budgetCategory = a.budgetCategory || fallback.budgetCategory || "divers";
          return {
            id: a.id || `nis2-act-${dIndex + 1}-${aIndex + 1}`,
            text: a.text || fallback.text || "",
            phase,
            status: a.status || fallback.status || "todo",
            owner: a.owner || "",
            workType: a.workType || fallback.workType || "initiative",
            budget,
            budget2026,
            budget2027,
            budget2028,
            budgetCategory,
            progress
          };
        }) : []
      };
    });
    // Budget categories (user-editable)
    var defaultBudgetCategories = [
      { value: "infra", label: t('budgetCategories.infra', 'Infrastructure') },
      { value: "cyber", label: t('budgetCategories.cyber', 'Cybersécurité') },
      { value: "divers", label: t('budgetCategories.divers', 'Divers') }
    ];
    var budgetCategories = Array.isArray(src.budgetCategories) && src.budgetCategories.length
      ? src.budgetCategories.map(function(c) { return { value: c.value || c.label || '', label: c.label || c.value || '' }; })
      : defaultBudgetCategories;
    // Migrate legacy "ssi"/"spot" → "cyber" in all domain actions
    domains.forEach(function(d) {
      (d.actions || []).forEach(function(a) {
        if (a.budgetCategory === 'ssi' || a.budgetCategory === 'spot') a.budgetCategory = 'cyber';
      });
    });
    // NB : l'ancienne migration de titres de blocs (SSI/SPOT/Cybersécurité → « Budget 2028 »,
    // Infrastructure → « Budget 2027 ») a été retirée — elle réécrivait silencieusement des
    // libellés personnalisés et entrait en collision avec le nom « Budget Cybersécurité ».
    // Budget Cybersécurité — années configurables (libellés libres).
    var budgetYears;
    if (Array.isArray(src.budgetYears) && src.budgetYears.length) {
      budgetYears = src.budgetYears.map(function(yr) { return String(yr).trim(); }).filter(Boolean);
    } else if (Array.isArray(src.budgetLines) && src.budgetLines.length) {
      // budgetLines importées sans budgetYears : reconstituer les années depuis les
      // clés de montants présentes (sinon ces montants seraient orphelins/perdus).
      var seen = {};
      src.budgetLines.forEach(function(l) {
        if (l && l.amounts && typeof l.amounts === 'object') {
          Object.keys(l.amounts).forEach(function(yr) { var k = String(yr).trim(); if (k) seen[k] = true; });
        }
      });
      budgetYears = Object.keys(seen).sort();
      if (!budgetYears.length) budgetYears = defaultBudgetYears();
    } else {
      budgetYears = defaultBudgetYears();
    }
    budgetYears = budgetYears.filter(function(yr, i) { return budgetYears.indexOf(yr) === i; });
    if (!budgetYears.length) budgetYears = defaultBudgetYears();
    // Budget Cybersécurité — lignes budgétaires autonomes.
    // Si `budgetLines` est déjà présent (même vide) on le respecte ; sinon migration one-shot
    // depuis les budgets portés par les actions des domaines (rétro-compatibilité legacy).
    // NB : les champs budget des actions (domains[].budget2026/27/28 + budget) sont CONSERVÉS
    // volontairement (migration NON destructive : sécurité rollback / client non à jour) mais
    // ne sont PLUS la source de vérité une fois budgetLines créé — ne pas les rééditer.
    var budgetLines;
    if (Array.isArray(src.budgetLines)) {
      budgetLines = src.budgetLines.map(function(l, idx) {
        var srcAmounts = (l && l.amounts && typeof l.amounts === 'object') ? l.amounts : {};
        var amounts = {};
        budgetYears.forEach(function(yr) {
          var n = parseLoose(srcAmounts[yr]);
          amounts[yr] = Number.isFinite(n) ? n : 0;
        });
        return {
          id: (l && l.id) || ('bl-' + (idx + 1)),
          label: (l && l.label != null) ? String(l.label) : '',
          category: (l && l.category) || 'divers',
          owner: (l && l.owner) || '',
          status: normalizeBudgetStatus(l && l.status),
          amounts: amounts
        };
      });
    } else {
      budgetLines = migrateActionsToBudgetLines(domains, budgetYears, parseLoose);
    }
    return { phases: normalizedPhases, maturity, statuses, budgetBlocks, customBlocks, domains, budgetCategories, budgetYears, budgetLines };
  };
  if (typeof window !== 'undefined') {
    window.DOMAIN_BY_PREFIX = DOMAIN_BY_PREFIX;
    window.OPERATIONAL_CAPABILITIES = OPERATIONAL_CAPABILITIES;
    window.NIS2_CATEGORIES = NIS2_CATEGORIES;
  }
  // modules/dashboard.js
	  function initDashboard() {
	    updateDashboard();
	  }
	  function updateDashboard() {
	    _csiRenderDashboardViewSwitch();
	    updateKPIs();
	    updateCharts();
	    updateControlsLabel();
	    if (_csiDashboardConsolidatedActive()) updateConsolidatedDashboard();
	    else _csiApplyDashboardModeFrame(false);
	    if (typeof updateDashboardActivity === 'function') updateDashboardActivity();
	    // === CSI-ME === tuile « Vue groupe » (compte principal, mode multi-entités).
	    if (window.CSI_EntityUI && window.CSI_EntityUI.renderGroupTile) window.CSI_EntityUI.renderGroupTile();
	  }
	  function _csiConsolidation() { return window.CSI_EntityConsolidation || null; }
	  function _csiDashboardConsolidatedActive() {
	    var C = _csiConsolidation();
	    return !!(C && C.isAllowed && C.isAllowed() && C.getDashboardMode && C.getDashboardMode() === 'consolidated');
	  }
	  function _csiRenderDashboardViewSwitch() {
	    var sw = document.getElementById('dashboardViewSwitch');
	    if (!sw) return;
	    var C = _csiConsolidation();
	    var allowed = !!(C && C.isAllowed && C.isAllowed());
	    sw.style.display = allowed ? '' : 'none';
	    if (!allowed && C && C.setDashboardMode) C.setDashboardMode('scope');
	    var mode = allowed && C.getDashboardMode ? C.getDashboardMode() : 'scope';
	    sw.querySelectorAll('[data-dashboard-view]').forEach(function(btn) {
	      var on = btn.getAttribute('data-dashboard-view') === mode;
	      btn.classList.toggle('dashboard-view-switch__btn--active', on);
	      btn.setAttribute('aria-pressed', on ? 'true' : 'false');
	      if (!btn._csiViewBound) {
	        btn._csiViewBound = true;
	        btn.addEventListener('click', function() {
	          var target = btn.getAttribute('data-dashboard-view') || 'scope';
	          if (C && C.setDashboardMode && C.setDashboardMode(target)) updateDashboard();
	        });
	      }
	    });
	  }
	  function _csiApplyDashboardModeFrame(consolidated) {
	    var dash = document.getElementById('dashboard');
	    if (dash) dash.classList.toggle('dashboard--consolidated', !!consolidated);
	    var note = document.getElementById('dashboardConsolidatedNote');
	    if (note) note.style.display = consolidated ? '' : 'none';
	    var panel = document.getElementById('dashboardMaturityPanel');
	    if (panel) panel.style.display = consolidated ? '' : 'none';
	  }
	  function _csiSectionMetric(summary, section, key) {
	    var s = summary && summary.sections ? summary.sections[section] : null;
	    if (!s) return 0;
	    if (!key) return s.total || 0;
	    return s[key] || 0;
	  }
	  function _csiSetMetric(id, value) {
	    var el = document.getElementById(id);
	    if (el) el.textContent = String(value == null ? 0 : value);
	  }
	  function updateConsolidatedDashboard() {
	    var C = _csiConsolidation();
	    if (!C || !C.fetchSummary || !C.isAllowed || !C.isAllowed()) { _csiApplyDashboardModeFrame(false); return; }
	    _csiApplyDashboardModeFrame(true);
	    var previousMetrics = {};
	    ['totalActions','criticalRisks','openNonConformities','projectRisksCount','criticalAssetsCount','threatsCount','auditsCount','documentsCount','riskAnalysesCount','managementReviewsCount'].forEach(function(id) {
	      var metric = document.getElementById(id);
	      previousMetrics[id] = metric ? metric.textContent : '';
	      _csiSetMetric(id, '…');
	    });
	    C.fetchSummary({ force: true }).then(function(summary) {
	      if (!_csiDashboardConsolidatedActive() || !summary) return;
	      _csiApplyDashboardModeFrame(true);
	      _csiSetMetric('totalActions', _csiSectionMetric(summary, 'actions', 'active'));
	      _csiSetMetric('criticalRisks', _csiSectionMetric(summary, 'risks', 'critical'));
	      _csiSetMetric('openNonConformities', _csiSectionMetric(summary, 'nonconformities', 'open'));
	      _csiSetMetric('projectRisksCount', _csiSectionMetric(summary, 'project-risks'));
	      _csiSetMetric('criticalAssetsCount', _csiSectionMetric(summary, 'critical-assets'));
	      _csiSetMetric('threatsCount', _csiSectionMetric(summary, 'threats-section'));
	      _csiSetMetric('auditsCount', _csiSectionMetric(summary, 'audits'));
	      _csiSetMetric('documentsCount', _csiSectionMetric(summary, 'documents'));
	      _csiSetMetric('riskAnalysesCount', _csiSectionMetric(summary, 'risk-analysis'));
	      _csiSetMetric('managementReviewsCount', _csiSectionMetric(summary, 'managementReviews'));
	      _csiRenderConsolidatedAlerts(summary);
	      _csiRenderMaturityByEntity(summary.maturity || []);
	    }).catch(function(error) {
	      if (window.console && console.warn) void 0;
	      if (typeof showToast === 'function') showToast(t('dashboard.consolidatedLoadError', 'Vue consolidée indisponible pour le moment.'), 'warning');
	      if (!C.isAllowed || !C.isAllowed() || (error && (error.status === 400 || error.status === 401 || error.status === 403))) {
	        if (C.setDashboardMode) C.setDashboardMode('scope');
	        _csiRenderDashboardViewSwitch();
	        _csiApplyDashboardModeFrame(false);
	        return;
	      }
	      Object.keys(previousMetrics).forEach(function(id) {
	        _csiSetMetric(id, previousMetrics[id]);
	      });
	      _csiRenderMaturityByEntity([]);
	    });
	  }
	  function _csiRenderConsolidatedAlerts(summary) {
	    var alertsList = document.getElementById("alertsList");
	    if (!alertsList) return;
	    var alerts = [];
	    var critical = _csiSectionMetric(summary, 'risks', 'critical');
	    var overdueActions = _csiSectionMetric(summary, 'actions', 'overdue');
	    var overdueAudits = _csiSectionMetric(summary, 'audits', 'overdue');
	    var upcomingAudits = _csiSectionMetric(summary, 'audits', 'upcoming30');
	    var redProjects = _csiSectionMetric(summary, 'project-risks', 'red');
	    if (critical > 0) alerts.push({ icon: 'critical', iconClass: 'fas fa-skull-crossbones', text: critical + ' ' + t('dashboard.alertCriticalRisks'), badge: t('dashboard.badgeUrgent'), target: 'risks' });
	    if (overdueActions > 0) alerts.push({ icon: 'warning', iconClass: 'fas fa-clock', text: overdueActions + ' ' + t('dashboard.alertOverdueActions'), badge: t('dashboard.badgeOverdue'), target: 'actions' });
	    if (overdueAudits > 0) alerts.push({ icon: 'warning', iconClass: 'fas fa-clipboard-check', text: overdueAudits + ' ' + t('dashboard.alertOverdueAudits', 'audit(s) en retard'), badge: t('dashboard.badgeOverdue'), target: 'audits' });
	    if (upcomingAudits > 0) alerts.push({ icon: 'info', iconClass: 'fas fa-calendar-check', text: upcomingAudits + ' ' + t('dashboard.alertUpcomingAudits'), badge: t('dashboard.badgePlanned'), target: 'audits' });
	    if (redProjects > 0) alerts.push({ icon: 'purple', iconClass: 'fas fa-diagram-project', text: redProjects + ' ' + t('dashboard.alertProjectsAtRisk'), badge: t('dashboard.badgeProjects'), target: 'project-risks' });
	    if (alerts.length === 0) {
	      alertsList.innerHTML = '<div class="dashboard-alerts__empty"><i class="fas fa-check-circle"></i>&nbsp;' + escapeHtml(t('dashboard.noAlerts')) + '</div>';
	      return;
	    }
	    alertsList.innerHTML = alerts.map(function(a) {
	      return '<div class="alert-item" data-nav-target="' + escapeHtml(a.target) + '" role="button" tabindex="0">' +
	        '<div class="alert-item__icon alert-item__icon--' + escapeHtml(a.icon) + '"><i class="' + escapeHtml(a.iconClass) + '"></i></div>' +
	        '<span class="alert-item__text">' + escapeHtml(a.text) + '</span>' +
	        '<span class="alert-item__badge">' + escapeHtml(a.badge) + '</span></div>';
	    }).join('');
	    alertsList.querySelectorAll('.alert-item[data-nav-target]').forEach(function(item) {
	      item.addEventListener('click', function() { _csiOpenConsolidatedSection(item.getAttribute('data-nav-target')); });
	      item.addEventListener('keypress', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); item.click(); } });
	    });
	  }
	  function _csiRenderMaturityByEntity(rows) {
	    var tbody = document.getElementById('dashboardMaturityRows');
	    if (!tbody) return;
	    if (!rows.length) {
	      tbody.innerHTML = '<tr><td colspan="6" class="text-muted">' + escapeHtml(t('dashboard.noMaturityData', 'Aucune maturité renseignée.')) + '</td></tr>';
	      return;
	    }
	    tbody.innerHTML = rows.map(function(r) {
	      var conformity = (r.implementedControls || 0) + '/' + (r.applicableControls || 0) + ' · ' + (r.controlRate || 0) + '%';
	      return '<tr>' +
	        '<td><span class="entity-badge">' + escapeHtml(r.entityName || r.entityId || '-') + '</span></td>' +
	        '<td>' + escapeHtml(r.referential || '-') + '</td>' +
	        '<td>' + escapeHtml(conformity) + '</td>' +
	        '<td><strong>' + escapeHtml(String(r.isoMaturityRate || 0)) + '%</strong></td>' +
	        '<td><strong>' + escapeHtml(String(r.maturityRate || 0)) + '%</strong></td>' +
	        '<td>' + escapeHtml(String(r.evaluatedControls || 0)) + '</td>' +
	      '</tr>';
	    }).join('');
	  }
	  function _csiOpenConsolidatedSection(sectionId) {
	    var C = _csiConsolidation();
	    if (C && C.isSectionConsolidable && C.isSectionConsolidable(sectionId) && C.setSectionMode) {
	      C.setSectionMode(sectionId, 'consolidated');
	    }
	    if (typeof switchModule === "function") switchModule(sectionId);
	    else if (typeof loadModuleContent === "function") loadModuleContent(sectionId);
	  }
	  function _csiIsSectionConsolidable(sectionId) {
	    var C = _csiConsolidation();
	    return !!(C && C.isSectionConsolidable && C.isSectionConsolidable(sectionId));
	  }
	  function _csiRerenderSection(sectionId) {
	    if (typeof _rerenderCurrentTab === 'function') _rerenderCurrentTab(sectionId);
	    else if (sectionId === 'risk-analysis' && typeof window.renderRiskAnalysisModule === 'function') window.renderRiskAnalysisModule();
	    else if (typeof loadModuleContent === 'function') loadModuleContent(sectionId);
	  }
	  function _csiGetPathValue(row, path) {
	    if (!row || !path) return '';
	    var parts = String(path).split('.');
	    var value = row;
	    for (var index = 0; index < parts.length; index++) {
	      if (value == null || typeof value !== 'object' || !(parts[index] in value)) return '';
	      value = value[parts[index]];
	    }
	    return value == null ? '' : value;
	  }
	  function _csiPickValue(row, paths) {
	    for (var index = 0; index < paths.length; index++) {
	      var value = _csiGetPathValue(row, paths[index]);
	      if (value !== '' && value != null) return value;
	    }
	    return '';
	  }
	  function _csiHumanValue(value) {
	    if (Array.isArray(value)) return value.join(', ');
	    if (value == null || value === '') return '—';
	    if (typeof value === 'boolean') return value ? t('common.yes', 'Oui') : t('common.no', 'Non');
	    return String(value).replace(/_/g, ' ');
	  }
	  function _csiDateValue(value) {
	    if (!value) return '—';
	    var date = new Date(value);
	    return isNaN(date.getTime()) ? String(value) : date.toLocaleDateString();
	  }
	  function _csiPercentValue(value) {
	    var number = parseInt(value, 10);
	    if (!Number.isFinite(number)) number = 0;
	    return Math.max(0, Math.min(100, number)) + '%';
	  }
	  function _csiRiskScoreValue(value, row) {
	    var impact = parseFloat(_csiPickValue(row, ['impact'])) || 0;
	    var probability = parseFloat(_csiPickValue(row, ['probability', 'likelihood'])) || 0;
	    var score = impact * probability;
	    return score ? String(score) : '—';
	  }
	  function _csiReviewCommentsValue(value, row) {
	    if (Array.isArray(row && row.comments)) return String(row.comments.length);
	    return _csiHumanValue(value);
	  }
	  var _csiConsolidatedSectionSchemas = {
	    'risks': [
	      { label: 'entities.cols.title', fallback: 'Risque', paths: ['title', 'name'] },
	      { label: 'entities.cols.impactShort', fallback: 'I', paths: ['impact'] },
	      { label: 'entities.cols.likelihoodShort', fallback: 'V', paths: ['probability', 'likelihood'] },
	      { label: 'entities.cols.severity', fallback: 'Niveau', paths: ['level', 'severity'] },
	      { label: 'risks.columns.treatment', fallback: 'Traitement', paths: ['treatment'] },
	      { label: 'risks.columns.codirDate', fallback: 'Date CODIR', paths: ['codirDate', 'reviewDate'], format: _csiDateValue }
	    ],
	    'threats-section': [
	      { label: 'entities.cols.title', fallback: 'Titre', paths: ['title', 'name'] },
	      { label: 'entities.cols.impactShort', fallback: 'I', paths: ['impact'] },
	      { label: 'entities.cols.likelihoodShort', fallback: 'V', paths: ['likelihood', 'probability'] },
	      { label: 'threats.columns.score', fallback: 'Score', paths: ['score'], format: _csiRiskScoreValue },
	      { label: 'entities.cols.severity', fallback: 'Niveau', paths: ['level', 'severity'] },
	      { label: 'entities.cols.status', fallback: 'Statut', paths: ['status'] }
	    ],
	    'actions': [
	      { label: 'actions.id', fallback: 'ID', paths: ['displayId', 'ref', 'id'] },
	      { label: 'entities.cols.title', fallback: 'Action', paths: ['title', 'name'] },
	      { label: 'entities.cols.priority', fallback: 'Priorité', paths: ['priority'] },
	      { label: 'entities.cols.status', fallback: 'Statut', paths: ['status'] },
	      { label: 'actions.progress', fallback: 'Avancement', paths: ['progress'], format: _csiPercentValue },
	      { label: 'entities.cols.dueDate', fallback: 'Échéance', paths: ['dueDate'], format: _csiDateValue },
	      { label: 'entities.cols.assignee', fallback: 'Responsable', paths: ['assignedTo', 'owner'] }
	    ],
	    'nonconformities': [
	      { label: 'entities.cols.title', fallback: 'Titre', paths: ['title', 'name'] },
	      { label: 'entities.cols.type', fallback: 'Type', paths: ['type'] },
	      { label: 'entities.cols.status', fallback: 'Statut', paths: ['status'] },
	      { label: 'entities.cols.date', fallback: 'Date', paths: ['detectionDate', 'date'], format: _csiDateValue },
	      { label: 'entities.cols.assignee', fallback: 'Responsable', paths: ['assignedTo', 'owner'] },
	      { label: 'entities.cols.item', fallback: 'Description', paths: ['description'] }
	    ],
	    'critical-assets': [
	      { label: 'entities.cols.name', fallback: 'Produit', paths: ['productName', 'name', 'title'] },
	      { label: 'criticalAssets.columns.beneficiary', fallback: 'Bénéficiaire', paths: ['beneficiary'] },
	      { label: 'criticalAssets.columns.assetType', fallback: 'Type', paths: ['assetType'] },
	      { label: 'entities.cols.importance', fallback: 'Importance', paths: ['importance'] },
	      { label: 'criticalAssets.columns.rto', fallback: 'RTO', paths: ['rto'] },
	      { label: 'criticalAssets.columns.rpo', fallback: 'RPO', paths: ['rpo'] },
	      { label: 'criticalAssets.columns.availability', fallback: 'Disponibilité', paths: ['availability'] },
	      { label: 'entities.cols.impactShort', fallback: 'Impact', paths: ['impact'] }
	    ],
	    'audits': [
	      { label: 'entities.cols.title', fallback: 'Titre', paths: ['title', 'name'] },
	      { label: 'entities.cols.type', fallback: 'Type', paths: ['type'] },
	      { label: 'audits.columns.scope', fallback: 'Périmètre', paths: ['scope'] },
	      { label: 'entities.cols.date', fallback: 'Date', paths: ['plannedDate', 'date'], format: _csiDateValue },
	      { label: 'audits.columns.auditor', fallback: 'Auditeur', paths: ['auditor'] },
	      { label: 'entities.cols.status', fallback: 'Statut', paths: ['status'] }
	    ],
	    'documents': [
	      { label: 'entities.cols.category', fallback: 'Niveau', paths: ['category'] },
	      { label: 'entities.cols.type', fallback: 'Type', paths: ['type', 'title'] },
	      { label: 'documents.columns.tool', fallback: 'Outil', paths: ['tool'] },
	      { label: 'documents.columns.link', fallback: 'Lien', paths: ['link'] }
	    ],
	    'stakeholders': [
	      { label: 'entities.cols.name', fallback: 'Nom', paths: ['name', 'title'] },
	      { label: 'entities.cols.role', fallback: 'Rôle', paths: ['role'] },
	      { label: 'fields.contact', fallback: 'Contact', paths: ['contact'] },
	      { label: 'entities.cols.item', fallback: 'Notes', paths: ['notes'] }
	    ],
	    'kpis': [
	      { label: 'entities.cols.indicator', fallback: 'Indicateur', paths: ['title', 'name'] },
	      { label: 'kpi.description', fallback: 'Description', paths: ['description'] },
	      { label: 'kpi.progress', fallback: 'Progression', paths: ['progress'], format: _csiPercentValue },
	      { label: 'entities.cols.target', fallback: 'Cible', paths: ['target'] }
	    ],
	    'managementReviews': [
	      { label: 'entities.cols.date', fallback: 'Date', paths: ['date'], format: _csiDateValue },
	      { label: 'reviews.columns.participants', fallback: 'Participants', paths: ['participants'] },
	      { label: 'reviews.columns.inputs', fallback: 'Entrées', paths: ['inputs'] },
	      { label: 'reviews.columns.decisions', fallback: 'Décisions', paths: ['decisions'] },
	      { label: 'reviews.columns.comments', fallback: 'Commentaires', paths: ['comments'], format: _csiReviewCommentsValue }
	    ],
	    'project-risks': [
	      { label: 'projectRisks.projectName', fallback: 'Projet', paths: ['projectName', 'title', 'name'] },
	      { label: 'projectRisks.beneficiary', fallback: 'Bénéficiaire', paths: ['beneficiary'] },
	      { label: 'projectRisks.projectDescription', fallback: 'Description', paths: ['description'] },
	      { label: 'entities.cols.date', fallback: 'Date', paths: ['completionDate', 'date'], format: _csiDateValue },
	      { label: 'entities.cols.status', fallback: 'Statut', paths: ['status'] },
	      { label: 'entities.cols.priority', fallback: 'Priorité', paths: ['priority'] },
	      { label: 'projectRisks.note', fallback: 'Note', paths: ['note'] }
	    ],
	    'risk-analysis': [
	      { label: 'riskAnalysis.listColumns.title', fallback: 'Titre', paths: ['meta.title', 'identity.name', 'title', 'name'] },
	      { label: 'riskAnalysis.listColumns.created', fallback: 'Créée le', paths: ['meta.createdAt', 'createdAt'], format: _csiDateValue },
	      { label: 'riskAnalysis.listColumns.status', fallback: 'Statut', paths: ['meta.status', 'status'] },
	      { label: 'riskAnalysis.listColumns.score', fallback: 'Score', paths: ['results.globalScore'] },
	      { label: 'projectRisks.beneficiary', fallback: 'Bénéficiaire', paths: ['identity.beneficiary', 'beneficiary'] }
	    ]
	  };
	  function _csiSectionName(sectionId) {
	    return t('entities.sections.' + sectionId, sectionId);
	  }
	  function _csiSectionSchema(sectionId) {
	    return _csiConsolidatedSectionSchemas[sectionId] || [
	      { label: 'entities.cols.title', fallback: 'Intitulé', paths: ['title', 'name', 'id'] },
	      { label: 'entities.cols.status', fallback: 'Statut', paths: ['status'] }
	    ];
	  }
	  function _csiRowTitle(sectionId, row) {
	    var schema = _csiSectionSchema(sectionId);
	    return _csiHumanValue(_csiPickValue(row, schema[0].paths || ['title', 'name', 'id']));
	  }
	  function _csiEntityMeta(row) {
	    var entity = row && row._entity && typeof row._entity === 'object' ? row._entity : {};
	    return {
	      id: entity.id || '',
	      name: entity.name || entity.id || t('entities.mainEntity', 'Entité principale')
	    };
	  }
	  function _csiEnsureSectionViewBar(sectionId, module, consolidated) {
	    var bar = module.querySelector('.csi-section-viewbar[data-section="' + sectionId + '"]');
	    var previousSearch = bar ? (bar.querySelector('[data-csi-section-search]') || {}).value || '' : '';
	    var previousEntity = bar ? (bar.querySelector('[data-csi-entity-filter]') || {}).value || '' : '';
	    if (!bar) {
	      bar = document.createElement('div');
	      bar.className = 'csi-section-viewbar';
	      bar.setAttribute('data-section', sectionId);
	      var header = module.querySelector('.module__header');
	      if (header && header.parentNode) header.parentNode.insertBefore(bar, header.nextSibling);
	      else module.insertBefore(bar, module.firstChild);
	    } else {
	      // Toolbar v2 : la rangée « Vue » doit TOUJOURS être en premier. Un re-init
	      // des SectionToolbar (ex. changement de langue) réinsère la toolbar juste
	      // après le header, passant devant la viewbar — on la repositionne.
	      var existingHeader = module.querySelector('.module__header');
	      if (existingHeader && existingHeader.parentNode && bar.previousElementSibling !== existingHeader) {
	        existingHeader.parentNode.insertBefore(bar, existingHeader.nextSibling);
	      }
	    }
	    bar.innerHTML =
	      '<div class="csi-section-viewbar__left">' +
	        '<span class="csi-section-viewbar__label">' + escapeHtml(t('dashboard.viewLabel', 'Vue')) + '</span>' +
	        '<div class="csi-section-toggle" role="group" aria-label="' + escapeHtml(t('entities.sectionViewAria', 'Vue de la section')) + '">' +
	          '<button type="button" class="csi-section-toggle__btn' + (!consolidated ? ' csi-section-toggle__btn--active' : '') + '" data-csi-section-mode="scope" aria-pressed="' + (!consolidated ? 'true' : 'false') + '">' + escapeHtml(t('dashboard.currentPerimeter', 'Périmètre courant')) + '</button>' +
	          '<button type="button" class="csi-section-toggle__btn' + (consolidated ? ' csi-section-toggle__btn--active' : '') + '" data-csi-section-mode="consolidated" aria-pressed="' + (consolidated ? 'true' : 'false') + '">' + escapeHtml(t('dashboard.allEntities', 'Toutes les entités')) + '</button>' +
	        '</div>' +
	        (consolidated ? '<span class="csi-readonly-pill"><i class="fas fa-eye" aria-hidden="true"></i> ' + escapeHtml(t('entities.readOnly', 'Lecture seule')) + '</span>' : '') +
	      '</div>' +
	      '<div class="csi-section-viewbar__filters" style="' + (consolidated ? '' : 'display:none') + '">' +
	        '<select class="form-control form-control--sm" data-csi-entity-filter aria-label="' + escapeHtml(t('entities.entityCol', 'Entité')) + '"><option value="">' + escapeHtml(t('entities.entityFilterAll', 'Toutes les entités')) + '</option></select>' +
	        '<input type="search" class="form-control form-control--sm" data-csi-section-search placeholder="' + escapeHtml(t('entities.sectionSearchPlaceholder', 'Rechercher dans la vue consolidée…')) + '" aria-label="' + escapeHtml(t('common.search', 'Rechercher...')) + '">' +
	        '<button type="button" class="btn btn--outline btn--sm" data-csi-section-refresh><i class="fas fa-rotate-right" aria-hidden="true"></i> ' + escapeHtml(t('entities.refresh', 'Actualiser')) + '</button>' +
	      '</div>';
	    var searchInput = bar.querySelector('[data-csi-section-search]');
	    var entitySelect = bar.querySelector('[data-csi-entity-filter]');
	    if (searchInput) searchInput.value = previousSearch;
	    if (entitySelect) entitySelect.value = previousEntity;
	    bar.querySelectorAll('[data-csi-section-mode]').forEach(function(button) {
	      button.addEventListener('click', function() {
	        var C = _csiConsolidation();
	        var mode = button.getAttribute('data-csi-section-mode') || 'scope';
	        if (C && C.setSectionMode) C.setSectionMode(sectionId, mode);
	        _csiRerenderSection(sectionId);
	      });
	    });
	    if (searchInput) {
	      searchInput.addEventListener('input', function() {
	        if (module._csiConsolidatedData) _csiRenderConsolidatedSectionRows(module, sectionId, module._csiConsolidatedData);
	      });
	    }
	    if (entitySelect) {
	      entitySelect.addEventListener('change', function() {
	        if (module._csiConsolidatedData) _csiRenderConsolidatedSectionRows(module, sectionId, module._csiConsolidatedData);
	      });
	    }
	    var refreshButton = bar.querySelector('[data-csi-section-refresh]');
	    if (refreshButton) {
	      refreshButton.addEventListener('click', function() {
	        module._csiForceConsolidatedFetch = true;
	        _csiRerenderSection(sectionId);
	      });
	    }
	    // Toolbar v2 : fusion visuelle viewbar + SectionToolbar en UNE carte.
	    // Classe modificatrice posée en JS (pas de :has — compat vieux navigateurs).
	    // En vue consolidée, la toolbar native est masquée (_csiSetNativeHidden) :
	    // la viewbar redevient une carte autonome (pas de coin plat orphelin).
	    var siblingToolbar = bar.nextElementSibling;
	    var isToolbar = !!(siblingToolbar && siblingToolbar.classList && siblingToolbar.classList.contains('section-toolbar'));
	    var attach = isToolbar && !consolidated;
	    bar.classList.toggle('csi-section-viewbar--attached', attach);
	    if (isToolbar) siblingToolbar.classList.toggle('section-toolbar--attached', attach);
	    return bar;
	  }
	  function _csiCleanupConsolidatedSection(module, options) {
	    if (!module) return;
	    module.classList.remove('module--consolidated-read');
	    var overlay = module.querySelector('.csi-consolidated-section');
	    if (overlay) overlay.remove();
	    Array.prototype.forEach.call(module.children, function(child) {
	      child.classList.remove('csi-section-native--hidden');
	    });
	    if (options && options.removeBar) {
	      var bar = module.querySelector('.csi-section-viewbar');
	      if (bar) bar.remove();
	      // Toolbar v2 : plus de viewbar au-dessus → la toolbar redevient une carte pleine.
	      var attachedToolbar = module.querySelector('.section-toolbar--attached');
	      if (attachedToolbar) attachedToolbar.classList.remove('section-toolbar--attached');
	    }
	  }
	  function _csiSetNativeHidden(module, hidden) {
	    Array.prototype.forEach.call(module.children, function(child) {
	      var keep = child.classList.contains('module__header') ||
	        child.classList.contains('csi-section-viewbar') ||
	        child.classList.contains('csi-consolidated-section');
	      if (!keep) child.classList.toggle('csi-section-native--hidden', !!hidden);
	    });
	  }
	  function _csiConsolidatedTarget(module, sectionId, options) {
	    if (options && options.contentId) return document.getElementById(options.contentId);
	    var overlay = module.querySelector('.csi-consolidated-section');
	    if (!overlay) {
	      overlay = document.createElement('div');
	      overlay.className = 'csi-consolidated-section';
	      var bar = module.querySelector('.csi-section-viewbar');
	      if (bar && bar.parentNode) bar.parentNode.insertBefore(overlay, bar.nextSibling);
	      else module.appendChild(overlay);
	    }
	    return overlay;
	  }
	  function _csiPopulateEntityFilter(module, rows) {
	    var select = module.querySelector('[data-csi-entity-filter]');
	    if (!select) return '';
	    var currentValue = select.value || '';
	    var entities = {};
	    rows.forEach(function(row) {
	      var entity = _csiEntityMeta(row);
	      if (entity.id) entities[entity.id] = entity.name;
	    });
	    var html = '<option value="">' + escapeHtml(t('entities.entityFilterAll', 'Toutes les entités')) + '</option>';
	    Object.keys(entities).sort(function(left, right) {
	      return String(entities[left]).localeCompare(String(entities[right]));
	    }).forEach(function(id) {
	      html += '<option value="' + escapeHtml(id) + '">' + escapeHtml(entities[id]) + '</option>';
	    });
	    select.innerHTML = html;
	    if (currentValue && entities[currentValue]) select.value = currentValue;
	    return select.value || '';
	  }
	  function _csiVisibleCellText(row, column) {
	    var rawValue = _csiPickValue(row, column.paths || []);
	    var formatted = column.format ? column.format(rawValue, row) : _csiHumanValue(rawValue);
	    return formatted == null ? '' : String(formatted);
	  }
	  function _csiConsolidatedRelatedHtml(sectionId) {
	    if (sectionId !== 'project-risks') return '';
	    return '<div class="csi-consolidated-related">' +
	      '<div class="csi-consolidated-related__text">' +
	        '<strong>' + escapeHtml(t('projectRisks.consolidatedAnalysesTitle', 'Analyses de risques réalisées')) + '</strong>' +
	        '<span>' + escapeHtml(t('projectRisks.consolidatedAnalysesHint', 'Cette table liste le portefeuille des risques projets. Les analyses détaillées disposent d\'une vue consolidée dédiée.')) + '</span>' +
	      '</div>' +
	      '<button type="button" class="btn btn--outline btn--sm" data-csi-open-risk-analysis>' +
	        '<i class="fas fa-magnifying-glass-chart" aria-hidden="true"></i> ' +
	        escapeHtml(t('projectRisks.openConsolidatedAnalyses', 'Voir toutes les analyses')) +
	      '</button>' +
	    '</div>';
	  }
	  function _csiRenderConsolidatedSectionRows(module, sectionId, data) {
	    var target = module._csiConsolidatedTarget || _csiConsolidatedTarget(module, sectionId, {});
	    var rows = data && Array.isArray(data.items) ? data.items.slice() : [];
	    var schema = _csiSectionSchema(sectionId);
	    var entityFilter = _csiPopulateEntityFilter(module, rows);
	    var searchInput = module.querySelector('[data-csi-section-search]');
	    var search = NORMALIZE(searchInput ? searchInput.value || '' : '');
	    var filtered = rows.filter(function(row) {
	      var entity = _csiEntityMeta(row);
	      if (entityFilter && entity.id !== entityFilter) return false;
	      if (!search) return true;
	      var haystack = [entity.name, entity.id];
	      schema.forEach(function(column) { haystack.push(_csiVisibleCellText(row, column)); });
	      return NORMALIZE(haystack.join(' ')).indexOf(search) !== -1;
	    });
	    filtered.sort(function(left, right) {
	      var leftEntity = _csiEntityMeta(left).name;
	      var rightEntity = _csiEntityMeta(right).name;
	      var entityDiff = SortUtils.text(leftEntity, rightEntity);
	      if (entityDiff !== 0) return entityDiff;
	      return SortUtils.text(_csiVisibleCellText(left, schema[0]), _csiVisibleCellText(right, schema[0]));
	    });
	    var headerHtml = '<th scope="col">' + escapeHtml(t('entities.entityCol', 'Entité')) + '</th>';
	    schema.forEach(function(column) {
	      headerHtml += '<th scope="col">' + escapeHtml(t(column.label, column.fallback)) + '</th>';
	    });
	    headerHtml += '<th scope="col" class="csi-actions-col">' + escapeHtml(t('riskAnalysis.listColumns.actions', 'Actions')) + '</th>';
	    var bodyHtml = '';
	    if (!filtered.length) {
	      bodyHtml = '<tr><td colspan="' + (schema.length + 2) + '" class="table__empty-state">' + escapeHtml(t('entities.noItems', 'Aucun élément.')) + '</td></tr>';
	    } else {
	      filtered.forEach(function(row) {
	        var entity = _csiEntityMeta(row);
	        bodyHtml += '<tr>' +
	          '<td><span class="entity-badge" title="' + escapeHtml(entity.name) + '">' + escapeHtml(entity.name) + '</span></td>';
	        schema.forEach(function(column) {
	          bodyHtml += '<td><div class="cell-clamp cell-clamp--2">' + escapeHtml(_csiVisibleCellText(row, column)) + '</div></td>';
	        });
	        bodyHtml += '<td class="csi-consolidated-actions">';
	        if (entity.id) {
	          bodyHtml += '<button type="button" class="btn btn--outline btn--sm" data-csi-open-source data-entity="' + escapeHtml(entity.id) + '" data-item="' + escapeHtml(row.id || '') + '">' +
	            '<i class="fas fa-arrow-right" aria-hidden="true"></i> ' + escapeHtml(t('entities.openInEntity', 'Ouvrir dans l\'entité')) + '</button>';
	        } else {
	          bodyHtml += '—';
	        }
	        bodyHtml += '</td></tr>';
	      });
	    }
	    var totalCount = data && data.total != null ? data.total : rows.length;
	    var loadedCount = data && data.loaded != null ? data.loaded : rows.length;
	    var countLabel = String(filtered.length) + '/' + String(totalCount);
	    if (data && data.truncated) countLabel += ' · ' + String(loadedCount) + ' ' + t('adminUi.loadedSuffix', 'chargés');
	    var truncatedHtml = data && data.truncated ?
	      '<small class="csi-consolidated-truncated" title="' + escapeHtml(t('entities.truncatedTitle', 'Affichage limité pour préserver la fluidité.')) + '">' +
	        '<i class="fas fa-triangle-exclamation" aria-hidden="true"></i> ' +
	        escapeHtml(t('entities.truncatedHint', 'Liste partielle — affinez par recherche ou entité.')) +
	      '</small>' : '';
	    var relatedHtml = _csiConsolidatedRelatedHtml(sectionId);
	    target.innerHTML =
	      '<div class="csi-consolidated-banner" role="status">' +
	        '<i class="fas fa-layer-group" aria-hidden="true"></i>' +
	        '<span>' + escapeHtml(t('entities.consoleHint', 'Lecture seule. Chaque ligne porte l\'entité d\'origine. L\'édition se fait dans le périmètre concerné.')) + '</span>' +
	        truncatedHtml +
	        '<strong>' + escapeHtml(countLabel) + '</strong>' +
	      '</div>' +
	      relatedHtml +
	      '<div class="table-container csi-consolidated-tablewrap">' +
	        '<table class="table csi-consolidated-table">' +
	          '<thead><tr>' + headerHtml + '</tr></thead>' +
	          '<tbody>' + bodyHtml + '</tbody>' +
	        '</table>' +
	      '</div>';
	    target.querySelectorAll('[data-csi-open-risk-analysis]').forEach(function(button) {
	      button.addEventListener('click', function() {
	        var C = _csiConsolidation();
	        if (C && C.setSectionMode) C.setSectionMode('risk-analysis', 'consolidated');
	        if (typeof switchModule === 'function') switchModule('risk-analysis');
	        else document.querySelector('[data-tab="risk-analysis"]')?.click();
	        if (typeof window.renderRiskAnalysisModule === 'function') window.renderRiskAnalysisModule();
	      });
	    });
	    target.querySelectorAll('[data-csi-open-source]').forEach(function(button) {
	      button.addEventListener('click', function() {
	        _csiOpenConsolidatedSource(sectionId, button.getAttribute('data-entity'), button.getAttribute('data-item'));
	      });
	    });
	  }
	  function _csiOpenConsolidatedSource(sectionId, entityId, itemId) {
	    var C = _csiConsolidation();
	    if (C && C.setSectionMode) C.setSectionMode(sectionId, 'scope');
	    if (!window._csiSwitchScope) {
	      if (typeof switchModule === 'function') switchModule(sectionId);
	      return;
	    }
	    window._csiSwitchScope(entityId === 'group' ? null : entityId).then(function(result) {
	      if (!result || result.ok === false) return;
	      if (typeof switchModule === 'function') switchModule(sectionId);
	      else if (typeof loadModuleContent === 'function') loadModuleContent(sectionId);
	      if (typeof showToast2 === 'function') {
	        showToast2(t('entities.openedInEntity', 'Périmètre source ouvert. L\'élément reste en consultation.'), 'info', 5000);
	      }
	      if (itemId) {
	        window._csiLastOpenedConsolidatedItem = { section: sectionId, id: itemId, at: Date.now() };
	      }
	    });
	  }
	  function _csiRenderConsolidatedSectionIfNeeded(sectionId, options) {
	    options = options || {};
	    var module = document.getElementById(sectionId);
	    var C = _csiConsolidation();
	    var allowed = !!(module && C && C.isAllowed && C.isAllowed() && C.isSectionConsolidable && C.isSectionConsolidable(sectionId));
	    if (!module) return false;
	    if (!allowed) {
	      _csiCleanupConsolidatedSection(module, { removeBar: true });
	      return false;
	    }
	    var consolidated = C.getSectionMode && C.getSectionMode(sectionId) === 'consolidated';
	    _csiEnsureSectionViewBar(sectionId, module, consolidated);
	    if (!consolidated) {
	      _csiCleanupConsolidatedSection(module, { removeBar: false });
	      return false;
	    }
	    module.classList.add('module--consolidated-read');
	    if (!options.contentId) _csiSetNativeHidden(module, true);
	    var target = _csiConsolidatedTarget(module, sectionId, options);
	    module._csiConsolidatedTarget = target;
	    target.innerHTML = '<div class="csi-consolidated-loading"><i class="fas fa-spinner fa-spin" aria-hidden="true"></i> ' + escapeHtml(t('entities.loading', 'Chargement…')) + '</div>';
	    var requestToken = Date.now() + ':' + Math.random();
	    module._csiConsolidatedToken = requestToken;
	    var force = !!module._csiForceConsolidatedFetch;
	    module._csiForceConsolidatedFetch = false;
	    C.fetchSection(sectionId, { force: force }).then(function(data) {
	      if (module._csiConsolidatedToken !== requestToken) return;
	      if (!C.isAllowed || !C.isAllowed() || C.getSectionMode(sectionId) !== 'consolidated') return;
	      module._csiConsolidatedData = data || { items: [], total: 0 };
	      _csiRenderConsolidatedSectionRows(module, sectionId, module._csiConsolidatedData);
	    }).catch(function(error) {
	      target.innerHTML = '<div class="csi-consolidated-error"><i class="fas fa-triangle-exclamation" aria-hidden="true"></i> ' + escapeHtml(t('entities.loadFailed', 'Chargement impossible')) + '</div>';
	      void 0;
	    });
	    return true;
	  }
	  window._csiRenderConsolidatedSectionIfNeeded = _csiRenderConsolidatedSectionIfNeeded;
  function validateDataQuality() {
    const issues = [];
    const collectDuplicates = (items, label, key = "title") => {
      const map = new Map();
      items.forEach((it) => {
        const val = (it[key] || "").trim().toLowerCase();
        if (!val) return;
        map.set(val, (map.get(val) || 0) + 1);
      });
      const dups = [...map.entries()].filter(([, count]) => count > 1);
      dups.forEach(([val, count]) => issues.push(`Doublon ${label}: "${val}" (${count}x)`));
    };
    collectDuplicates(appData.actions, "action");
    collectDuplicates(appData.risks, "risque");
    collectDuplicates(appData.documents, "document", "type");

    const controlsMissingSubcat = (appData.controls || []).filter(c => !c.subcategory || (Array.isArray(c.subcategory) && c.subcategory.length === 0));
    if (controlsMissingSubcat.length) {
      issues.push(`${controlsMissingSubcat.length} contrôle(s) sans sous-catégorie opérationnelle`);
    }

    const badLinks = (appData.documents || []).filter((d) => {
      if (!d.link) return false;
      try {
        const url = new URL(d.link);
        return !["http:", "https:"].includes(url.protocol);
      } catch (e) {
        return true;
      }
    });
    badLinks.forEach((doc) => issues.push(`Lien document invalide: ${doc.link || "(vide)"}`));

    if (issues.length === 0) {
      showToast2(t('toasts.noInconsistency'), "success");
      return;
    }
    const body = issues.map((i) => `- ${i}`).join("\n");
    openModal(t('modals.dataQuality'), [
      { name: "issues", label: "Points détectés", type: "textarea", value: body, readOnly: true }
    ], () => true);
  }
  function getHistoryLimit() {
    try {
      const raw = localStorage.getItem(HISTORY_LIMIT_STORAGE_KEY);
      const value = parseInt(raw, 10);
      if (Number.isFinite(value) && value >= 1 && value <= 30) return value;
    } catch (e) {
      // no-op
    }
    return HISTORY_LIMIT_DEFAULT;
  }
  function normalizeHistorySnapshot(entry) {
    const cleanLabel = typeof entry?.label === "string" ? entry.label.trim() : "";
    const data = typeof entry?.data === "string" ? entry.data : "";
    const tag = entry?.tag === "auto" ? "auto" : "manuel";
    const size = typeof entry?.size === "number" ? entry.size : (data ? data.length : 0);
    return {
      ...entry,
      label: cleanLabel || "Snapshot",
      tag,
      size,
      data
    };
  }
  function getHistorySnapshots() {
    // Mode serveur : historique en RAM uniquement
    // FIX-SNAPSHOT-MODE (audit 2026-06-10, bundle.js:1192) : utiliser le même prédicat
    // que saveData (isServerModeUnlocked, basé licence/édition) et non le protocole —
    // une édition locale servie en http(s) stockait ses données en localStorage mais ses
    // snapshots en RAM volatile (perdus à chaque rechargement).
    if (typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()) {
      return _ramHistorySnapshots.map(normalizeHistorySnapshot);
    }
    try {
      const raw = localStorage.getItem(HISTORY_KEY);
      const list = raw ? JSON.parse(raw) : [];
      if (!Array.isArray(list)) return [];
      return list.map(normalizeHistorySnapshot);
    } catch (e) {
      return [];
    }
  }
  function saveHistorySnapshots(list) {
    const limit = getHistoryLimit();
    const normalized = Array.isArray(list) ? list.map(normalizeHistorySnapshot) : [];
    normalized.sort((a, b) => SortUtils.date(a.date, b.date));
    // Mode serveur : stocker en RAM uniquement
    // FIX-SNAPSHOT-MODE : même prédicat que getHistorySnapshots (voir ci-dessus)
    if (typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()) {
      _ramHistorySnapshots = normalized.slice(-limit);
      return true;
    }
    try {
      localStorage.setItem(HISTORY_KEY, JSON.stringify(normalized.slice(-limit)));
      refreshStorageHealth();
      return true;
    } catch (e) {
      // FIX-SNAPSHOT-QUOTA (audit 2026-06-10) : ne plus échouer en silence (quota
      // localStorage dépassé) — retourner false pour que l'appelant alerte l'utilisateur.
      void 0;
      return false;
    }
  }
  function addHistorySnapshot(label = "Snapshot", options = {}) {
    try {
      ensureNis2Plan();
      ensureCriticalAssets();
      ensureProjectRisks();
      const data = JSON.stringify(appData);
      const cleanLabel = typeof label === "string" ? label.trim() : "Snapshot";
      const tag = options?.tag === "auto" ? "auto" : "manuel";
      const entry = {
        id: generateId("snapshot"),
        label: cleanLabel || "Snapshot",
        date: new Date().toISOString(),
        data,
        size: data.length,
        tag
      };
      const list = getHistorySnapshots();
      list.push(entry);
      // FIX-SNAPSHOT-QUOTA (audit 2026-06-10) : un échec d'écriture (quota localStorage)
      // affichait quand même le toast de succès — y compris pour les snapshots de
      // sécurité automatiques avant restauration/réinitialisation/import.
      if (!saveHistorySnapshots(list)) {
        showToast2(t('toasts.snapshotError', 'Impossible de créer le snapshot : {{message}}').replace('{{message}}', t('settings.storageInsufficient', 'Espace de stockage insuffisant')), "error", 8000);
        renderHistoryList();
        return false;
      }
      showToast2(t('toasts.snapshotSaved'), "success");
      renderHistoryList();
      return true;
    } catch (e) {
      showToast2(t('toasts.snapshotError', 'Impossible de créer le snapshot : {{message}}').replace('{{message}}', e.message), "error");
      return false;
    }
  }
  function formatSnapshotSize(bytes) {
    const size = Number(bytes);
    if (!Number.isFinite(size) || size <= 0) return "0 KB";
    if (size < 1024) return `${size} o`;
    const kb = size / 1024;
    if (kb < 1024) return `${kb.toFixed(1)} Ko`;
    const mb = kb / 1024;
    return `${mb.toFixed(1)} Mo`;
  }
  function renderHistoryList() {
    const container = document.getElementById("historyList");
    if (!container) return;
    const searchInput = document.getElementById("historySearch");
    const tagFilter = document.getElementById("historyTagFilter");
    const searchQuery = NORMALIZE(searchInput ? searchInput.value.trim() : "");
    const selectedTag = tagFilter ? tagFilter.value : "all";
    const list = getHistorySnapshots().slice().sort((a, b) => SortUtils.date(b.date, a.date));
    const filtered = list.filter((snap) => {
      const tagMatch = selectedTag === "all" || snap.tag === selectedTag;
      if (!tagMatch) return false;
      if (!searchQuery) return true;
      return NORMALIZE(snap.label).includes(searchQuery);
    });
    container.innerHTML = "";
    if (!list.length) {
      container.textContent = t('snapshots.empty', 'Aucun snapshot enregistré');
      const meta = document.getElementById("historyMeta");
      if (meta) {
        meta.innerHTML = "";
        const empty = document.createElement("div");
        empty.textContent = t('snapshots.historyEmpty', 'Historique vide.');
        meta.appendChild(empty);
      }
      return;
    }
    const meta = document.getElementById("historyMeta");
    if (meta) {
      meta.innerHTML = "";
      const countLine = document.createElement("div");
      const limit = getHistoryLimit();
      countLine.textContent = t('snapshots.kept', 'Snapshots conservés : {{count}}/{{limit}}').replace('{{count}}', list.length).replace('{{limit}}', limit);
      const visibleLine = document.createElement("div");
      visibleLine.textContent = t('snapshots.displayed', 'Affichés : {{count}}').replace('{{count}}', filtered.length);
      const latest = list[0];
      const latestLine = document.createElement("div");
      const latestDate = latest?.date ? new Date(latest.date).toLocaleString() : "N/A";
      latestLine.textContent = t('snapshots.latest', 'Dernier snapshot : {{date}}').replace('{{date}}', latestDate);
      const totalSize = list.reduce((sum, snap) => {
        const size = typeof snap.size === "number" ? snap.size : (snap.data ? snap.data.length : 0);
        return sum + (Number.isFinite(size) ? size : 0);
      }, 0);
      const sizeLine = document.createElement("div");
      sizeLine.textContent = t('snapshots.totalSize', 'Taille cumulée : {{size}}').replace('{{size}}', formatSnapshotSize(totalSize));
      meta.append(countLine, visibleLine, latestLine, sizeLine);
    }
    if (!filtered.length) {
      container.textContent = t('snapshots.noMatch', 'Aucun snapshot ne correspond aux filtres.');
      return;
    }
    filtered.forEach((snap) => {
      const item = document.createElement("div");
      item.className = "history-item";
      const date = snap.date ? new Date(snap.date).toLocaleString() : "N/A";
      const size = typeof snap.size === "number" ? snap.size : (snap.data ? snap.data.length : 0);
      const sizeLabel = formatSnapshotSize(size);
      const tagLabel = snap.tag === "auto" ? t('snapshots.tagAuto', 'Auto') : t('snapshots.tagManual', 'Manuel');
      item.innerHTML = `
        <div class="history-item__info">
          <div class="history-item__headline">
            <span class="history-item__tag history-item__tag--${escapeHtml(snap.tag)}">${tagLabel}</span>
            <div class="history-item__title">${escapeHtml(snap.label || "Snapshot")}</div>
          </div>
          <div class="history-item__meta">${escapeHtml(date)} • ${escapeHtml(sizeLabel)}</div>
        </div>
        <div class="history-item__actions">
          <button class="btn btn--outline btn--sm" data-action="restore" data-id="${snap.id}">${t('snapshots.restore', 'Restaurer')}</button>
          <button class="btn btn--outline btn--sm" data-action="download" data-id="${snap.id}">${t('snapshots.export', 'Exporter')}</button>
          <button class="btn btn--outline btn--sm" data-action="delete" data-id="${snap.id}">${t('common.delete', 'Supprimer')}</button>
        </div>
      `;
      container.appendChild(item);
    });
    container.querySelectorAll("[data-action]").forEach((btn) => {
      btn.addEventListener("click", (e) => {
        const id = e.currentTarget.dataset.id;
        const action = e.currentTarget.dataset.action;
        const snap = getHistorySnapshots().find((s) => s.id === id);
        if (!snap) return;
        if (action === "restore") {
          showConfirmModal(t('common.confirm', 'Confirmer'), t('confirms.restoreSnapshot', 'Restaurer ce snapshot ? Les données actuelles seront remplacées.'), function() {
            try {
              addHistorySnapshot(t('snapshots.backupBeforeRestore', 'Sauvegarde avant restauration'), { tag: "auto" });
            } catch (err) {
              void 0;
            }
            try {
              // Mode serveur : charger dans appData + push au serveur, PUIS recharger
              // FIX-C2: Attendre la fin du push serveur avant de recharger la page
              // FIX-SNAPSHOT-MODE (audit 2026-06-10) : prédicat aligné sur saveData — une
              // édition locale servie en http(s) prenait la branche serveur (push sans
              // auth = échec silencieux) et la restauration était perdue au rechargement.
              if (typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()) {
                var restoredData = JSON.parse(snap.data);
                Object.assign(appData, restoredData);
                window.appData = appData;
                if (window._csiInvalidateSearchIndex) window._csiInvalidateSearchIndex(); // (audit 2026-07-03) appData muté par restauration snapshot ; en cas d'échec sync (423) il n'y a pas de reload → index recherche à rafraîchir
                showToast2(t('toasts.snapshotRestored', 'Snapshot restauré, rechargement...'), "success");
                if (window.CSI_SyncManager && window.CSI_SyncManager.forceServerSync) {
                  window.CSI_SyncManager.forceServerSync().then(function(r) {
                    // FIX-RESTORE-423 (revue prod 2026-06-11) : forceServerSync RÉSOUT
                    // {success:false} sur 423/erreur (ne rejette plus). NE PAS recharger si
                    // le push a échoué — un reload écraserait la restauration locale par
                    // l'ancien état serveur. Message clair + on garde l'état restauré à l'écran.
                    if (r && r.success) {
                      setTimeout(function() { location.reload(); }, 300);
                    } else if (r && r.code === 423) {
                      showToast2(t('toasts.restoreNeedsLock', 'Restauration non appliquée au serveur : passez en mode édition (ou reprenez la main) puis recommencez.'), 'error', 12000);
                    } else {
                      showToast2(t('toasts.restoreSyncFailed', 'Échec de synchronisation de la restauration — non appliquée au serveur. Réessayez.'), 'error', 12000);
                    }
                  }).catch(function(err) {
                    void 0;
                    showToast2(t('toasts.restoreSyncFailed', 'Échec de synchronisation de la restauration — non appliquée au serveur. Réessayez.'), 'error', 12000);
                  });
                } else {
                  setTimeout(function() { location.reload(); }, 300);
                }
              } else {
                // Mode local : comportement inchangé
                localStorage.setItem("smsi_data", snap.data);
                showToast2(t('toasts.snapshotRestored', 'Snapshot restauré, rechargement...'), "success");
                setTimeout(() => location.reload(), 300);
              }
            } catch(e) {
              void 0;
              showToast2(t('toasts.restoreFailed', 'Impossible de restaurer le snapshot'), 'error');
            }
          }, { type: 'warning' });
        } else if (action === "download") {
          downloadJsonBackup(snap.data, `smsi_snapshot_${snap.id}.json`);
        } else if (action === "delete") {
          showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.deleteSnapshot', 'Supprimer ce snapshot ?'), function() {
            const nextList = getHistorySnapshots().filter((s) => s.id !== snap.id);
            saveHistorySnapshots(nextList);
            renderHistoryList();
          }, { type: 'danger' });
        }
      });
    });
  }
  function clearHistorySnapshots() {
    showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.clearSnapshots', "Supprimer tout l'historique des snapshots ?"), function() {
      try {
        // FIX-SNAPSHOT-MODE : prédicat aligné + nettoyage des deux emplacements en mode
        // local (d'éventuels snapshots RAM hérités d'une session http-local pré-correctif).
        if (typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()) {
          _ramHistorySnapshots = [];
        } else {
          _ramHistorySnapshots = [];
          localStorage.removeItem(HISTORY_KEY);
        }
        renderHistoryList();
        refreshStorageHealth();
        showToast2(t('toasts.snapshotHistoryCleared'), "info");
      } catch (e) {
        void 0;
        showToast2(t('toasts.snapshotHistoryError', "Impossible de supprimer l'historique"), "error");
      }
    }, { type: 'danger' });
  }
  window.addHistorySnapshot = addHistorySnapshot;
  window.refreshStorageHealth = refreshStorageHealth;

  // ============================================================
  // SORT UTILITIES — Generic, locale-aware sorting
  // ============================================================
  const SortUtils = (() => {
    const collator = new Intl.Collator("fr", { sensitivity: "base", numeric: true });

    return {
      /** Locale-aware text comparison (French, case/accent insensitive, numeric) */
      text(a, b) {
        return collator.compare(a || "", b || "");
      },

      /** Numeric comparison with fallback for null/undefined */
      numeric(a, b, fallback) {
        const fa = fallback !== undefined ? fallback : 0;
        return (a ?? fa) - (b ?? fa);
      },

      /** ISO date string comparison. nullsLast=true puts empty dates at the end */
      date(a, b, nullsLast) {
        const sentinel = (nullsLast !== false) ? 8640000000000000 : -8640000000000000;
        const da = a ? Date.parse(a) : NaN;
        const db = b ? Date.parse(b) : NaN;
        return (Number.isFinite(da) ? da : sentinel) - (Number.isFinite(db) ? db : sentinel);
      },

      /** Creates a comparator from an enum→order map. Unknown values sort last (fallback). */
      orderedMap(map, fallback) {
        const fb = fallback !== undefined ? fallback : 99;
        return function(a, b) { return (map[a] ?? fb) - (map[b] ?? fb); };
      },

      /** Reverses a comparator's direction */
      desc(compareFn) {
        return function(a, b) { return compareFn(b, a); };
      },

      /** Chains multiple comparators (first non-zero wins) */
      chain() {
        var fns = Array.prototype.slice.call(arguments);
        return function(a, b) {
          for (var i = 0; i < fns.length; i++) {
            var r = fns[i](a, b);
            if (r !== 0) return r;
          }
          return 0;
        };
      },

      /** Reference string sorter using collator (e.g., "A.5.1" < "A.5.10") */
      reference(a, b) {
        return collator.compare(a || "", b || "");
      }
    };
  })();
  window.SortUtils = SortUtils;

  // ============================================================
  // AUDIT LOG (Traçabilité)
  // ============================================================
  const AUDIT_ACTION_ICONS = { add: 'fa-plus', edit: 'fa-pen', delete: 'fa-trash', import: 'fa-file-import', export: 'fa-file-export', config: 'fa-gear' };
  const AUDIT_CAT_KEYS = { controls: 'catControls', actions: 'catActions', risks: 'catRisks', audits: 'catAudits', nc: 'catNc', reviews: 'catReviews', kpis: 'catKpis', documents: 'catDocuments', stakeholders: 'catStakeholders', threats: 'catThreats', riskAnalysis: 'catRiskAnalysis', projectRisks: 'catProjectRisks', criticalAssets: 'catCriticalAssets', impactMatrices: 'catImpactMatrices', nis2: 'catNis2', soa: 'catSoa', settings: 'catSettings', system: 'catSystem' };

  function getAuditUser() {
    if (window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn && window.CSI_AuthManager.isLoggedIn()) {
      var u = window.CSI_AuthManager.getCurrentUser && window.CSI_AuthManager.getCurrentUser();
      if (u) return typeof u === 'string' ? u : (u.email || u.name || u.username || t('audit.localUser'));
    }
    return t('audit.localUser');
  }

  function addAuditEntry(action, category, target, details) {
    if (!appData.auditLog) appData.auditLog = [];
    // FIX-AUDITLOG-CORPLOCAL (audit 2026-06-10) : isServerModeUnlocked() retourne toujours
    // false en file:// — la licence Corporate Local active (CSI_LocalTrialManager) doit
    // aussi débloquer le plafond de 100 entrées des éditions payantes.
    // FIX-AUDITLOG-EDITION (revue audit 2026-06-11) : baser le plafond sur l'ÉDITION
    // (window._csiEdition, synchrone et fiable) et non sur isLicensed() — ce dernier peut
    // être transitoirement faux (détection async non finie au boot, Firefox+file:// sans
    // crypto.subtle, licence expirée), ce qui aurait fait retomber le plafond à 10 et
    // TRONQUÉ IRRÉVERSIBLEMENT un journal de 100 entrées au prochain ajout.
    var _ed = window._csiEdition;
    var _paidEdition = !!(_ed && (_ed.id === 'corporate-local' || _ed.id === 'corporate-server'));
    var limit = ((typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()) || _paidEdition) ? 100 : (DEMO_MODE ? 30 : 10);
    appData.auditLog.push({
      id: generateId("audit"),
      timestamp: new Date().toISOString(),
      user: getAuditUser(),
      action: action || 'edit',
      category: category || 'system',
      target: (target || '').substring(0, 120),
      details: (details || '').substring(0, 200)
    });
    // Le plafond est désormais STABLE (dépend de l'édition, pas d'un état transitoire) :
    // le slice normal ne risque plus de jeter brutalement 90 entrées sur une bascule.
    if (appData.auditLog.length > limit) {
      appData.auditLog = appData.auditLog.slice(-limit);
    }
    // FIX-GLOBALMETA-SYNC (audit 2026-06-10) : marquer le journal comme à pousser
    // (saveData enverra le pseudo-onglet 'global-meta' en mode serveur).
    window._csiGlobalMetaDirty = true;
    // Persist audit entries immediately
    if (typeof saveData === 'function') saveData();
  }
  window.addAuditEntry = addAuditEntry;

  function getAuditActionLabel(action) {
    var map = { add: 'audit.actionAdd', edit: 'audit.actionEdit', delete: 'audit.actionDelete', import: 'audit.actionImport', export: 'audit.actionExport', config: 'audit.actionConfig' };
    return t(map[action] || map.edit);
  }
  function getAuditCatLabel(cat) {
    var key = AUDIT_CAT_KEYS[cat];
    return key ? t('audit.' + key) : cat;
  }

  function updateDashboardActivity() {
    var list = document.getElementById('activityList');
    if (!list) return;
    var log = (appData.auditLog || []).slice(-3).reverse();
    if (!log.length) {
      list.innerHTML = '<div class="dashboard-activity__empty">' + escapeHtml(t('audit.noActivity')) + '</div>';
      return;
    }
    var html = '';
    log.forEach(function(entry) {
      var d;
      try { d = new Date(entry.timestamp); } catch(e) { d = new Date(); }
      var timeStr = String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
      var dayDiff = Math.round((Date.now() - d.getTime()) / 86400000);
      if (dayDiff >= 1) timeStr = d.toLocaleDateString(undefined, { day:'2-digit', month:'2-digit' });
      var iconClass = AUDIT_ACTION_ICONS[entry.action] || 'fa-pen';
      html += '<div class="activity-item">';
      html += '<span class="activity-item__time">' + timeStr + '</span>';
      html += '<span class="activity-item__icon activity-item__icon--' + escapeHtml(entry.action) + '"><i class="fas ' + iconClass + '"></i></span>';
      html += '<span class="activity-item__text" title="' + escapeHtml(entry.target || '') + '">' + escapeHtml(entry.target || '-') + '</span>';
      html += '</div>';
    });
    list.innerHTML = html;
  }
  window.updateDashboardActivity = updateDashboardActivity;

  function renderAuditLogModule() {
    var container = document.getElementById('auditLogContent');
    if (!container) return;
    var log = (appData.auditLog || []).slice().reverse();
    var html = '<div class="audit-page">';
    // Header
    html += '<div class="audit-page__header">';
    html += '<div class="audit-page__title"><i class="fas fa-clock-rotate-left"></i> ' + escapeHtml(t('audit.title')) + '</div>';
    html += '</div>';
    // Toolbar v2 : même langage visuel que SectionToolbar (markup généré à la main —
    // le composant s'insère via .module__header/.table-container, inapplicable ici).
    html += '<div class="section-toolbar">';
    html += '<div class="section-toolbar__row section-toolbar__filters">';
    html += '<input type="search" id="auditFilterSearch" class="form-control form-control--sm section-toolbar__search" placeholder="' + escapeHtml(t('common.search', 'Rechercher...')) + '..." aria-label="' + escapeHtml(t('common.search', 'Rechercher...')) + '">';
    html += '<select id="auditFilterAction" class="form-control form-control--sm" aria-label="' + escapeHtml(t('audit.columnAction')) + '"><option value="">' + escapeHtml(t('audit.columnAction')) + '</option>';
    ['add','edit','delete','import','export','config'].forEach(function(a) {
      html += '<option value="' + a + '">' + escapeHtml(getAuditActionLabel(a)) + '</option>';
    });
    html += '</select>';
    html += '<select id="auditFilterCategory" class="form-control form-control--sm" aria-label="' + escapeHtml(t('audit.columnCategory')) + '"><option value="">' + escapeHtml(t('audit.columnCategory')) + '</option>';
    Object.keys(AUDIT_CAT_KEYS).forEach(function(c) {
      html += '<option value="' + escapeHtml(c) + '">' + escapeHtml(getAuditCatLabel(c)) + '</option>';
    });
    html += '</select>';
    html += '<button class="btn btn--danger btn--sm section-toolbar__end" id="auditClearBtn" data-readonly-lock="true"><i class="fas fa-trash"></i> ' + escapeHtml(t('audit.clearHistory')) + '</button>';
    html += '</div></div>';
    // Table — Lot 1b : 4 colonnes (Catégorie fusionnée dans Cible, Détails en tooltip +
    // ligne extensible au clic), enveloppée dans .table-container comme les autres tableaux.
    html += '<div class="table-container"><table class="table--audit"><thead><tr>';
    html += '<th>' + escapeHtml(t('audit.columnDate')) + '</th>';
    html += '<th>' + escapeHtml(t('audit.columnUser')) + '</th>';
    html += '<th>' + escapeHtml(t('audit.columnAction')) + '</th>';
    html += '<th>' + escapeHtml(t('audit.columnTarget')) + '</th>';
    html += '</tr></thead><tbody id="auditTableBody">';
    if (!log.length) {
      html += '<tr><td colspan="4" class="audit-empty">' + escapeHtml(t('audit.emptyState')) + '</td></tr>';
    } else {
      log.forEach(function(entry) {
        var d;
        try { d = new Date(entry.timestamp); } catch(e) { d = new Date(); }
        var dateStr = d.toLocaleDateString() + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
        var hasDetails = !!(entry.details && String(entry.details).trim());
        html += '<tr data-action="' + escapeHtml(entry.action) + '" data-category="' + escapeHtml(entry.category || '') + '" data-target="' + escapeHtml((entry.target || '').toLowerCase()) + '" data-user="' + escapeHtml((entry.user || '').toLowerCase()) + '" data-details="' + escapeHtml((entry.details ? String(entry.details) : '').toLowerCase()) + '"'
          + (hasDetails ? ' class="audit-row--expandable" title="' + escapeHtml(entry.details) + '"' : '') + '>';
        html += '<td style="white-space:nowrap">' + escapeHtml(dateStr) + '</td>';
        html += '<td>' + escapeHtml(entry.user || '') + '</td>';
        html += '<td><span class="audit-badge audit-badge--' + escapeHtml(entry.action) + '"><i class="fas ' + (AUDIT_ACTION_ICONS[entry.action] || 'fa-pen') + '"></i> ' + escapeHtml(getAuditActionLabel(entry.action)) + '</span></td>';
        html += '<td class="audit-target" title="' + escapeHtml(entry.target || '') + '"><span class="audit-cat-badge">' + escapeHtml(getAuditCatLabel(entry.category)) + '</span> · ' + escapeHtml(entry.target || '—') + '</td>';
        html += '</tr>';
        if (hasDetails) {
          html += '<tr class="audit-details-row" hidden><td colspan="4" class="audit-details">' + escapeHtml(entry.details) + '</td></tr>';
        }
      });
    }
    html += '</tbody></table></div></div>';
    container.innerHTML = html;
    // Filters
    var filterAction = document.getElementById('auditFilterAction');
    var filterCategory = document.getElementById('auditFilterCategory');
    var filterSearch = document.getElementById('auditFilterSearch');
    var clearBtn = document.getElementById('auditClearBtn');
    function applyFilters() {
      var action = filterAction ? filterAction.value : '';
      var category = filterCategory ? filterCategory.value : '';
      var search = filterSearch ? filterSearch.value.toLowerCase().trim() : '';
      var rows = document.querySelectorAll('#auditTableBody tr[data-action]');
      rows.forEach(function(row) {
        var matchAction = !action || row.dataset.action === action;
        var matchCategory = !category || row.dataset.category === category;
        // Recherche étendue : cible + utilisateur + détails
        var haystack = (row.dataset.target || '') + ' ' + (row.dataset.user || '') + ' ' + (row.dataset.details || '');
        var matchSearch = !search || haystack.indexOf(search) >= 0;
        var visible = matchAction && matchCategory && matchSearch;
        row.style.display = visible ? '' : 'none';
        // Ligne de détails associée : masquée si sa ligne parente est filtrée,
        // et repliée à chaque changement de filtre.
        var next = row.nextElementSibling;
        if (next && next.classList.contains('audit-details-row')) {
          next.hidden = true;
          next.style.display = visible ? '' : 'none';
        }
      });
    }
    if (filterAction) filterAction.addEventListener('change', applyFilters);
    if (filterCategory) filterCategory.addEventListener('change', applyFilters);
    if (filterSearch) filterSearch.addEventListener('input', applyFilters);
    // Lot 1b : clic sur une ligne = déplier/replier les détails
    var auditBody = document.getElementById('auditTableBody');
    if (auditBody) auditBody.addEventListener('click', function(e) {
      var row = e.target && e.target.closest ? e.target.closest('tr[data-action]') : null;
      if (!row) return;
      var next = row.nextElementSibling;
      if (next && next.classList.contains('audit-details-row')) next.hidden = !next.hidden;
    });
    if (clearBtn) clearBtn.addEventListener('click', function() {
      showConfirmModal(
        t('audit.clearTitle', 'Effacer le journal'),
        t('audit.clearConfirm'),
        function() {
          appData.auditLog = [];
          saveData();
          renderAuditLogModule();
        },
        { type: 'danger' }
      );
    });
    if (typeof window.refreshReadOnlyLocks === 'function') window.refreshReadOnlyLocks();
  }
  window.renderAuditLogModule = renderAuditLogModule;

  function shouldShowWelcome(hasCachedData) {
    // FIX-WIZARD-SERVER (audit 2026-06-10) : évaluer la branche serveur AVANT le
    // court-circuit hasCachedData. En http(s), hasCachedData est forcé à true
    // (isServerMode() basé sur le protocole), ce qui rendait la branche wasSetup()
    // inatteignable — régression 2d9a728 sur le correctif 67e4826. Le comportement
    // local (file://) est inchangé : isServerModeUnlocked() y retourne false.
    // Mode serveur : le login wall s'exécute AVANT ce check, donc isLoggedIn()=true.
    // On ne montre le wizard que si c'est le tout premier setup admin (wasSetup=true).
    // Les utilisateurs normaux qui se connectent ensuite ne voient pas le wizard.
    if (typeof isServerModeUnlocked === "function" && isServerModeUnlocked()
        && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn()) {
      return !!(window.CSI_AuthManager.wasSetup && window.CSI_AuthManager.wasSetup());
    }
    if (hasCachedData) return false;
    if (typeof shouldAutoUnlockForTests === "function" && shouldAutoUnlockForTests()) return false;
    try {
      return !localStorage.getItem(WELCOME_STORAGE_KEY);
    } catch (e) {
      return false;
    }
  }
  // =====================================================
  // WIZARD DE CONFIGURATION AU PREMIER LANCEMENT
  // =====================================================

  const WIZARD_IMAGE_URL = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23e8d5e0'/%3E%3Cstop offset='50%25' style='stop-color:%23d4c4d9'/%3E%3Cstop offset='100%25' style='stop-color:%23c9d4e8'/%3E%3C/linearGradient%3E%3ClinearGradient id='cloud' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23ffffff;stop-opacity:0.9'/%3E%3Cstop offset='100%25' style='stop-color:%23f0f4f8;stop-opacity:0.8'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect fill='url(%23bg)' width='400' height='300'/%3E%3Cellipse cx='200' cy='120' rx='140' ry='80' fill='url(%23cloud)' stroke='%23ffffff' stroke-width='3'/%3E%3Crect x='110' y='80' width='180' height='100' rx='8' fill='%23ffffff' fill-opacity='0.95' stroke='%23e0e0e0'/%3E%3Ccircle cx='120' cy='90' r='3' fill='%23ff6b6b'/%3E%3Ccircle cx='130' cy='90' r='3' fill='%23ffd93d'/%3E%3Ccircle cx='140' cy='90' r='3' fill='%2332d296'/%3E%3Crect x='125' y='110' width='50' height='55' rx='4' fill='%23f8f9fa' stroke='%23e9ecef'/%3E%3Crect x='185' y='110' width='50' height='55' rx='4' fill='%23f8f9fa' stroke='%23e9ecef'/%3E%3Crect x='245' y='110' width='40' height='55' rx='4' fill='%23f8f9fa' stroke='%23e9ecef'/%3E%3Cpath d='M150 125 L150 145 L140 145 L150 155 L160 145 L150 145' fill='none' stroke='%231fb8cd' stroke-width='2'/%3E%3Ccircle cx='210' cy='137' r='12' fill='none' stroke='%231fb8cd' stroke-width='2'/%3E%3Ccircle cx='210' cy='137' r='4' fill='%231fb8cd'/%3E%3Cpath d='M260 125 L270 125 L270 150 L260 150 M265 130 L265 145' fill='none' stroke='%231fb8cd' stroke-width='2'/%3E%3Cpath d='M180 230 C180 200 200 190 200 190 L200 190' fill='none' stroke='%23d4a5a5' stroke-width='8' stroke-linecap='round'/%3E%3Cellipse cx='200' cy='250' rx='60' ry='8' fill='%23c9c9c9' fill-opacity='0.3'/%3E%3C/svg%3E";

  function showSetupWizard(onComplete) {
    const _t = window.CSI_i18n ? window.CSI_i18n.t.bind(window.CSI_i18n) : (k) => k;
    // Skip if already shown or not first launch
    const isFirstLang = window.CSI_i18n && window.CSI_i18n.isFirstLaunch();
    const isFirstWelcome = shouldShowWelcome(false);

    if (!isFirstLang && !isFirstWelcome) {
      if (onComplete) onComplete();
      return;
    }

    if (document.getElementById("setupWizardOverlay")) {
      if (onComplete) onComplete();
      return;
    }

    // Kick license detection early if not yet started (async, non-blocking)
    // This ensures isServerModeUnlocked() returns true ASAP for shouldShow() checks
    var LM = window.CSI_LicenseManager;
    if (LM && !LM.isDetected() && window.location.protocol !== 'file:') {
      LM.detect().catch(function() {});
    }

    let currentStep = 0;
    const steps = [];

    // Step 1: Language selection (if needed)
    if (isFirstLang) {
      steps.push({
        id: "language",
        title: "Welcome",
        subtitle: "Choose your language",
        content: () => {
          const langs = window.CSI_i18n.getAvailableLanguages();
          return `
            <div class="wizard-language-grid">
              ${Object.entries(langs).map(([code, info]) => `
                <button class="wizard-lang-btn" type="button" data-lang="${code}">
                  <span class="wizard-lang-btn__flag">${info.flag}</span>
                  <span class="wizard-lang-btn__name">${info.name}</span>
                  <span class="wizard-lang-btn__check"><i class="fas fa-check"></i></span>
                </button>
              `).join("")}
            </div>
            <p class="wizard-hint" id="setupWizardLangHint">You can change this choice in settings.</p>
          `;
        },
        onEnter: (stepEl) => {
          const langBtns = stepEl.querySelectorAll(".wizard-lang-btn");
          langBtns.forEach(btn => {
            btn.addEventListener("click", () => {
              langBtns.forEach(b => b.classList.remove("wizard-lang-btn--selected"));
              btn.classList.add("wizard-lang-btn--selected");
            });
          });
        },
        onLeave: (stepEl) => {
          const selectedBtn = stepEl.querySelector(".wizard-lang-btn--selected");
          let lang = selectedBtn ? selectedBtn.dataset.lang : null;
          if (lang && window.CSI_i18n) {
            window.CSI_i18n.setLanguage(lang);
            window.CSI_i18n.markLanguageChosen();
            window.CSI_i18n.translatePage();
            updateWizardTexts();
          }
        },
        canProceed: (stepEl) => {
          return !!stepEl.querySelector(".wizard-lang-btn--selected");
        }
      });
    }

    // Step: License activation (HTTP(S) mode + trial detected, or Corporate Local in file://)
    var TM = window.CSI_TrialManager;
    var _isCorporateLocal = window._csiEdition && window._csiEdition.id === 'corporate-local';
    if (TM && TM.isDetected() && (window.location.protocol !== 'file:' || _isCorporateLocal)) {
      var trialActive = TM.isTrialActive();
      var trialExpired = TM.isTrialExpired() || TM.isLicenseExpired();
      var needsLicense = TM.needsLicense();

      if (trialActive || needsLicense) {
        steps.push({
          id: 'license',
          title: _t('license.title', 'Activation'),
          subtitle: needsLicense ? _t('license.trialExpired', 'Période d\'essai expirée') : _t('license.trialActive', 'Période d\'essai active'),
          content: function() {
            var daysLeft = TM.getDaysLeft();
            var badgeHtml = '';
            if (trialActive) {
              badgeHtml = '<div class="csi-trial-banner csi-trial-banner--active" style="padding:12px 16px;border-radius:8px;margin-bottom:16px;text-align:center;font-size:0.95rem;">' +
                '<i class="fas fa-clock"></i> ' + escapeHtml(_t('wizard.trialBadge', 'Essai gratuit — {days} jours restants sur 14').replace('{days}', String(daysLeft))) +
                '</div>' +
                '<p style="font-size:0.85rem;color:var(--color-text-secondary,#666);margin:0 0 12px;line-height:1.5;">' +
                escapeHtml(_t('wizard.trialExplain', 'Vous disposez d\'un essai gratuit de 14 jours pour tester le mode serveur avec synchronisation, co-édition et gestion des utilisateurs.')) +
                '</p>' +
                '<p style="font-size:0.85rem;color:var(--color-text-secondary,#666);margin:0 0 16px;line-height:1.5;">' +
                escapeHtml(_t('wizard.trialExpiry', 'Après l\'expiration, les données restent consultables mais les modifications seront bloquées jusqu\'à l\'activation d\'une clé de licence.')) +
                '</p>';
            } else {
              badgeHtml = '<div class="csi-trial-banner csi-trial-banner--expired" style="padding:12px 16px;border-radius:8px;margin-bottom:16px;text-align:center;font-size:0.95rem;">' +
                '<i class="fas fa-exclamation-triangle"></i> ' + _t('license.readOnlyNotice', 'Mode lecture seule — entrez une clé de licence pour réactiver') +
                '</div>';
            }
            return badgeHtml +
              '<div class="wizard-step-license">' +
              '<label style="display:block;margin-bottom:6px;font-weight:500;font-size:0.9rem;">' + _t('license.pasteKey', 'Collez votre clé d\'activation') + '</label>' +
              '<textarea id="wizardLicenseKey" class="csi-license-textarea" placeholder="CSI2-..." ' +
              'style="width:100%;min-height:80px;padding:10px;border:1px solid var(--color-border,#ddd);border-radius:6px;font-family:monospace;font-size:0.85rem;resize:vertical;box-sizing:border-box;"></textarea>' +
              '<button id="wizardLicenseActivateBtn" type="button" style="margin-top:8px;padding:8px 18px;border:none;border-radius:6px;background:var(--color-primary,#3b82f6);color:#fff;font-size:0.9rem;cursor:pointer;">' + _t('license.activateBtn', 'Activer') + '</button>' +
              '<div id="wizardLicenseMsg" style="margin-top:8px;font-size:0.9rem;display:none;padding:8px 12px;border-radius:6px;"></div>' +
              '</div>';
          },
          onEnter: function(stepEl) {
            var keyInput = stepEl.querySelector('#wizardLicenseKey');
            if (keyInput) setTimeout(function() { keyInput.focus(); }, 100);
            // Wire up Activate button
            var activateBtn = stepEl.querySelector('#wizardLicenseActivateBtn');
            if (activateBtn && TM) {
              activateBtn.addEventListener('click', function() {
                var ki = stepEl.querySelector('#wizardLicenseKey');
                var key = ki ? (ki.value || '').trim() : '';
                var msgDiv = stepEl.querySelector('#wizardLicenseMsg');
                if (!key || key.indexOf('CSI2-') !== 0) {
                  if (msgDiv) {
                    msgDiv.style.display = 'block';
                    msgDiv.textContent = _t('license.error', 'Clé de licence invalide');
                    msgDiv.style.background = 'var(--color-danger-bg,#fef2f2)';
                    msgDiv.style.color = 'var(--color-danger,#dc3545)';
                  }
                  return;
                }
                activateBtn.disabled = true;
                activateBtn.textContent = '...';
                TM.activate(key).then(function(result) {
                  stepEl._licenseOk = true;
                  if (msgDiv) {
                    msgDiv.style.display = 'block';
                    msgDiv.textContent = _t('license.success', 'Licence activée avec succès');
                    msgDiv.style.background = 'var(--color-success-bg,#f0fdf4)';
                    msgDiv.style.color = 'var(--color-success,#22c55e)';
                  }
                  activateBtn.textContent = _t('license.activated', 'Activee');
                }).catch(function(err) {
                  stepEl._licenseOk = false;
                  if (msgDiv) {
                    msgDiv.style.display = 'block';
                    msgDiv.textContent = err.message || _t('license.error', 'Clé invalide');
                    msgDiv.style.background = 'var(--color-danger-bg,#fef2f2)';
                    msgDiv.style.color = 'var(--color-danger,#dc3545)';
                  }
                  activateBtn.disabled = false;
                  activateBtn.textContent = _t('license.activateBtn', 'Activer');
                });
              });
            }
          },
          onLeave: function(stepEl) {
            // Activation is now handled by the Activate button
          },
          canProceed: function(stepEl) {
            // If trial expired, license must be activated successfully
            if (needsLicense) {
              if (!stepEl._licenseOk) {
                var msgDiv = stepEl.querySelector('#wizardLicenseMsg');
                if (msgDiv) {
                  msgDiv.style.display = 'block';
                  msgDiv.textContent = _t('license.mustActivate', 'Veuillez activer votre licence avant de continuer');
                  msgDiv.style.background = 'var(--color-danger-bg,#fef2f2)';
                  msgDiv.style.color = 'var(--color-danger,#dc3545)';
                }
                return false;
              }
            }
            return true;
          }
        });
      }
    }

    // Step: Framework selection (only on first welcome, if multiple frameworks available, and not forced by edition)
    var _edWiz = window._csiEdition;
    if (_edWiz && _edWiz.forcedFrameworks) {
      // Edition avec frameworks forcés → activer automatiquement sans wizard
      if (typeof appData !== 'undefined') {
        if (!appData.frameworkMapper) appData.frameworkMapper = {};
        appData.frameworkMapper.enabledFrameworks = _defaultEnabledFrameworks();
      }
    } else if (isFirstWelcome && window._csiFrameworks && Object.keys(window._csiFrameworks).length > 0) {
      steps.push({
        id: "frameworks",
        title: _t('setup.frameworksTitle'),
        subtitle: _t('setup.frameworksSubtitle'),
        content: () => {
          var fws = window._csiFrameworks;
          var ids = Object.keys(fws);
          // Deduplicate aliases: skip frameworks whose aliasOf target is also loaded
          var seen = [];
          ids = ids.filter(function(id) {
            var meta = fws[id].meta || {};
            // If this framework is an alias of another loaded framework, skip it
            if (meta.aliasOf && fws[meta.aliasOf]) return false;
            // Also check object identity (e.g. 'nis2' -> 'cyfun2025' share same object)
            for (var i = 0; i < seen.length; i++) {
              if (fws[id] === fws[seen[i]]) return false;
            }
            seen.push(id);
            return true;
          });
          var items = ids.map(function(id) {
            var meta = fws[id].meta || {};
            var isMandatory = !!meta.mandatory;
            var controlCount = meta.controlCount || meta.clauseCount || (fws[id].controls ? fws[id].controls.length : (fws[id].clauses ? fws[id].clauses.length : (fws[id].requirements ? fws[id].requirements.length : 0)));
            var iconClass = meta.icon || 'fa-shield-halved';
            var color = meta.color || 'var(--color-primary)';
            return '<div class="wizard-framework-item' + (isMandatory ? ' wizard-framework-item--locked' : '') + '" data-fw-id="' + id + '">' +
              '<div class="wizard-framework-item__icon" style="color:' + color + '"><i class="fas ' + iconClass + '"></i></div>' +
              '<div class="wizard-framework-item__info">' +
                '<strong>' + (meta.shortName || meta.name || id) + '</strong>' +
                '<span>' + (meta.name || id) + ' — ' + controlCount + ' ' + _t('setup.frameworksControls') + '</span>' +
              '</div>' +
              '<div class="wizard-framework-item__toggle">' +
                (isMandatory
                  ? '<span class="wizard-framework-item__badge"><i class="fas fa-lock"></i> ' + _t('setup.frameworksLocked') + '</span>'
                  : '<label class="toggle" style="margin:0"><input type="checkbox" class="toggle__input" data-fw-toggle="' + id + '" checked><span class="toggle__slider"></span></label>'
                ) +
              '</div>' +
            '</div>';
          });
          return '<div class="wizard-frameworks-list">' + items.join('') + '</div>' +
            '<p class="wizard-hint"><i class="fas fa-info-circle"></i> ' + _t('setup.frameworksHint') + '</p>';
        },
        onEnter: function(stepEl) {
          // Toggle visual feedback on item click (not for locked items)
          var items = stepEl.querySelectorAll('.wizard-framework-item:not(.wizard-framework-item--locked)');
          items.forEach(function(item) {
            item.addEventListener('click', function(e) {
              if (e.target.closest('.toggle')) return;
              var toggle = item.querySelector('.toggle__input');
              if (toggle) toggle.checked = !toggle.checked;
            });
          });
        },
        onLeave: function(stepEl) {
          // Collect enabled frameworks and save
          var enabled = [];
          var fws = window._csiFrameworks;
          Object.keys(fws).forEach(function(id) {
            var meta = fws[id].meta || {};
            if (meta.mandatory) {
              enabled.push(id);
              return;
            }
            var toggle = stepEl.querySelector('[data-fw-toggle="' + id + '"]');
            if (toggle && toggle.checked) {
              enabled.push(id);
            } else if (!toggle) {
              // This ID was deduped (alias) — check if its target object is enabled
              for (var j = 0; j < enabled.length; j++) {
                if (fws[enabled[j]] === fws[id]) {
                  enabled.push(id); // include alias too
                  break;
                }
              }
            }
          });
          if (typeof appData !== 'undefined') {
            if (!appData.frameworkMapper) appData.frameworkMapper = {};
            appData.frameworkMapper.enabledFrameworks = enabled;
            if (typeof saveData === 'function') saveData();
          }
        },
        canProceed: function() { return true; }
      });

      // Step: NIS2 Assurance Level (conditional — shown only if NIS2/CyFun2025 selected in frameworks step)
      steps.push({
        id: 'nis2-level',
        title: _t('wizard.nis2LevelTitle', "Niveau d'assurance NIS2"),
        subtitle: _t('wizard.nis2LevelSub', 'Définissez le niveau CyFun correspondant à votre organisation'),
        // Remplacé par l'étape combinée 'nis2-setup' (référentiel NIS2 + degré). Conservé inerte.
        shouldShow: function() { return false; },
        content: function() {
          return '<div class="wizard-nis2-level">' +
            '<div class="cyfun-level-info">' +
              '<i class="fas fa-info-circle cyfun-level-info__icon"></i>' +
              '<strong>' + _t('wizard.nis2LevelAdviceLabel', 'Conseil :') + '</strong> ' +
              _t('wizard.nis2LevelAdvice', 'Avant de choisir, nous vous recommandons d\'utiliser l\'outil d\'auto-identification du') +
              ' <a href="https://atwork.safeonweb.be/fr/nis2" target="_blank" rel="noopener">CCB (Centre for Cybersecurity Belgium)</a> ' +
              _t('wizard.nis2LevelAdvice2', 'pour déterminer votre niveau d\'assurance NIS2.') +
            '</div>' +
            '<p class="cyfun-level-option__desc">' + _t('wizard.nis2LevelSelect', 'Sélectionnez le niveau d\'assurance NIS2 correspondant à votre organisation :') + '</p>' +
            '<div class="cyfun-level-options">' +
              '<label class="cyfun-level-option">' +
                '<input type="radio" name="wizardNis2Level" value="basic" checked>' +
                '<div>' +
                  '<strong class="cyfun-level-option__title--basic">BASIC</strong> ' +
                  '<span class="cyfun-level-option__meta">(34 ' + _t('wizard.nis2Requirements', 'exigences') + ', 13 ' + _t('wizard.nis2KeyMeasures', 'mesures clés') + ')</span>' +
                  '<p class="cyfun-level-option__desc">' + _t('wizard.nis2LevelBasicDesc', 'Pour les organisations qui débutent leur démarche NIS2. Seuil de conformité : 2.5/5.') + '</p>' +
                '</div>' +
              '</label>' +
              '<label class="cyfun-level-option">' +
                '<input type="radio" name="wizardNis2Level" value="important">' +
                '<div>' +
                  '<strong class="cyfun-level-option__title--important">IMPORTANT</strong> ' +
                  '<span class="cyfun-level-option__meta">(133 ' + _t('wizard.nis2Requirements', 'exigences') + ', 22 ' + _t('wizard.nis2KeyMeasures', 'mesures clés') + ')</span>' +
                  '<p class="cyfun-level-option__desc">' + _t('wizard.nis2LevelImportantDesc', 'Pour les entités importantes au sens NIS2. Seuil de conformité : 3/5.') + '</p>' +
                '</div>' +
              '</label>' +
              '<label class="cyfun-level-option">' +
                '<input type="radio" name="wizardNis2Level" value="essential">' +
                '<div>' +
                  '<strong class="cyfun-level-option__title--essential">ESSENTIAL</strong> ' +
                  '<span class="cyfun-level-option__meta">(218 ' + _t('wizard.nis2Requirements', 'exigences') + ', 29 ' + _t('wizard.nis2KeyMeasures', 'mesures clés') + ')</span>' +
                  '<p class="cyfun-level-option__desc">' + _t('wizard.nis2LevelEssentialDesc', 'Pour les entités essentielles au sens NIS2. Seuil de conformité : 3.5/5 + minimum par catégorie.') + '</p>' +
                '</div>' +
              '</label>' +
            '</div>' +
            '<div class="cyfun-level-lang">' +
              '<label class="cyfun-level-lang__label">' + _t('wizard.nis2Lang', 'Langue des contrôles :') + '</label>' +
              '<div class="cyfun-level-lang__options">' +
                '<label class="cyfun-level-lang__option"><input type="radio" name="wizardNis2Lang" value="en" checked><span>English</span></label>' +
                '<label class="cyfun-level-lang__option"><input type="radio" name="wizardNis2Lang" value="fr"><span>Français</span></label>' +
                '<label class="cyfun-level-lang__option"><input type="radio" name="wizardNis2Lang" value="nl"><span>Nederlands</span></label>' +
              '</div>' +
              '<p class="cyfun-level-lang__hint">' + _t('wizard.nis2LangHint', 'La langue des titres et descriptions des exigences NIS2.') + '</p>' +
            '</div>' +
          '</div>';
        },
        onEnter: function(stepEl) {
          var curLevel = appData.cyfunLevel || 'basic';
          var radio = stepEl.querySelector('input[name="wizardNis2Level"][value="' + curLevel + '"]');
          if (radio) radio.checked = true;
          var curLang = appData.cyfunLang || 'en';
          var langRadio = stepEl.querySelector('input[name="wizardNis2Lang"][value="' + curLang + '"]');
          if (langRadio) langRadio.checked = true;
        },
        onLeave: function(stepEl) {
          var levelRadio = stepEl.querySelector('input[name="wizardNis2Level"]:checked');
          var level = levelRadio ? levelRadio.value : 'basic';
          var langRadio = stepEl.querySelector('input[name="wizardNis2Lang"]:checked');
          var lang = langRadio ? langRadio.value : 'en';
          appData.cyfunLang = lang;
          setCyfunAssuranceLevel(level);
          recalcNis2Maturity();
          appData._cyfunLevelChosen = true;
          var _s = document.getElementById('cyfunLevelSelect');
          if (_s) _s.value = level;
          if (typeof saveData === 'function') saveData();
          // FIX-GLOBAL-SYNC: Push cyfunLevel choice to server for other users
          // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
          // FIX-CONFIG-PERSIST-LOCK (revue prod 2026-06-11) : acquérir le verrou si besoin
          _persistConfigWithLock(['cyfunLevel', 'cyfunLang', '_cyfunLevelChosen', 'nis2Controls', '_cyfunEvalArchive', 'targetMaturity']);
        },
        canProceed: function() { return true; }
      });
    }

    // Step: Référentiel NIS2 (Belgique/CyFun ↔ France/ReCyF) + degré — toutes éditions
    if (isFirstWelcome) {
      steps.push({
        id: 'nis2-setup',
        title: _t('wizard.nis2SetupTitle', 'Référentiel NIS2'),
        subtitle: _t('wizard.nis2SetupSub', "Choisissez le référentiel et le niveau adaptés à votre organisation"),
        shouldShow: function() {
          var _fwE = (appData.frameworkMapper && appData.frameworkMapper.enabledFrameworks) || [];
          return (_fwE.indexOf('nis2') >= 0 || _fwE.indexOf('cyfun2025') >= 0 || _fwE.indexOf('recyf2025') >= 0);
        },
        content: function() {
          var fwR = (window._csiFrameworks || {})['recyf2025'];
          var _cntEI = fwR ? fwR.requirements.filter(function(r) { return r.level === 'importante'; }).length : 76;
          var _cntEE = fwR ? fwR.requirements.length : 152;
          return '<div class="wizard-nis2-level">' +
            '<p class="cyfun-level-option__desc">' + _t('wizard.nis2RefSelect', 'Quel référentiel NIS2 souhaitez-vous utiliser ?') + '</p>' +
            '<div class="cyfun-level-options">' +
              '<label class="cyfun-level-option"><input type="radio" name="wizardNis2Ref" value="cyfun2025" checked>' +
                '<div><strong>NIS2 Belgique — CyFun 2025 (CCB)</strong>' +
                  '<p class="cyfun-level-option__desc">' + _t('wizard.nis2RefBEDesc', 'Cadre belge (CCB) basé sur NIST CSF 2.0. Niveaux Basic / Important / Essential, évaluation par maturité.') + '</p></div></label>' +
              '<label class="cyfun-level-option"><input type="radio" name="wizardNis2Ref" value="recyf2025">' +
                '<div><strong>NIS2 France — ReCyF (ANSSI)</strong>' +
                  '<p class="cyfun-level-option__desc">' + _t('wizard.nis2RefFRDesc', "Référentiel Cyber France (ANSSI). 20 objectifs de sécurité, degrés Entité Importante / Essentielle, moyens acceptables de conformité.") + '</p></div></label>' +
            '</div>' +
            '<div id="wizCyfunBlock" style="margin-top:8px;">' +
              '<div class="cyfun-level-info"><i class="fas fa-info-circle cyfun-level-info__icon"></i> ' +
                '<strong>' + _t('wizard.nis2LevelAdviceLabel', 'Conseil :') + '</strong> ' +
                _t('wizard.nis2LevelAdvice', "Avant de choisir, nous vous recommandons d'utiliser l'outil d'auto-identification du") +
                ' <a href="https://atwork.safeonweb.be/fr/nis2" target="_blank" rel="noopener">CCB (Centre for Cybersecurity Belgium)</a> ' +
                _t('wizard.nis2LevelAdvice2', "pour déterminer votre niveau d'assurance NIS2.") + '</div>' +
              '<div class="cyfun-level-options">' +
                '<label class="cyfun-level-option"><input type="radio" name="wizardNis2Level" value="basic" checked><div><strong class="cyfun-level-option__title--basic">BASIC</strong> <span class="cyfun-level-option__meta">(34 ' + _t('wizard.nis2Requirements', 'exigences') + ')</span></div></label>' +
                '<label class="cyfun-level-option"><input type="radio" name="wizardNis2Level" value="important"><div><strong class="cyfun-level-option__title--important">IMPORTANT</strong> <span class="cyfun-level-option__meta">(133 ' + _t('wizard.nis2Requirements', 'exigences') + ')</span></div></label>' +
                '<label class="cyfun-level-option"><input type="radio" name="wizardNis2Level" value="essential"><div><strong class="cyfun-level-option__title--essential">ESSENTIAL</strong> <span class="cyfun-level-option__meta">(218 ' + _t('wizard.nis2Requirements', 'exigences') + ')</span></div></label>' +
              '</div>' +
              '<div class="cyfun-level-lang"><label class="cyfun-level-lang__label">' + _t('wizard.nis2Lang', 'Langue des contrôles :') + '</label>' +
                '<div class="cyfun-level-lang__options">' +
                  '<label class="cyfun-level-lang__option"><input type="radio" name="wizardNis2Lang" value="en" checked><span>English</span></label>' +
                  '<label class="cyfun-level-lang__option"><input type="radio" name="wizardNis2Lang" value="fr"><span>Français</span></label>' +
                  '<label class="cyfun-level-lang__option"><input type="radio" name="wizardNis2Lang" value="nl"><span>Nederlands</span></label>' +
                '</div></div>' +
            '</div>' +
            '<div id="wizRecyfBlock" style="margin-top:8px;display:none;">' +
              '<div class="cyfun-level-info"><i class="fas fa-info-circle cyfun-level-info__icon"></i> ' +
                '<strong>' + _t('wizard.nis2LevelAdviceLabel', 'Conseil :') + '</strong> ' +
                _t('wizard.recyfAdvice', "Le degré dépend de la qualification de votre entité au titre de NIS2 : Entité Importante (EI) ou Entité Essentielle (EE).") + '</div>' +
              '<div class="cyfun-level-options">' +
                '<label class="cyfun-level-option"><input type="radio" name="wizardRecyfDegree" value="importante" checked><div><strong>Entité Importante (EI)</strong> <span class="cyfun-level-option__meta">(' + _cntEI + ' ' + _t('common.measures', 'mesures') + ', ' + _t('wizard.recyfObj115', 'objectifs 1 à 15') + ')</span><p class="cyfun-level-option__desc">' + _t('wizard.recyfEIDesc', "Pour les entités importantes au sens NIS2.") + '</p></div></label>' +
                '<label class="cyfun-level-option"><input type="radio" name="wizardRecyfDegree" value="essentielle"><div><strong>Entité Essentielle (EE)</strong> <span class="cyfun-level-option__meta">(' + _cntEE + ' ' + _t('common.measures', 'mesures') + ', ' + _t('wizard.recyfObj120', 'objectifs 1 à 20') + ')</span><p class="cyfun-level-option__desc">' + _t('wizard.recyfEEDesc', "Pour les entités essentielles (dont approche par les risques, audit, supervision).") + '</p></div></label>' +
              '</div>' +
            '</div>' +
          '</div>';
        },
        onEnter: function(stepEl) {
          var beBlock = stepEl.querySelector('#wizCyfunBlock');
          var frBlock = stepEl.querySelector('#wizRecyfBlock');
          function syncBlocks() {
            var sel = stepEl.querySelector('input[name="wizardNis2Ref"]:checked');
            var v = sel ? sel.value : 'cyfun2025';
            if (beBlock) beBlock.style.display = (v === 'cyfun2025') ? '' : 'none';
            if (frBlock) frBlock.style.display = (v === 'recyf2025') ? '' : 'none';
          }
          var refRadios = stepEl.querySelectorAll('input[name="wizardNis2Ref"]');
          for (var i = 0; i < refRadios.length; i++) refRadios[i].addEventListener('change', syncBlocks);
          var curRef = appData.nis2Referential || 'cyfun2025';
          var rr = stepEl.querySelector('input[name="wizardNis2Ref"][value="' + curRef + '"]');
          if (rr) rr.checked = true;
          var lr = stepEl.querySelector('input[name="wizardNis2Level"][value="' + (appData.cyfunLevel || 'basic') + '"]');
          if (lr) lr.checked = true;
          var lgr = stepEl.querySelector('input[name="wizardNis2Lang"][value="' + (appData.cyfunLang || 'en') + '"]');
          if (lgr) lgr.checked = true;
          var dr = stepEl.querySelector('input[name="wizardRecyfDegree"][value="' + (appData.recyfLevel || 'importante') + '"]');
          if (dr) dr.checked = true;
          syncBlocks();
        },
        onLeave: function(stepEl) {
          var refSel = stepEl.querySelector('input[name="wizardNis2Ref"]:checked');
          var ref = refSel ? refSel.value : 'cyfun2025';
          appData.nis2Referential = ref;
          appData._nis2ReferentialLocked = true; // 1er choix confirmé -> référentiel figé (Reset pour changer)
          // Aligner immédiatement la liste des frameworks activés sur le référentiel choisi
          // (le référentiel actif doit être « activé », l'autre retiré) — modèle exclusif.
          if (typeof reconcileNis2Frameworks === 'function') reconcileNis2Frameworks();
          if (ref === 'recyf2025') {
            var degSel = stepEl.querySelector('input[name="wizardRecyfDegree"]:checked');
            if (typeof setRecyfDegree === 'function') setRecyfDegree(degSel ? degSel.value : 'importante');
            appData._cyfunLevelChosen = true; // évite la modale CyFun post-wizard
          } else {
            var levelRadio = stepEl.querySelector('input[name="wizardNis2Level"]:checked');
            var level = levelRadio ? levelRadio.value : 'basic';
            var langRadio = stepEl.querySelector('input[name="wizardNis2Lang"]:checked');
            appData.cyfunLang = langRadio ? langRadio.value : 'en';
            if (typeof setCyfunAssuranceLevel === 'function') setCyfunAssuranceLevel(level);
            if (typeof recalcNis2Maturity === 'function') recalcNis2Maturity();
            appData._cyfunLevelChosen = true;
            var _s = document.getElementById('cyfunLevelSelect');
            if (_s) _s.value = level;
          }
          if (typeof saveData === 'function') saveData();
          if (typeof applyNis2Referential === 'function') applyNis2Referential(false);
          // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
          // FIX-CONFIG-PERSIST-LOCK (revue prod 2026-06-11)
          _persistConfigWithLock(['nis2Referential', '_nis2ReferentialLocked', 'cyfunLevel', 'cyfunLang', '_cyfunLevelChosen', 'nis2Controls', '_cyfunEvalArchive', 'recyfControls', 'recyfLevel', 'targetMaturity', 'frameworkMapper']);
        },
        canProceed: function() { return true; }
      });
    }

    // Step: Welcome & Demo data
    if (isFirstWelcome) {
      steps.push({
        id: "welcome",
        title: _t('setup.welcomeTitle'),
        subtitle: _t('setup.welcomeSubtitle'),
        content: () => `
          <div class="wizard-welcome-content">
            <div class="wizard-features">
              <div class="wizard-feature">
                <div class="wizard-feature__icon"><i class="fas fa-shield-halved"></i></div>
                <div class="wizard-feature__text">
                  <strong>ISO 27001 & NIS2</strong>
                  <span id="setupFeatureCompliance">${_t('setup.featureCompliance')}</span>
                </div>
              </div>
              <div class="wizard-feature">
                <div class="wizard-feature__icon"><i class="fas fa-chart-pie"></i></div>
                <div class="wizard-feature__text">
                  <strong id="setupFeatureDashboards">${_t('setup.featureDashboards')}</strong>
                  <span id="setupFeatureDashboardsDesc">${_t('setup.featureDashboardsDesc')}</span>
                </div>
              </div>
              <div class="wizard-feature">
                <div class="wizard-feature__icon"><i class="fas fa-file-export"></i></div>
                <div class="wizard-feature__text">
                  <strong id="setupFeatureExports">${_t('setup.featureExports')}</strong>
                  <span id="setupFeatureExportsDesc">${_t('setup.featureExportsDesc')}</span>
                </div>
              </div>
            </div>
          </div>
        `,
        onEnter: () => {},
        onLeave: () => {
          try { localStorage.setItem(WELCOME_STORAGE_KEY, "1"); } catch (e) { void 0; }
        },
        canProceed: () => true
      });

      // Import step removed — data import is now available via Settings
    }

    // Step: Local password setup (LOCAL mode only)
    if (isFirstWelcome && window.location.protocol === 'file:') {
      steps.push({
        id: 'local-password',
        title: _t('wizard.localPwdTitle', 'Protection par mot de passe'),
        subtitle: _t('wizard.localPwdSub', 'Obligatoire pour protéger vos données'),
        content: function() {
          return '<div class="wizard-password-setup">' +
            '<p style="margin-bottom:16px;line-height:1.6;">' + _t('wizard.localPwdExplain', 'Définissez un mot de passe pour protéger l\'accès au mode édition. Ce mot de passe empêche les modifications accidentelles de vos données.') + '</p>' +
            '<div class="wizard-pwd-field"><label for="wizardLocalPwd">' + _t('wizard.localPwdLabel', 'Mot de passe') + '</label>' +
            '<input type="password" id="wizardLocalPwd" class="form-control" autocomplete="new-password" placeholder="' + _t('wizard.localPwdPlaceholder', 'Minimum 18 caractères') + '"></div>' +
            '<div class="wizard-pwd-field" style="margin-top:10px;"><label for="wizardLocalPwdConfirm">' + _t('wizard.localPwdConfirmLabel', 'Confirmer le mot de passe') + '</label>' +
            '<input type="password" id="wizardLocalPwdConfirm" class="form-control" autocomplete="new-password" placeholder="' + _t('wizard.localPwdConfirmPlaceholder', 'Répétez le mot de passe') + '"></div>' +
            '<div id="wizardLocalPwdPolicy" style="margin-top:12px;"></div>' +
            '<div id="wizardLocalPwdError" style="display:none;margin-top:8px;padding:8px 12px;border-radius:8px;background:var(--color-danger-bg,#fee2e2);color:var(--color-danger,#dc2626);font-size:0.85rem;"></div>' +
            '<p class="wizard-hint" style="margin-top:16px;"><i class="fas fa-shield-halved"></i> ' +
              _t('wizard.localPwdHint', 'Ce mot de passe est requis pour protéger l\'accès au mode édition de vos données.') +
            '</p>' +
          '</div>';
        },
        onEnter: function(stepEl) {
          var pwdInput = stepEl.querySelector('#wizardLocalPwd');
          var confirmInput = stepEl.querySelector('#wizardLocalPwdConfirm');
          var policyDiv = stepEl.querySelector('#wizardLocalPwdPolicy');
          var errorDiv = stepEl.querySelector('#wizardLocalPwdError');
          if (pwdInput && policyDiv && typeof createPasswordPolicyList === 'function') {
            var result = createPasswordPolicyList(MIN_EDIT_PASSWORD_LENGTH);
            policyDiv.appendChild(result.wrap);
            var updatePolicy = function() {
              updatePasswordPolicyList(result.list, pwdInput.value, MIN_EDIT_PASSWORD_LENGTH);
              errorDiv.style.display = 'none';
            };
            pwdInput.addEventListener('input', updatePolicy);
            updatePolicy();
          }
          if (confirmInput) {
            confirmInput.addEventListener('input', function() {
              errorDiv.style.display = 'none';
            });
          }
        },
        onLeave: function(stepEl) {
          var pwdInput = stepEl.querySelector('#wizardLocalPwd');
          var pwd = pwdInput ? (pwdInput.value || '').trim() : '';
          if (pwd) {
            var hash = hashEditPasswordSync(pwd);
            setStoredEditPasswordHash(hash);
            try { localStorage.setItem('smsi_password_setup_done', '1'); } catch(e) { void 0; }
          }
        },
        canProceed: function(stepEl) {
          var pwdInput = stepEl.querySelector('#wizardLocalPwd');
          var confirmInput = stepEl.querySelector('#wizardLocalPwdConfirm');
          var errorDiv = stepEl.querySelector('#wizardLocalPwdError');
          var pwd = pwdInput ? (pwdInput.value || '').trim() : '';
          var confirm = confirmInput ? (confirmInput.value || '').trim() : '';
          // Password is mandatory — cannot skip
          if (!pwd) {
            if (errorDiv) {
              errorDiv.textContent = _t('wizard.localPwdRequired', 'Un mot de passe est requis pour protéger vos données.');
              errorDiv.style.display = 'block';
            }
            return false;
          }
          // Validate password policy
          var policy = getEditPasswordPolicyStatus(pwd, MIN_EDIT_PASSWORD_LENGTH);
          if (!policy.isValid) {
            if (errorDiv) {
              errorDiv.textContent = _t('wizard.localPwdWeak', 'Le mot de passe ne respecte pas les critères requis.');
              errorDiv.style.display = 'block';
            }
            return false;
          }
          if (pwd !== confirm) {
            if (errorDiv) {
              errorDiv.textContent = _t('wizard.localPwdMismatch', 'Les mots de passe ne correspondent pas.');
              errorDiv.style.display = 'block';
            }
            return false;
          }
          return true;
        }
      });
    }

    // Step: Transversality presentation (both modes, on first welcome)
    if (isFirstWelcome) {
      steps.push({
        id: 'transversality',
        title: _t('wizard.transTitle', 'Transversalité des référentiels'),
        subtitle: _t('wizard.transSub', 'Un effort, de multiples conformités'),
        content: function() {
          return '<div class="wizard-transversality">' +
            '<div class="wizard-trans-diagram">' +
              '<svg viewBox="0 0 400 240" class="wizard-trans-svg" aria-hidden="true">' +
                // Central hub
                '<circle cx="200" cy="120" r="45" fill="var(--color-primary,#b07c7c)" opacity="0.15" />' +
                '<circle cx="200" cy="120" r="35" fill="var(--color-primary,#b07c7c)" opacity="0.25" />' +
                '<text x="200" y="115" text-anchor="middle" fill="var(--color-primary,#b07c7c)" font-size="11" font-weight="700">' + (function() { var _fw = _getActiveControlsFramework(); return _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) : 'ISO 27002'; })() + '</text>' +
                '<text x="200" y="130" text-anchor="middle" fill="var(--text-secondary,#666)" font-size="8">93 ' + _t('wizard.transMeasures', 'mesures') + '</text>' +
                // NIS2 spoke (top-right)
                '<line x1="235" y1="105" x2="310" y2="55" stroke="var(--color-primary,#b07c7c)" stroke-width="2" opacity="0.5" stroke-dasharray="4,3" />' +
                '<circle cx="320" cy="48" r="28" fill="var(--color-info,#3b82f6)" opacity="0.12" />' +
                '<text x="320" y="45" text-anchor="middle" fill="var(--color-info,#3b82f6)" font-size="10" font-weight="600">NIS2</text>' +
                '<text x="320" y="57" text-anchor="middle" fill="var(--text-secondary,#666)" font-size="7">Directive EU</text>' +
                // CyFun spoke (bottom-right)
                '<line x1="235" y1="135" x2="310" y2="185" stroke="var(--color-primary,#b07c7c)" stroke-width="2" opacity="0.5" stroke-dasharray="4,3" />' +
                '<circle cx="320" cy="192" r="28" fill="var(--color-warning,#f59e0b)" opacity="0.12" />' +
                '<text x="320" y="189" text-anchor="middle" fill="var(--color-warning,#f59e0b)" font-size="10" font-weight="600">CyFun</text>' +
                '<text x="320" y="201" text-anchor="middle" fill="var(--text-secondary,#666)" font-size="7">CCB 2025</text>' +
                // NIST spoke (left)
                '<line x1="165" y1="120" x2="90" y2="120" stroke="var(--color-primary,#b07c7c)" stroke-width="2" opacity="0.5" stroke-dasharray="4,3" />' +
                '<circle cx="70" cy="120" r="28" fill="var(--color-success,#22c55e)" opacity="0.12" />' +
                '<text x="70" y="117" text-anchor="middle" fill="var(--color-success,#22c55e)" font-size="10" font-weight="600">NIST CSF</text>' +
                '<text x="70" y="129" text-anchor="middle" fill="var(--text-secondary,#666)" font-size="7">2.0</text>' +
              '</svg>' +
            '</div>' +
            '<div class="wizard-trans-benefits">' +
              '<div class="wizard-trans-benefit">' +
                '<div class="wizard-trans-benefit__icon"><i class="fas fa-clock"></i></div>' +
                '<div class="wizard-trans-benefit__text">' +
                  '<strong>' + _t('wizard.transTimeSave', 'Gain de temps') + '</strong>' +
                  '<p>' + _t('wizard.transTimeSaveDesc', 'Documentez une mesure ISO 27002 : elle couvre automatiquement les exigences NIS2, CyFun et NIST CSF correspondantes.') + '</p>' +
                '</div>' +
              '</div>' +
              '<div class="wizard-trans-benefit">' +
                '<div class="wizard-trans-benefit__icon"><i class="fas fa-link"></i></div>' +
                '<div class="wizard-trans-benefit__text">' +
                  '<strong>' + _t('wizard.transConsistency', 'Cohérence garantie') + '</strong>' +
                  '<p>' + _t('wizard.transConsistencyDesc', 'Les mappings automatiques entre référentiels assurent une cohérence de bout en bout, sans ressaisie ni oubli.') + '</p>' +
                '</div>' +
              '</div>' +
              '<div class="wizard-trans-benefit">' +
                '<div class="wizard-trans-benefit__icon"><i class="fas fa-chart-line"></i></div>' +
                '<div class="wizard-trans-benefit__text">' +
                  '<strong>' + _t('wizard.transVisibility', 'Visibilité unifiée') + '</strong>' +
                  '<p>' + _t('wizard.transVisibilityDesc', 'Un tableau de bord unique reflète votre conformité sur l\'ensemble des référentiels activés.') + '</p>' +
                '</div>' +
              '</div>' +
            '</div>' +
          '</div>';
        },
        onEnter: function() {},
        onLeave: function() {},
        canProceed: function() { return true; }
      });
    }

    // === SERVER MODE SLIDES ===
    // Positioned after general slides so that CSI_LicenseManager.detect() (async)
    // has completed by the time the wizard reaches these steps.
    // shouldShow() provides lazy evaluation as a safety net.
    var _wizardServerStatus = null;

    // Step: Server pre-flight checks (HTTP(S) mode + server mode unlocked)
    if (isFirstWelcome && window.location.protocol !== 'file:') {
      steps.push({
        id: 'server-check',
        shouldShow: function() { return isServerModeUnlocked(); },
        title: _t('wizard.serverCheck', 'Vérification du serveur'),
        subtitle: _t('wizard.serverCheckSub', 'Vérification de la disponibilité des services'),
        content: function() {
          // Build check items via safe string (static content only, no user data)
          var checkItems = [
            { key: 'sync', label: _t('wizard.checkSync', 'Point d\'accès synchronisation') + ' (sync.php)' },
            { key: 'users', label: _t('wizard.checkUsers', 'Point d\'accès utilisateurs') + ' (users.php)' },
            { key: 'sysinfo', label: _t('wizard.checkSysInfo', 'Informations système') },
            { key: 'runtime', label: _t('wizard.checkRuntime', 'Environnement d\'exécution') },
            { key: 'license', label: _t('wizard.checkLicense', 'État de la licence') },
            { key: 'config', label: _t('wizard.checkServer', 'Configuration serveur') }
          ];
          var html = '<div class="wizard-check-list" id="wizardCheckList">';
          for (var ci = 0; ci < checkItems.length; ci++) {
            html += '<div class="wizard-check-item wizard-check-item--pending" data-check="' + checkItems[ci].key + '">' +
              '<span class="wizard-check-icon"><i class="fas fa-spinner fa-spin"></i></span>' +
              '<span class="wizard-check-label">' + escapeHtml(checkItems[ci].label) + '</span>' +
              '<span class="wizard-check-status"></span></div>';
          }
          html += '</div>' +
            '<div id="wizardCheckSummary" style="margin-top:16px;text-align:center;font-size:0.95rem;"></div>' +
            '<div style="text-align:center;margin-top:12px;"><button type="button" id="wizardRecheckBtn" class="btn btn--outline" style="display:none;"><i class="fas fa-redo"></i> ' + escapeHtml(_t('wizard.recheck', 'Re-vérifier')) + '</button></div>';
          return html;
        },
        onEnter: function(stepEl) {
          function runChecks() {
            var items = stepEl.querySelectorAll('.wizard-check-item');
            items.forEach(function(item) {
              item.className = 'wizard-check-item wizard-check-item--pending';
              var icon = item.querySelector('.wizard-check-icon');
              while (icon.firstChild) icon.removeChild(icon.firstChild);
              var spinner = document.createElement('i');
              spinner.className = 'fas fa-spinner fa-spin';
              icon.appendChild(spinner);
              item.querySelector('.wizard-check-status').textContent = '';
            });
            var summary = stepEl.querySelector('#wizardCheckSummary');
            summary.textContent = '';
            var recheckBtn = stepEl.querySelector('#wizardRecheckBtn');
            recheckBtn.style.display = 'none';
            var results = { sync: null, users: null, sysinfo: null, runtime: null, license: null, config: null };

            function updateItem(check, ok, detail) {
              var item = stepEl.querySelector('[data-check="' + check + '"]');
              if (!item) return;
              item.className = 'wizard-check-item ' + (ok ? 'wizard-check-item--ok' : 'wizard-check-item--error');
              var icon = item.querySelector('.wizard-check-icon');
              while (icon.firstChild) icon.removeChild(icon.firstChild);
              var ic = document.createElement('i');
              ic.className = ok ? 'fas fa-check-circle' : 'fas fa-times-circle';
              icon.appendChild(ic);
              item.querySelector('.wizard-check-status').textContent = detail || '';
              results[check] = ok;
              checkAllDone();
            }

            function checkAllDone() {
              if (Object.values(results).some(function(v) { return v === null; })) return;
              var allOk = Object.values(results).every(function(v) { return v; });
              summary.textContent = '';
              var statusIcon = document.createElement('i');
              var statusText = document.createElement('span');
              statusText.style.fontWeight = '500';
              if (allOk) {
                statusIcon.className = 'fas fa-check-circle';
                statusText.style.color = 'var(--color-success,#22c55e)';
                statusText.textContent = ' ' + _t('wizard.serverReady', 'Serveur prêt');
              } else {
                statusIcon.className = 'fas fa-exclamation-triangle';
                statusText.style.color = 'var(--color-danger,#dc3545)';
                statusText.textContent = ' ' + _t('wizard.serverError', 'Problème détecté');
                recheckBtn.style.display = '';
              }
              statusText.style.color = allOk ? 'var(--color-success,#22c55e)' : 'var(--color-danger,#dc3545)';
              summary.appendChild(statusIcon);
              summary.appendChild(statusText);
            }

            // Check sync.php
            // FIX-WIZARD-CONFIG-RACE (audit 2026-06-10) : conserver les promesses des deux
            // requêtes de statut pour attendre leur fin réelle (au lieu d'un délai fixe de
            // 500 ms qui marquait à tort « Serveur indisponible » sur un serveur lent).
            var _syncCheckP = fetchWithTimeout('./data/sync.php?action=status', { cache: 'no-store' })
              .then(function(r) { return r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)); })
              .then(function(d) {
                _wizardServerStatus = d;
                updateItem('sync', true, 'OK');
              })
              .catch(function() { updateItem('sync', false, _t('wizard.syncMissing', 'Non déployé')); });

            // Check users.php
            var _usersCheckP = fetchWithTimeout('./data/users.php?action=status', { cache: 'no-store' })
              .then(function(r) { return r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)); })
              .then(function(d) {
                _wizardServerStatus = _wizardServerStatus || {};
                _wizardServerStatus.configured = d.configured;
                _wizardServerStatus.userCount = d.userCount;
                _wizardServerStatus.maxUsers = d.maxUsers;
                updateItem('users', true, d.userCount + '/' + d.maxUsers + ' ' + _t('users.usersLabel', 'utilisateurs'));
              })
              .catch(function() { updateItem('users', false, _t('wizard.usersMissing', 'Non déployé')); });

            // Check system info (runtime, extensions, disk, permissions)
            fetchWithTimeout('./data/sync.php?action=system-info', { cache: 'no-store' })
              .then(function(r) { return r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)); })
              .then(function(d) {
                var issues = [];
                var runtime = 'PHP ' + (d.phpVersion || '');
                if (d.phpVersion && !d.phpVersionOk) issues.push('PHP ' + d.phpVersion + ' (min ' + (d.phpVersionMin || '7.4') + ')');
                if (d.requiredExtensions && !d.allExtensionsOk) {
                  var missing = Object.keys(d.requiredExtensions).filter(function(k) { return !d.requiredExtensions[k]; });
                  if (missing.length) issues.push('ext: ' + missing.join(', '));
                }
                if (d.writePermissions && !d.writePermissions.dataDir) issues.push(_t('wizard.noWriteAccess', 'Pas d\'accès écriture'));
                if (d.diskSpace && !d.diskSpace.sufficient) issues.push(_t('wizard.lowDiskSpace', 'Espace disque insuffisant'));
                if (issues.length) {
                  updateItem('sysinfo', false, issues.join(' | '));
                } else {
                  var detail = runtime;
                  if (d.diskSpace && d.diskSpace.free && d.diskSpace.free !== 'N/A') detail += ' · ' + d.diskSpace.free + ' ' + _t('wizard.freeSpace', 'libre');
                  updateItem('sysinfo', true, detail);
                }
                // Runtime detection — prominent separate item
                var runtimeLabel = _t('wizard.runtimePHP', 'Apache/PHP') + ' ' + (d.phpVersion || '');
                updateItem('runtime', true, _t('wizard.runtimeDetected', 'Détecté : {runtime}').replace('{runtime}', runtimeLabel));
              })
              .catch(function() {
                updateItem('sysinfo', false, _t('wizard.sysInfoUnavailable', 'Info système indisponible'));
                updateItem('runtime', false, _t('wizard.sysInfoUnavailable', 'Info système indisponible'));
              });

            // Check license/trial
            var _TM = window.CSI_TrialManager;
            if (_TM && _TM.isDetected()) {
              var state = _TM.getState();
              if (state && state.licensed) {
                updateItem('license', true, _t('license.active', 'Licence active'));
              } else if (state && state.trial && !state.trialExpired && state.trialDaysLeft > 0) {
                updateItem('license', true, _t('license.daysLeft', '{days} jours restants').replace('{days}', String(state.trialDaysLeft)));
              } else {
                updateItem('license', false, _t('license.trialExpired', 'Période d\'essai expirée'));
              }
            } else {
              updateItem('license', true, _t('license.active', 'Active'));
            }

            // Check server configured
            // FIX-WIZARD-CONFIG-RACE (audit 2026-06-10) : attendre la fin des deux requêtes
            // de statut (timeout 8 s assuré par fetchWithTimeout) avant d'évaluer la
            // configuration — les chaînes .catch ci-dessus résolvent toujours.
            Promise.all([_syncCheckP, _usersCheckP]).then(function() {
              if (_wizardServerStatus) {
                updateItem('config', true, _wizardServerStatus.configured ? _t('wizard.configured', 'Configuré') : _t('wizard.firstSetup', 'Premier démarrage'));
              } else {
                updateItem('config', false, _t('wizard.serverUnavailable', 'Serveur indisponible'));
              }
            });
          }

          runChecks();
          var recheckBtn = stepEl.querySelector('#wizardRecheckBtn');
          if (recheckBtn) {
            recheckBtn.addEventListener('click', function() { runChecks(); });
          }
        },
        canProceed: function() { return true; }
      });
    }

    // Step: Network security warning (server mode only)
    if (isFirstWelcome && window.location.protocol !== 'file:') {
      steps.push({
        id: 'network-security',
        shouldShow: function() { return isServerModeUnlocked(); },
        title: _t('wizard.networkSecTitle', 'Sécurité réseau'),
        subtitle: _t('wizard.networkSecSub', 'Information importante'),
        content: function() {
          return '<div style="padding:4px 0;">' +
            '<div style="background:linear-gradient(135deg,#f59e0b 0%,#d97706 100%);color:#fff;padding:16px 18px;border-radius:10px;margin-bottom:16px;display:flex;align-items:flex-start;gap:12px;">' +
              '<i class="fas fa-exclamation-triangle" style="font-size:1.4rem;margin-top:2px;flex-shrink:0;"></i>' +
              '<span style="font-size:0.95rem;line-height:1.5;">' + escapeHtml(_t('wizard.networkSecWarning', 'Cette application est conçue pour un usage en réseau local (LAN/intranet) uniquement.')) + '</span>' +
            '</div>' +
            '<p style="font-size:0.9rem;color:var(--color-text,#333);line-height:1.6;margin:0 0 16px;" id="wizardNetSecExplain">' + escapeHtml(_t('wizard.networkSecExplain', 'Cyber-Assistant intègre une authentification et des contrôles d\'accès, mais n\'est pas conçu pour être exposé sur Internet. Si l\'application est hébergée sur un réseau local, il est essentiel de restreindre les accès au niveau du réseau (pare-feu, VLAN, VPN) car elle ne dispose pas de protections suffisantes contre les attaques depuis Internet.')) + '</p>' +
            '<p style="font-weight:600;font-size:0.9rem;margin:0 0 8px;color:var(--color-text,#333);" id="wizardNetSecRecTitle">' + escapeHtml(_t('wizard.networkSecRecTitle', 'Précautions recommandées :')) + '</p>' +
            '<ul style="list-style:none;padding:0;margin:0 0 16px;">' +
              '<li style="padding:6px 0;font-size:0.88rem;color:var(--color-text,#333);display:flex;align-items:center;gap:8px;" id="wizardNetSecRec1"><i class="fas fa-shield-alt" style="color:#3b82f6;width:16px;text-align:center;"></i> ' + escapeHtml(_t('wizard.networkSecRec1', 'Déployer derrière un pare-feu d\'entreprise ou un VPN')) + '</li>' +
              '<li style="padding:6px 0;font-size:0.88rem;color:var(--color-text,#333);display:flex;align-items:center;gap:8px;" id="wizardNetSecRec2"><i class="fas fa-network-wired" style="color:#3b82f6;width:16px;text-align:center;"></i> ' + escapeHtml(_t('wizard.networkSecRec2', 'Restreindre l\'accès au réseau interne uniquement')) + '</li>' +
              '<li style="padding:6px 0;font-size:0.88rem;color:var(--color-text,#333);display:flex;align-items:center;gap:8px;" id="wizardNetSecRec3"><i class="fas fa-lock" style="color:#3b82f6;width:16px;text-align:center;"></i> ' + escapeHtml(_t('wizard.networkSecRec3', 'Utiliser HTTPS avec un certificat TLS valide')) + '</li>' +
              '<li style="padding:6px 0;font-size:0.88rem;color:var(--color-text,#333);display:flex;align-items:center;gap:8px;" id="wizardNetSecRec4"><i class="fas fa-globe" style="color:#dc3545;width:16px;text-align:center;"></i> ' + escapeHtml(_t('wizard.networkSecRec4', 'Ne jamais exposer l\'application sur Internet')) + '</li>' +
              '<li style="padding:6px 0;font-size:0.88rem;color:var(--color-text,#333);display:flex;align-items:center;gap:8px;" id="wizardNetSecRec5"><i class="fas fa-sync-alt" style="color:#3b82f6;width:16px;text-align:center;"></i> ' + escapeHtml(_t('wizard.networkSecRec5', 'Mettre à jour régulièrement l\'environnement serveur')) + '</li>' +
              '<li style="padding:6px 0;font-size:0.88rem;color:var(--color-text,#333);display:flex;align-items:center;gap:8px;" id="wizardNetSecRec6"><i class="fas fa-user-shield" style="color:#dc3545;width:16px;text-align:center;"></i> ' + escapeHtml(_t('wizard.networkSecRec6', 'Limiter l\'accès à l\'application aux seuls utilisateurs autorisés')) + '</li>' +
              '<li style="padding:6px 0;font-size:0.88rem;color:var(--color-text,#333);display:flex;align-items:center;gap:8px;" id="wizardNetSecRec7"><i class="fas fa-ban" style="color:#dc3545;width:16px;text-align:center;"></i> ' + escapeHtml(_t('wizard.networkSecRec7', 'Ne pas laisser l\'application en libre accès sur le réseau sans authentification')) + '</li>' +
            '</ul>' +
            '<label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:12px 14px;background:var(--color-surface-alt,#f8fafc);border:1px solid var(--color-border,#e2e8f0);border-radius:8px;">' +
              '<input type="checkbox" id="wizardNetSecAck" style="width:18px;height:18px;accent-color:#f59e0b;flex-shrink:0;">' +
              '<span style="font-size:0.9rem;color:var(--color-text,#333);" id="wizardNetSecAckLabel">' + escapeHtml(_t('wizard.networkSecAck', 'Je m\'engage à restreindre l\'accès à cette application aux seuls utilisateurs autorisés et à ne pas l\'exposer en libre accès sur le réseau')) + '</span>' +
            '</label>' +
          '</div>';
        },
        onEnter: function(stepEl) {
          var ackCheckbox = stepEl.querySelector('#wizardNetSecAck');
          if (ackCheckbox) {
            ackCheckbox.addEventListener('change', function() {
              var nextBtn = overlay ? overlay.querySelector('[data-wizard-action="next"]') : null;
              var finishBtn = overlay ? overlay.querySelector('[data-wizard-action="finish"]') : null;
              var btn = nextBtn || finishBtn;
              if (btn) btn.disabled = !ackCheckbox.checked;
            });
          }
        },
        canProceed: function() {
          var ack = document.querySelector('#wizardNetSecAck');
          return ack && ack.checked;
        }
      });
    }

    // Step: Admin account creation (server mode + not yet configured)
    if (isFirstWelcome && window.location.protocol !== 'file:') {
      steps.push({
        id: 'admin-setup',
        shouldShow: function() { return isServerModeUnlocked(); },
        title: _t('wizard.adminSetup', 'Compte administrateur'),
        subtitle: _t('auth.setupPrompt', 'Vous serez l\'administrateur.'),
        content: function() {
          if (_wizardServerStatus && _wizardServerStatus.configured) {
            return '<div style="text-align:center;padding:24px 0;">' +
              '<i class="fas fa-check-circle" style="font-size:2rem;color:var(--color-success,#22c55e);margin-bottom:12px;display:block;"></i>' +
              '<p style="font-size:1rem;color:var(--color-text,#333);">' + escapeHtml(_t('wizard.alreadyConfigured', 'Le serveur est déjà configuré.')) + '</p>' +
              '<p style="font-size:0.9rem;color:var(--color-text-secondary,#666);">' + escapeHtml(_t('wizard.loginHint', 'Connectez-vous après le wizard pour accéder au mode édition.')) + '</p>' +
              '</div>';
          }
          return '<div class="wizard-admin-form">' +
            '<label style="display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;">' + escapeHtml(_t('auth.yourName', 'Votre nom')) + '</label>' +
            '<input type="text" id="wizardAdminName" autocomplete="username" style="width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-size:0.95rem;box-sizing:border-box;">' +
            '<label style="display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;">' + escapeHtml(_t('auth.password', 'Mot de passe')) + '</label>' +
            '<input type="password" id="wizardAdminPwd" autocomplete="new-password" style="width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-size:0.95rem;box-sizing:border-box;">' +
            '<label style="display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;">' + escapeHtml(_t('auth.confirmPassword', 'Confirmer')) + '</label>' +
            '<input type="password" id="wizardAdminConfirm" autocomplete="new-password" style="width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-size:0.95rem;box-sizing:border-box;">' +
            '<p style="margin:0 0 8px;font-size:0.8rem;color:var(--color-text-secondary,#999);">' + escapeHtml(_t('auth.passwordPolicy', '18+ caractères, majuscule, minuscule, caractère spécial')) + '</p>' +
            '<div id="wizardAdminError" style="display:none;padding:8px 12px;background:var(--color-danger-light, #fee2e2);color:var(--color-danger, #b91c1c);border-radius:var(--border-radius-sm, 6px);margin-bottom:8px;font-size:0.85rem;"></div>' +
            '<div id="wizardAdminSuccess" style="display:none;padding:8px 12px;background:var(--color-success-bg,#f0fdf4);color:var(--color-success,#22c55e);border-radius:6px;margin-bottom:8px;font-size:0.85rem;"></div>' +
          '</div>';
        },
        onEnter: function(stepEl) {
          var nameInput = stepEl.querySelector('#wizardAdminName');
          if (nameInput) setTimeout(function() { nameInput.focus(); }, 100);
        },
        canProceed: function(stepEl) {
          if (_wizardServerStatus && _wizardServerStatus.configured) return true;
          var nameInput = stepEl.querySelector('#wizardAdminName');
          var pwdInput = stepEl.querySelector('#wizardAdminPwd');
          var confirmInput = stepEl.querySelector('#wizardAdminConfirm');
          var errorDiv = stepEl.querySelector('#wizardAdminError');
          if (!nameInput || !pwdInput) return false;

          var name = (nameInput.value || '').trim();
          var pwd = pwdInput.value || '';
          var confirm = confirmInput ? confirmInput.value || '' : '';

          if (!name) { if (errorDiv) { errorDiv.style.display = 'block'; errorDiv.textContent = _t('auth.nameRequired', 'Nom requis'); } return false; }

          var errors = [];
          if (pwd.length < 18) errors.push(_t('auth.chars18', '18+ characters'));
          if (!/[A-Z]/.test(pwd)) errors.push(_t('auth.uppercase', 'uppercase'));
          if (!/[a-z]/.test(pwd)) errors.push(_t('auth.lowercase', 'lowercase'));
          if (!/[^a-zA-Z0-9]/.test(pwd)) errors.push(_t('auth.specialChar', 'special character'));
          if (errors.length) { if (errorDiv) { errorDiv.style.display = 'block'; errorDiv.textContent = _t('auth.passwordInvalid', 'Mot de passe') + ': ' + errors.join(', '); } return false; }
          if (pwd !== confirm) { if (errorDiv) { errorDiv.style.display = 'block'; errorDiv.textContent = _t('auth.passwordMismatch', 'Les mots de passe ne correspondent pas'); } return false; }

          if (errorDiv) errorDiv.style.display = 'none';

          // Trigger async admin setup — block wizard until complete
          var successDiv = stepEl.querySelector('#wizardAdminSuccess');
          var AM = window.CSI_AuthManager;
          if (AM && typeof AM.setup === 'function' && !stepEl._setupStarted) {
            stepEl._setupStarted = true;
            if (errorDiv) errorDiv.style.display = 'none';
            nameInput.disabled = true;
            pwdInput.disabled = true;
            if (confirmInput) confirmInput.disabled = true;
            AM.setup(name, pwd).then(function(r) {
              if (r.ok) {
                _wizardServerStatus = _wizardServerStatus || {};
                _wizardServerStatus.configured = true;
                stepEl._setupDone = true;
                if (successDiv) { successDiv.style.display = 'block'; successDiv.textContent = _t('auth.accountCreated', 'Compte admin créé'); }
                // Auto-click next
                // FIX-WIZARD-AUTOADVANCE (audit 2026-06-10) : closest('[class*=wizard]')
                // retournait stepEl lui-même (classe wizard-step-content) → le bouton
                // « Suivant » du footer n'était jamais trouvé. Cibler l'overlay du wizard
                // et son bouton [data-wizard-action], en vérifiant que l'étape est toujours
                // affichée (l'utilisateur peut avoir cliqué Suivant manuellement entre-temps).
                var _wizOverlay = stepEl.closest('.wizard-overlay') || document.getElementById('setupWizardOverlay');
                if (_wizOverlay) {
                  setTimeout(function() {
                    if (!document.body.contains(stepEl)) return; // étape déjà quittée
                    var btn = _wizOverlay.querySelector('[data-wizard-action="next"], [data-wizard-action="finish"]');
                    if (btn) btn.click();
                  }, 500);
                }
              } else {
                stepEl._setupStarted = false;
                nameInput.disabled = false;
                pwdInput.disabled = false;
                if (confirmInput) confirmInput.disabled = false;
                if (errorDiv) { errorDiv.style.display = 'block'; errorDiv.textContent = (r.data && r.data.error) || _t('common.error', 'Erreur'); }
              }
            }).catch(function(err) {
              stepEl._setupStarted = false;
              nameInput.disabled = false;
              pwdInput.disabled = false;
              if (confirmInput) confirmInput.disabled = false;
              if (errorDiv) { errorDiv.style.display = 'block'; errorDiv.textContent = err.message || _t('common.error', 'Erreur'); }
            });
            return false; // Block until setup completes
          }
          return stepEl._setupDone === true;
        },
        onLeave: function() {}
      });
    }

    // Step: Server version promo (LOCAL mode only)
    if (isFirstWelcome && window.location.protocol === 'file:') {
      steps.push({
        id: 'server-promo',
        title: _t('wizard.serverPromoTitle', 'Version Serveur disponible'),
        subtitle: _t('wizard.serverPromoSub', 'Collaborez en équipe sur votre SMSI'),
        content: function() {
          return '<div class="wizard-promo">' +
            '<div class="wizard-promo-features">' +
              '<div class="wizard-promo-feature">' +
                '<div class="wizard-promo-feature__icon" style="background:var(--color-info-bg,#dbeafe);color:var(--color-info,#3b82f6);"><i class="fas fa-users"></i></div>' +
                '<div class="wizard-promo-feature__text">' +
                  '<strong>' + _t('wizard.serverPromoCoEdit', 'Co-édition en temps réel') + '</strong>' +
                  '<p>' + _t('wizard.serverPromoCoEditDesc', 'Travaillez à plusieurs simultanément sur le même SMSI avec un système de verrouillage intelligent.') + '</p>' +
                '</div>' +
              '</div>' +
              '<div class="wizard-promo-feature">' +
                '<div class="wizard-promo-feature__icon" style="background:var(--color-warning-bg,#fef3c7);color:var(--color-warning,#f59e0b);"><i class="fas fa-user-shield"></i></div>' +
                '<div class="wizard-promo-feature__text">' +
                  '<strong>' + _t('wizard.serverPromoUsers', 'Gestion des utilisateurs') + '</strong>' +
                  '<p>' + _t('wizard.serverPromoUsersDesc', 'Rôles administrateur et éditeur, jusqu\'à 5 utilisateurs avec authentification sécurisée.') + '</p>' +
                '</div>' +
              '</div>' +
              '<div class="wizard-promo-feature">' +
                '<div class="wizard-promo-feature__icon" style="background:var(--color-success-bg,#dcfce7);color:var(--color-success,#22c55e);"><i class="fas fa-cloud-arrow-up"></i></div>' +
                '<div class="wizard-promo-feature__text">' +
                  '<strong>' + _t('wizard.serverPromoSync', 'Synchronisation automatique') + '</strong>' +
                  '<p>' + _t('wizard.serverPromoSyncDesc', 'Données centralisées sur le serveur, accessibles depuis n\'importe quel poste connecté.') + '</p>' +
                '</div>' +
              '</div>' +
            '</div>' +
            '<div class="wizard-promo-cta">' +
              '<a href="https://cyber-assistant.com" target="_blank" rel="noopener" class="wizard-promo-link">' +
              '<i class="fas fa-globe"></i> ' + _t('wizard.serverPromoLink', 'En savoir plus sur cyber-assistant.com') +
              ' <i class="fas fa-external-link-alt" style="font-size:0.8em;opacity:0.7;"></i></a>' +
            '</div>' +
          '</div>';
        },
        onEnter: function() {},
        onLeave: function() {},
        canProceed: function() { return true; }
      });
    }

    // Step: Server capabilities (SERVER mode only)
    if (isFirstWelcome && window.location.protocol !== 'file:') {
      steps.push({
        id: 'server-capabilities',
        shouldShow: function() { return isServerModeUnlocked(); },
        title: _t('wizard.serverCapTitle', 'Fonctionnalités serveur'),
        subtitle: _t('wizard.serverCapSub', 'Cette version serveur vous permet\u2026'),
        content: function() {
          return '<div class="wizard-server-caps">' +
            '<div class="wizard-cap-grid">' +
              '<div class="wizard-cap-card">' +
                '<div class="wizard-cap-card__icon"><i class="fas fa-users-gear"></i></div>' +
                '<strong>' + _t('wizard.serverCapCoEdit', 'Co-édition') + '</strong>' +
                '<p>' + _t('wizard.serverCapCoEditDesc', 'Travaillez à plusieurs simultanément avec un verrouillage automatique par section.') + '</p>' +
              '</div>' +
              '<div class="wizard-cap-card">' +
                '<div class="wizard-cap-card__icon"><i class="fas fa-user-shield"></i></div>' +
                '<strong>' + _t('wizard.serverCapUsers', 'Gestion des utilisateurs') + '</strong>' +
                '<p>' + _t('wizard.serverCapUsersDesc', 'Rôles admin et éditeur, jusqu\'à 5 utilisateurs avec sessions sécurisées.') + '</p>' +
              '</div>' +
              '<div class="wizard-cap-card">' +
                '<div class="wizard-cap-card__icon"><i class="fas fa-arrows-rotate"></i></div>' +
                '<strong>' + _t('wizard.serverCapSync', 'Synchronisation') + '</strong>' +
                '<p>' + _t('wizard.serverCapSyncDesc', 'Données centralisées et synchronisées automatiquement entre tous les postes.') + '</p>' +
              '</div>' +
              '<div class="wizard-cap-card">' +
                '<div class="wizard-cap-card__icon"><i class="fas fa-lock"></i></div>' +
                '<strong>' + _t('wizard.serverCapLock', 'Verrouillage') + '</strong>' +
                '<p>' + _t('wizard.serverCapLockDesc', 'Système de heartbeat empêchant les conflits d\'édition entre collaborateurs.') + '</p>' +
              '</div>' +
            '</div>' +
          '</div>';
        },
        onEnter: function() {},
        onLeave: function() {},
        canProceed: function() { return true; }
      });
    }

    // Disclaimer slide (shown only in public/open edition — when only open-* frameworks are loaded)
    steps.push({
      id: 'framework-disclaimer',
      title: _t('wizard.disclaimerTitle', 'Référentiels de sécurité'),
      subtitle: _t('wizard.disclaimerSubtitle', 'Information importante'),
      shouldShow: function() {
        var fws = window._csiFrameworks || {};
        // Show only when NO official ISO frameworks are present (public edition)
        return !fws['iso27002'] && !fws['iso27001'];
      },
      content: function() {
        return '<div class="wizard-disclaimer" style="text-align:left;max-width:600px;margin:0 auto;">' +
          '<div style="background:var(--color-info-bg,#eff6ff);border:1px solid var(--color-info,#3b82f6);border-radius:8px;padding:16px;margin-bottom:16px;">' +
            '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">' +
              '<i class="fas fa-info-circle" style="color:var(--color-info,#3b82f6);font-size:18px;"></i>' +
              '<strong style="color:var(--color-info,#3b82f6);">' + _t('wizard.disclaimerInfoTitle', 'Référentiels ouverts') + '</strong>' +
            '</div>' +
            '<p style="margin:0;line-height:1.5;">' + _t('wizard.disclaimerInfoBody', 'Cette édition utilise les référentiels <strong>Open Security Controls</strong> (93 contrôles) et <strong>Open Security Management Framework</strong> (clauses 4 à 10), inspirés des bonnes pratiques internationales en matière de sécurité de l\'information.') + '</p>' +
          '</div>' +
          '<div style="background:var(--color-warning-bg,#fffbeb);border:1px solid var(--color-warning,#f59e0b);border-radius:8px;padding:16px;">' +
            '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">' +
              '<i class="fas fa-exclamation-triangle" style="color:var(--color-warning,#f59e0b);font-size:18px;"></i>' +
              '<strong style="color:var(--color-warning,#f59e0b);">' + _t('wizard.disclaimerWarningTitle', 'Avertissement') + '</strong>' +
            '</div>' +
            '<p style="margin:0;line-height:1.5;">' + _t('wizard.disclaimerWarningBody', 'Ces référentiels ne sont <strong>pas des normes ISO officielles</strong>. Ils ne se substituent pas aux normes ISO/IEC 27001:2022 et ISO/IEC 27002:2022. Pour une certification officielle, vous devez vous procurer les normes originales auprès de l\'ISO ou de votre organisme national de normalisation.') + '</p>' +
          '</div>' +
        '</div>';
      },
      onEnter: function() {},
      onLeave: function() {},
      canProceed: function() { return true; }
    });

    // === CSI-ME === Multi-entity feature slide — always shown (all editions ; en local
    // il est informatif/upsell, comme server-promo). Poussé SANS shouldShow (même
    // convention que corporate-upsell) ; le contenu s'adapte DANS content(), réévalué à
    // chaque rendu — donc après la détection de licence asynchrone.
    steps.push({
      id: 'multi-entity',
      title: _t('wizard.multiEntTitle', 'Multi-entités'),
      subtitle: _t('wizard.multiEntSub', 'Un SMSI, plusieurs périmètres cloisonnés'),
      content: function() {
        var _meServer = (typeof isServerModeUnlocked === 'function') && isServerModeUnlocked();
        var html = '<div class="wizard-server-caps">' +
          '<div class="wizard-cap-grid">' +
            '<div class="wizard-cap-card">' +
              '<div class="wizard-cap-card__icon"><i class="fas fa-sitemap"></i></div>' +
              '<strong>' + _t('wizard.multiEntIsolation', 'Cloisonnement') + '</strong>' +
              '<p>' + _t('wizard.multiEntIsolationDesc', 'Plusieurs entités ou sites dans une seule installation — les données de chaque périmètre (risques, actions, contrôles…) sont strictement séparées.') + '</p>' +
            '</div>' +
            '<div class="wizard-cap-card">' +
              '<div class="wizard-cap-card__icon"><i class="fas fa-user-tag"></i></div>' +
              '<strong>' + _t('wizard.multiEntAccounts', 'Comptes rattachés') + '</strong>' +
              '<p>' + _t('wizard.multiEntAccountsDesc', 'Chaque compte rattaché ne voit que son périmètre, avec des droits par section (lecture / écriture). Multi-appartenance possible.') + '</p>' +
            '</div>' +
            '<div class="wizard-cap-card">' +
              '<div class="wizard-cap-card__icon"><i class="fas fa-layer-group"></i></div>' +
              '<strong>' + _t('wizard.multiEntGroup', 'Vue groupe consolidée') + '</strong>' +
              '<p>' + _t('wizard.multiEntGroupDesc', 'Tuile de pilotage par entité sur le tableau de bord et console « Toutes les entités » en lecture consolidée.') + '</p>' +
            '</div>' +
            '<div class="wizard-cap-card">' +
              '<div class="wizard-cap-card__icon"><i class="fas fa-right-left"></i></div>' +
              '<strong>' + _t('wizard.multiEntTransfer', 'Répartition maîtrisée') + '</strong>' +
              '<p>' + _t('wizard.multiEntTransferDesc', 'Assistant de déplacement entre périmètres, exports nommés par entité et garde-fou anti-import croisé.') + '</p>' +
            '</div>' +
          '</div>';
        if (_meServer) {
          html += '<p class="wizard-hint" style="margin-top:12px;text-align:center;">' +
            '<i class="fas fa-toggle-on" style="margin-right:6px;"></i>' +
            _t('wizard.multiEntAvailServer', 'Fonction activable dans Réglages > Multi-entités (option de licence multi-entités).') + '</p>';
        } else {
          html += '<p class="wizard-hint" style="margin-top:12px;text-align:center;">' +
            _t('wizard.multiEntAvailLocal', 'Disponible en mode serveur (option de licence).') + ' ' +
            '<a href="https://cyber-assistant.com" target="_blank" rel="noopener" class="wizard-promo-link">' +
            '<i class="fas fa-globe"></i> ' + _t('wizard.serverPromoLink', 'En savoir plus sur cyber-assistant.com') +
            ' <i class="fas fa-external-link-alt" style="font-size:0.8em;opacity:0.7;"></i></a></p>';
        }
        return html + '</div>';
      },
      onEnter: function() {},
      onLeave: function() {},
      canProceed: function() { return true; }
    });

    // Corporate upsell slide — always shown (all editions, including demo)
    steps.push({
      id: 'corporate-upsell',
      title: _t('wizard.corporateUpsellSlideTitle', 'Édition Corporate'),
      subtitle: _t('wizard.corporateUpsellSlideSubtitle', 'Allez plus loin'),
      content: function() {
        return '<div class="wizard-corporate-upsell-slide" style="text-align:left;max-width:600px;margin:0 auto;">' +
          '<div style="background:linear-gradient(135deg,var(--color-info-bg,#eff6ff),#f0f4ff);border:1px solid var(--color-info,#3b82f6);border-radius:10px;padding:24px;margin-bottom:16px;">' +
            '<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">' +
              '<i class="fas fa-building" style="color:var(--color-info,#3b82f6);font-size:22px;"></i>' +
              '<strong style="color:var(--color-info,#3b82f6);font-size:1.1em;">' + _t('wizard.corporateUpsellTitle', 'Envie d\'aller plus loin ?') + '</strong>' +
            '</div>' +
            '<p style="margin:0 0 14px 0;line-height:1.6;font-size:0.97em;">' +
              _t('wizard.corporateUpsellBody', 'La <strong>version Corporate</strong> intègre les normes <strong>ISO/IEC 27001:2022</strong> et <strong>ISO/IEC 27002:2022</strong> officielles — idéale si vous visez une certification ou un audit formel.') +
            '</p>' +
            '<ul style="margin:0 0 14px 0;padding-left:20px;line-height:1.8;font-size:0.95em;">' +
              '<li><i class="fas fa-users" style="color:var(--color-info,#3b82f6);margin-right:6px;"></i>' + _t('wizard.corporateFeatureCollab', 'Co-édition multi-collaborateurs en temps réel') + '</li>' +
              '<li><i class="fas fa-server" style="color:var(--color-info,#3b82f6);margin-right:6px;"></i>' + _t('wizard.corporateFeatureServer', 'Mode serveur centralisé avec synchronisation') + '</li>' +
              '<li><i class="fas fa-certificate" style="color:var(--color-info,#3b82f6);margin-right:6px;"></i>' + _t('wizard.corporateFeatureISO', 'Normes ISO/IEC 27001 & 27002 officielles intégrées') + '</li>' +
              '<li><i class="fas fa-language" style="color:var(--color-info,#3b82f6);margin-right:6px;"></i>' + _t('wizard.corporateFeatureLang', 'Anglais natif avancé (traductions professionnelles)') + '</li>' +
            '</ul>' +
            '<div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:12px 16px;display:flex;align-items:center;gap:10px;">' +
              '<i class="fas fa-envelope" style="color:var(--color-info,#3b82f6);font-size:16px;"></i>' +
              '<span style="font-size:0.93em;color:var(--color-text-secondary,#555);">' +
                _t('wizard.corporateUpsellContact', 'Contactez-nous pour en savoir plus\u00a0: <a href="mailto:info@cyber-assistant.com" style="color:var(--color-info,#3b82f6);text-decoration:underline;">info@cyber-assistant.com</a>') +
              '</span>' +
            '</div>' +
          '</div>' +
        '</div>';
      },
      onEnter: function() {},
      onLeave: function() {},
      canProceed: function() { return true; }
    });

    // If no steps, just complete
    if (steps.length === 0) {
      if (onComplete) onComplete();
      return;
    }

    // Create wizard overlay
    const overlay = document.createElement("div");
    overlay.id = "setupWizardOverlay";
    overlay.className = "wizard-overlay";

    // Helper: get visible steps (filter out conditional steps that shouldShow() === false)
    const getVisibleSteps = () => steps.filter(s => !s.shouldShow || s.shouldShow());
    const getVisibleIndex = () => {
      var visible = getVisibleSteps();
      for (var i = 0; i < visible.length; i++) {
        if (visible[i] === steps[currentStep]) return i;
      }
      return 0;
    };

    const renderStep = () => {
      // Skip current step if it has shouldShow() returning false
      while (currentStep < steps.length && steps[currentStep].shouldShow && !steps[currentStep].shouldShow()) {
        currentStep++;
      }
      if (currentStep >= steps.length) { closeWizard(); return; }

      const step = steps[currentStep];
      // First slide (language) is always rendered in English
      // FIX-WIZARD-LANG-FLAG (audit 2026-06-10) : bascule TEMPORAIRE ({persist:false}) —
      // l'ancien setLanguage('en') persistant consommait le flag de premier lancement
      // (isFirstLaunch) dès le rendu : un F5 pendant le wizard figeait silencieusement la
      // langue sans choix utilisateur. Le flag n'est désormais consommé qu'à la validation
      // de l'étape (onLeave → setLanguage + markLanguageChosen).
      var _langBefore = null;
      if (step.id === 'language' && window.CSI_i18n) {
        _langBefore = window.CSI_i18n.getLanguage();
        window.CSI_i18n.setLanguage('en', { persist: false });
      }
      const visibleSteps = getVisibleSteps();
      const visibleIdx = getVisibleIndex();
      const isFirst = visibleIdx === 0;
      const isLast = visibleIdx === visibleSteps.length - 1;
      const progress = ((visibleIdx + 1) / visibleSteps.length) * 100;

      overlay.innerHTML = `
        <div class="wizard-modal" role="dialog" aria-modal="true" aria-labelledby="wizardTitle">
          <div class="wizard-modal__header">
            <div class="wizard-progress">
              <div class="wizard-progress__bar" style="width: ${progress}%"></div>
            </div>
            <div class="wizard-steps-indicator">
              ${visibleSteps.map((s, i) => `
                <span class="wizard-step-dot ${i < visibleIdx ? 'wizard-step-dot--done' : ''} ${i === visibleIdx ? 'wizard-step-dot--active' : ''}" data-step="${i}">
                  ${i < visibleIdx ? '<i class="fas fa-check"></i>' : i + 1}
                </span>
              `).join("")}
            </div>
          </div>

          <div class="wizard-modal__body">
            <div class="wizard-modal__image">
              <img src="${WIZARD_IMAGE_URL}" alt="Cyber-Assistant" class="wizard-hero-image" />
              <div class="wizard-modal__image-overlay">
                <i class="fas fa-shield-alt"></i>
                <span>Cyber-Assistant</span>
              </div>
            </div>

            <div class="wizard-modal__content">
              <div class="wizard-content-inner">
                <h2 class="wizard-title" id="wizardTitle">${step.title}</h2>
                <p class="wizard-subtitle">${step.subtitle}</p>
                <div class="wizard-step-content" data-step-id="${step.id}">
                  ${step.content()}
                </div>
              </div>

              <div class="wizard-modal__footer">
                ${!isFirst ? `
                  <button class="btn btn--outline" type="button" data-wizard-action="prev">
                    <i class="fas fa-arrow-left"></i> ${_t('setup.btnPrevious')}
                  </button>
                ` : '<div></div>'}
                <button class="btn btn--primary" type="button" data-wizard-action="${isLast ? 'finish' : 'next'}">
                  ${isLast ? '<i class="fas fa-rocket"></i> ' + _t('setup.btnStart') : _t('setup.btnNext') + ' <i class="fas fa-arrow-right"></i>'}
                </button>
              </div>
            </div>
          </div>
        </div>
      `;

      // Restore language after rendering language step in English
      // FIX-WIZARD-LANG-FLAG : restauration également non persistante
      if (_langBefore !== null && window.CSI_i18n) {
        window.CSI_i18n.setLanguage(_langBefore, { persist: false });
      }

      // Bind step enter
      const stepEl = overlay.querySelector(`[data-step-id="${step.id}"]`);
      if (step.onEnter) step.onEnter(stepEl);

      // Bind navigation
      const prevBtn = overlay.querySelector('[data-wizard-action="prev"]');
      const nextBtn = overlay.querySelector('[data-wizard-action="next"]');
      const finishBtn = overlay.querySelector('[data-wizard-action="finish"]');

      if (prevBtn) {
        prevBtn.addEventListener("click", () => {
          if (currentStep > 0) {
            currentStep--;
            // Skip conditional steps going backward
            while (currentStep > 0 && steps[currentStep].shouldShow && !steps[currentStep].shouldShow()) {
              currentStep--;
            }
            renderStep();
          }
        });
      }

      if (nextBtn) {
        nextBtn.addEventListener("click", () => {
          const step = steps[currentStep];
          const stepEl = overlay.querySelector(`[data-step-id="${step.id}"]`);
          if (step.canProceed(stepEl)) {
            if (step.onLeave) step.onLeave(stepEl);
            currentStep++;
            renderStep();
          }
        });
      }

      if (finishBtn) {
        finishBtn.addEventListener("click", () => {
          const step = steps[currentStep];
          const stepEl = overlay.querySelector(`[data-step-id="${step.id}"]`);
          if (step.canProceed && !step.canProceed(stepEl)) return;
          if (step.onLeave) step.onLeave(stepEl);
          closeWizard();
        });
      }
    };

    const updateWizardTexts = () => {
      // Re-translate all wizard step titles after language change
      // Note: step 'language' keeps hardcoded English (always shown in English)
      steps.forEach(function(s) {
        if (s.id === 'license') { s.title = _t('license.title', 'Activation'); s.subtitle = needsLicense ? _t('license.trialExpired', 'Trial expired') : _t('license.trialActive', 'Trial active'); }
        if (s.id === 'server-check') { s.title = _t('wizard.serverCheck', 'Server check'); s.subtitle = _t('wizard.serverCheckSub', 'Checking service availability'); }
        if (s.id === 'network-security') { s.title = _t('wizard.networkSecTitle', 'Network Security'); s.subtitle = _t('wizard.networkSecSub', 'Important information'); }
        if (s.id === 'admin-setup') { s.title = _t('wizard.adminSetup', 'Administrator account'); s.subtitle = _t('auth.setupPrompt', 'You will be the administrator.'); }
        if (s.id === 'frameworks') { s.title = _t('setup.frameworksTitle'); s.subtitle = _t('setup.frameworksSubtitle'); }
        if (s.id === 'nis2-level') { s.title = _t('wizard.nis2LevelTitle', 'NIS2 Assurance Level'); s.subtitle = _t('wizard.nis2LevelSub', 'Set the CyFun level for your organisation'); }
        if (s.id === 'welcome') { s.title = _t('setup.welcomeTitle'); s.subtitle = _t('setup.welcomeSubtitle'); }
        if (s.id === 'local-password') { s.title = _t('wizard.localPwdTitle', 'Password protection'); s.subtitle = _t('wizard.localPwdSub', 'Required to protect your data'); }
        if (s.id === 'transversality') { s.title = _t('wizard.transTitle', 'Cross-framework mapping'); s.subtitle = _t('wizard.transSub', 'One effort, multiple compliance'); }
        if (s.id === 'server-promo') { s.title = _t('wizard.serverPromoTitle', 'Server version available'); s.subtitle = _t('wizard.serverPromoSub', 'Collaborate as a team on your ISMS'); }
        if (s.id === 'server-capabilities') { s.title = _t('wizard.serverCapTitle', 'Server features'); s.subtitle = _t('wizard.serverCapSub', 'This server version enables\u2026'); }
        if (s.id === 'framework-disclaimer') { s.title = _t('wizard.disclaimerTitle', 'Référentiels de sécurité'); s.subtitle = _t('wizard.disclaimerSubtitle', 'Information importante'); }
        if (s.id === 'multi-entity') { s.title = _t('wizard.multiEntTitle', 'Multi-entités'); s.subtitle = _t('wizard.multiEntSub', 'Un SMSI, plusieurs périmètres cloisonnés'); }
        if (s.id === 'corporate-upsell') { s.title = _t('wizard.corporateUpsellSlideTitle', 'Édition Corporate'); s.subtitle = _t('wizard.corporateUpsellSlideSubtitle', 'Allez plus loin'); }
      });
      // Re-render current step to apply translations
      renderStep();
    };

    const closeWizard = () => {
      overlay.classList.remove("is-visible");
      setTimeout(() => {
        overlay.remove();
        if (window.CSI_FrameworkMapper) CSI_FrameworkMapper.applyFrameworkVisibility();
        queueEditPasswordSetup();
        // After wizard closes, check if CyFun level modal should be shown
        // (frameworks may have just been enabled in the wizard)
        var _fwE = (appData.frameworkMapper && appData.frameworkMapper.enabledFrameworks) || [];
        // m-RC4 (audit fond 2026-07-11) : inclure recyf2025 — sinon un utilisateur ReCyF n'ayant
        // pas fait le wizard ne voyait jamais la modale de choix de degré (EI/EE).
        var _nis2On = _fwE.indexOf('nis2') >= 0 || _fwE.indexOf('cyfun2025') >= 0 || _fwE.indexOf('recyf2025') >= 0;
        if (_nis2On && !appData._cyfunLevelChosen) {
          setTimeout(function() {
            var modal = document.getElementById("cyfunLevelModal");
            if (modal) {
              var curLang = appData.cyfunLang || 'en';
              var langRadio = modal.querySelector('input[name="cyfunLangChoice"][value="' + curLang + '"]');
              if (langRadio) langRadio.checked = true;
              if (window.prepareNis2LevelModal) window.prepareNis2LevelModal();
              modal.classList.add("modal-overlay--visible");
            }
          }, 500);
        }
        if (onComplete) onComplete();
      }, 300);
    };

    document.body.appendChild(overlay);
    renderStep();
    requestAnimationFrame(() => overlay.classList.add("is-visible"));
  }

  // Keep old functions for backwards compatibility but redirect to wizard
  function showLanguageModal(onComplete) {
    showSetupWizard(onComplete);
  }

  function updateKPIs() {
    var FM = window.CSI_FrameworkMapper;
    var isoStats = FM ? FM.getConformityStats(_getActiveControlsFrameworkId() || 'iso27002', { soaFilter: true }) : null;
    var applicableCount, implementedControls, conformityRate;
    if (isoStats) {
      applicableCount = isoStats.applicable;
      implementedControls = isoStats.implemented;
      conformityRate = isoStats.rate;
    } else {
      // Fallback: manual computation if FrameworkMapper not available
      const soaEntries = Array.isArray(appData.soa) ? appData.soa : [];
      const soaMap = new Map(soaEntries.map((entry) => [entry.controlId, entry]));
      const applicableControls = (appData.controls || []).filter((c) => {
        const entry = soaMap.get(c.id);
        if (!entry) return true;
        return entry.applicable !== false;
      });
      applicableCount = applicableControls.length || 0;
      implementedControls = applicableControls.filter((c) => ["implemente", "verifie"].includes(c.status)).length;
      conformityRate = applicableCount > 0 ? Math.round(implementedControls / applicableCount * 100) : 0;
    }
    // NOTE: control.status is the ISO 27002 implementation status
    // soa[].implStatus is the SOA-specific implementation status (may differ intentionally)
    const activeActions = (appData.actions || []).filter((a) => !["termine", "annule"].includes(a.status)).length;
    const criticalRisks = (appData.risks || []).filter((r) => r.level === "Critique").length;
    const openNCs = (Array.isArray(appData.nonconformities) ? appData.nonconformities : []).filter(function(nc) { return nc.status !== 'cloture' && nc.status !== 'closed' && nc.status !== 'fermee'; }).length;

    // KPI principaux
    safeSetText("conformityRate", conformityRate + "%");
    safeSetText("implementedControls", implementedControls + "/" + applicableCount);
    safeSetText("totalActions", activeActions);
    safeSetText("criticalRisks", criticalRisks);
    safeSetText("openNonConformities", openNCs);

    // A11y: update KPI card aria-labels with current values
    var _kpiLabels = [
      { sel: '[data-nav-target="controls"].kpi-card--primary', label: t('dashboard.conformityRate', 'Conformité globale'), value: conformityRate + '%' },
      { sel: '[data-nav-target="controls"].kpi-card--success', label: t('dashboard.implementedControls', 'Contrôles implémentés'), value: implementedControls + '/' + applicableCount },
      { sel: '[data-nav-target="actions"]', label: t('dashboard.totalActions', 'Actions en cours'), value: activeActions },
      { sel: '[data-nav-target="risks"]', label: t('dashboard.criticalRisks', 'Risques critiques'), value: criticalRisks },
      { sel: '[data-nav-target="nonconformities"]', label: t('dashboard.openNonConformities', 'Non-conformités'), value: openNCs }
    ];
    _kpiLabels.forEach(function(k) {
      var el = document.querySelector('.kpi-card' + k.sel);
      if (el) el.setAttribute('aria-label', k.label + ': ' + k.value);
    });

    // KPI secondaires
    const projectRisks = appData.projectRisks || [];
    const projectRisksTotal = projectRisks.length;
    const completedStatuses = ['green', 'yellow'];
    const projectRisksCompleted = projectRisks.filter(r => completedStatuses.includes(r.status)).length;
    const projectRisksAtRisk = projectRisks.filter(r => r.status === 'red').length;
    const projectRisksInProgress = projectRisks.filter(r => r.status === 'todo').length;
    safeSetText("projectRisksCount", projectRisksTotal);
    const detailEl = document.getElementById("projectRisksDetail");
    if (detailEl && projectRisksTotal > 0) {
      detailEl.textContent = `${projectRisksCompleted} ${t('labels.completed', 'terminées')}` + (projectRisksAtRisk > 0 ? ` · ${projectRisksAtRisk} ${t('labels.atRisk', 'à risque')}` : '') + ` · ${projectRisksInProgress} ${t('labels.todo', 'à faire')}`;
    }

    const criticalAssets = appData.criticalAssets || [];
    safeSetText("criticalAssetsCount", criticalAssets.length);

    const threats = appData.threats || [];
    safeSetText("threatsCount", threats.length);

    const audits = appData.audits || [];
    safeSetText("auditsCount", audits.length);

    const documents = appData.documents || [];
    safeSetText("documentsCount", documents.length);

    const riskAnalysesList = appData.riskAnalyses || [];
    safeSetText("riskAnalysesCount", riskAnalysesList.length);

    const managementReviewsList = appData.reviews || [];
    safeSetText("managementReviewsCount", managementReviewsList.length);

    // SoA coverage is managed by updateSoaCoverageStats() in loadSoaTable()
    // Removed: safeSetText("soaCoverage") was destroying sub-elements of the widget

    // Overdue audit detection
    var overdueAudits = (appData.audits || []).filter(function(a) {
      return a.plannedDate && a.status !== 'realise' && a.status !== 'annule' && new Date(a.plannedDate) < new Date();
    });
    var overdueEl = document.getElementById('overdueAuditsBadge');
    // FIX-OVERDUE-BADGE (audit 2026-06-10) : #overdueAuditsBadge n'existe dans aucun HTML —
    // créer le badge dynamiquement sur l'onglet Audits, même motif que les badges de
    // présence (.lock-badge greffés sur les boutons de navigation par lock-manager.js).
    if (!overdueEl) {
      var _auditsNavBtn = document.querySelector('.nav__item[data-tab="audits"]');
      if (_auditsNavBtn) {
        _auditsNavBtn.style.position = 'relative';
        overdueEl = document.createElement('span');
        overdueEl.id = 'overdueAuditsBadge';
        overdueEl.style.cssText = 'position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;padding:0 4px;border-radius:9px;font-size:var(--font-size-xs,0.7rem);font-weight:700;line-height:18px;text-align:center;color:#fff;background:var(--color-danger,#dc3545);pointer-events:none;box-sizing:border-box;display:none;';
        _auditsNavBtn.appendChild(overdueEl);
      }
    }
    if (overdueEl) {
      overdueEl.textContent = overdueAudits.length;
      overdueEl.style.display = overdueAudits.length > 0 ? '' : 'none';
    }

    // Mettre à jour le Score Global et les alertes
    updateScoreGlobal(conformityRate);
    updateDashboardAlerts();
    updateSparklines();
  }

  // Score Global SMSI
  function updateScoreGlobal(isoConformity) {
    // Calcul des scores composants — controls accessed via FrameworkMapper when available
    var FM = window.CSI_FrameworkMapper;
    var isoControls = FM ? FM.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    const soaEntries = Array.isArray(appData.soa) ? appData.soa : [];
    const soaMap = new Map(soaEntries.map((entry) => [entry.controlId, entry]));
    const applicableControls = isoControls.filter((c) => {
      const entry = soaMap.get(c.id);
      if (!entry) return true;
      return entry.applicable !== false;
    });

    // Score ISO 27001
    const isoScore = isoConformity;
    safeSetText("scoreIso27001", isoScore + "%");

    // Score NIS2 (moyenne des maturités NIS2) — only if enabled
    var nis2Enabled = window.CSI_FrameworkMapper && CSI_FrameworkMapper.isEnabled('nis2');
    var nis2Score = 0;
    if (nis2Enabled) {
      if ((appData.nis2Referential || 'cyfun2025') === 'recyf2025' && typeof computeRecyfConformity === 'function') {
        // Référentiel France actif → score = taux de conformité ReCyF.
        // M3 (audit 2026-07-10) : le KPI global du dashboard doit être « coverage-aware »
        // (les mesures applicables non évaluées comptent pour 0), sinon une seule mesure
        // conforme affichait 100 %. On utilise percentApplicable (dénominateur = applicables),
        // distinct du percent « mesures évaluées » affiché — et labellisé — dans le module ReCyF.
        nis2Score = computeRecyfConformity().percentApplicable;
        safeSetText("scoreNis2", nis2Score + "%");
      } else {
        const nis2Mat = appData.targetMaturity?.nis2 || {};
        // M3 : inclure les catégories non évaluées (valeur 0) au dénominateur — convention
        // CCB (non évalué = 0). Le filtre `v > 0` faisait afficher 100 % dès une seule
        // catégorie évaluée à 5/5.
        const nis2Values = Object.values(nis2Mat).filter(v => typeof v === 'number');
        const nis2Avg = nis2Values.length > 0 ? nis2Values.reduce((a, b) => a + b, 0) / nis2Values.length : 0;
        nis2Score = Math.round((nis2Avg / 5) * 100);
        safeSetText("scoreNis2", nis2Score + "%");
      }
    }

    // Score Risques maîtrisés
    const totalRisks = (appData.risks || []).length;
    const criticalGraveRisks = (appData.risks || []).filter(r => r.level === "Critique" || r.level === "Grave").length;
    const risksControlled = totalRisks > 0 ? Math.round(((totalRisks - criticalGraveRisks) / totalRisks) * 100) : 0;
    safeSetText("scoreRisks", risksControlled + "%");

    // Score global pondéré — weights change if NIS2 disabled
    const globalScore = nis2Enabled
      ? Math.round(isoScore * 0.4 + nis2Score * 0.3 + risksControlled * 0.3)
      : Math.round(isoScore * 0.55 + risksControlled * 0.45);

    // Lancer l'animation au chargement
    animateScoreGauge(globalScore);
  }

  function animateScoreGauge(globalScore) {
    const arc = document.getElementById("scoreGlobalArc");
    const valueEl = document.getElementById("scoreGlobalValue");
    const gaugeContainer = document.querySelector(".score-global__gauge");

    if (!arc || !valueEl) return;

    const circumference = 2 * Math.PI * 54; // r=54

    // Couleur dynamique selon le score
    let strokeColor = 'var(--color-danger, #ef4444)'; // Rouge < 40
    if (globalScore >= 70) strokeColor = 'var(--color-success, #22c55e)'; // Vert
    else if (globalScore >= 40) strokeColor = 'var(--color-warning, #f59e0b)'; // Orange

    // Reset pour l'animation
    arc.style.transition = 'none';
    arc.style.strokeDashoffset = circumference;
    arc.style.stroke = strokeColor;

    // Forcer le reflow
    arc.getBoundingClientRect();

    // Animation fluide de la jauge (1.5s)
    arc.style.transition = 'stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1)';
    const targetOffset = circumference - (globalScore / 100) * circumference;
    arc.style.strokeDashoffset = targetOffset;

    // Animation du compteur
    animateValue(valueEl, 0, globalScore, 1500);

    // A11y: update SVG aria-label with score
    var svgEl = document.getElementById('scoreGlobalSvg');
    if (svgEl) svgEl.setAttribute('aria-label', t('dashboard.scoreSMSI', 'Score SMSI') + ': ' + globalScore + '%');

    // Effet pulse sur le conteneur
    if (gaugeContainer) {
      gaugeContainer.classList.remove('gauge-pulse');
      void gaugeContainer.offsetWidth; // Force reflow
      gaugeContainer.classList.add('gauge-pulse');
    }
  }

  function animateValue(element, start, end, duration) {
    const startTime = performance.now();
    const update = (currentTime) => {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);
      const easeOut = 1 - Math.pow(1 - progress, 3);
      const current = Math.round(start + (end - start) * easeOut);
      element.textContent = current;
      if (progress < 1) requestAnimationFrame(update);
    };
    requestAnimationFrame(update);
  }

  // Alertes dynamiques
  function updateDashboardAlerts() {
    const alertsList = document.getElementById("alertsList");
    if (!alertsList) return;

    const alerts = [];

    // Risques critiques
    const criticalRisks = (appData.risks || []).filter(r => r.level === "Critique");
    if (criticalRisks.length > 0) {
      alerts.push({
        icon: 'critical',
        iconClass: 'fas fa-skull-crossbones',
        text: `${criticalRisks.length} ${t('dashboard.alertCriticalRisks')}`,
        badge: t('dashboard.badgeUrgent'),
        target: 'risks'
      });
    }

    // Actions en retard
    const today = new Date().toISOString().split('T')[0];
    const overdueActions = (appData.actions || []).filter(a => {
      if (['termine', 'annule'].includes(a.status)) return false;
      return a.dueDate && a.dueDate < today;
    });
    if (overdueActions.length > 0) {
      alerts.push({
        icon: 'warning',
        iconClass: 'fas fa-clock',
        text: `${overdueActions.length} ${t('dashboard.alertOverdueActions')}`,
        badge: t('dashboard.badgeOverdue'),
        target: 'actions'
      });
    }

    // Audits à venir (30 jours)
    const in30Days = new Date();
    in30Days.setDate(in30Days.getDate() + 30);
    const upcomingAudits = (appData.audits || []).filter(a => {
      if (!a.plannedDate) return false;
      const auditDate = new Date(a.plannedDate);
      return auditDate >= new Date() && auditDate <= in30Days;
    });
    if (upcomingAudits.length > 0) {
      alerts.push({
        icon: 'info',
        iconClass: 'fas fa-calendar-check',
        text: `${upcomingAudits.length} ${t('dashboard.alertUpcomingAudits')}`,
        badge: t('dashboard.badgePlanned'),
        target: 'audits'
      });
    }

    // Projets à risque (status red)
    const projectsAtRisk = (appData.projectRisks || []).filter(p => p.status === 'red');
    if (projectsAtRisk.length > 0) {
      alerts.push({
        icon: 'purple',
        iconClass: 'fas fa-diagram-project',
        text: `${projectsAtRisk.length} ${t('dashboard.alertProjectsAtRisk')}`,
        badge: t('dashboard.badgeProjects'),
        target: 'project-risks'
      });
    }

    // Rendu des alertes
    if (alerts.length === 0) {
      alertsList.innerHTML = `<div class="dashboard-alerts__empty"><i class="fas fa-check-circle"></i>&nbsp;${t('dashboard.noAlerts')}</div>`;
    } else {
      alertsList.innerHTML = alerts.map(a => `
        <div class="alert-item" data-nav-target="${a.target}" role="button" tabindex="0">
          <div class="alert-item__icon alert-item__icon--${a.icon}">
            <i class="${a.iconClass}"></i>
          </div>
          <span class="alert-item__text">${escapeHtml(a.text)}</span>
          <span class="alert-item__badge">${escapeHtml(a.badge)}</span>
        </div>
      `).join('');

      // Event listeners pour navigation
      alertsList.querySelectorAll('.alert-item[data-nav-target]').forEach(item => {
        item.addEventListener('click', () => {
          const target = item.getAttribute('data-nav-target');
          // Utiliser la même logique que setupKpiCardNavigation
          if (_csiDashboardConsolidatedActive() && _csiIsSectionConsolidable(target)) {
            _csiOpenConsolidatedSection(target);
            return;
          }
          if (typeof switchModule === "function") {
            switchModule(target);
          } else {
            const navBtn = document.querySelector(`.nav__item[data-tab="${target}"]`);
            if (navBtn) navBtn.click();
            else if (typeof loadModuleContent === "function") loadModuleContent(target);
          }
        });
        item.addEventListener('keypress', (e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            item.click();
          }
        });
      });
    }
  }

  // Sparklines mini-graphiques
  function updateSparklines() {
    // Historique des métriques (stocké dans appData)
    if (!appData.kpiHistory) {
      appData.kpiHistory = {
        conformity: [],
        controls: [],
        actions: [],
        risks: [],
        nc: []
      };
    }

    // Récupérer les valeurs actuelles via FrameworkMapper when available
    var _FM = window.CSI_FrameworkMapper;
    var _sparkIsoStats = _FM ? _FM.getConformityStats(_getActiveControlsFrameworkId() || 'iso27002', { soaFilter: true }) : null;
    var implementedCount, conformityRate;
    if (_sparkIsoStats) {
      implementedCount = _sparkIsoStats.implemented;
      conformityRate = _sparkIsoStats.rate;
    } else {
      const soaEntries = Array.isArray(appData.soa) ? appData.soa : [];
      const soaMap = new Map(soaEntries.map((entry) => [entry.controlId, entry]));
      const applicableControls = (appData.controls || []).filter((c) => {
        const entry = soaMap.get(c.id);
        if (!entry) return true;
        return entry.applicable !== false;
      });
      implementedCount = applicableControls.filter((c) => ["implemente", "verifie"].includes(c.status)).length;
      conformityRate = applicableControls.length > 0 ? Math.round(implementedCount / applicableControls.length * 100) : 0;
    }
    const activeActions = (appData.actions || []).filter((a) => !["termine", "annule"].includes(a.status)).length;
    const criticalRisks = (appData.risks || []).filter((r) => r.level === "Critique").length;
    const openNCs = (Array.isArray(appData.nonconformities) ? appData.nonconformities : []).filter(function(nc) { return nc.status !== 'cloture' && nc.status !== 'closed' && nc.status !== 'fermee'; }).length;

    // Ajouter au historique (max 6 points)
    const addToHistory = (arr, val) => {
      if (!Array.isArray(arr)) return;
      arr.push(val);
      if (arr.length > 6) arr.shift();
    };

    // Ne mettre à jour l'historique qu'une fois par session (éviter les doublons).
    // (audit global 2026-07-03) PAS en mode lecture seule : un simple consulteur (viewer, ou
    // co-éditeur en consultation) ne doit ni muter kpiHistory persisté ni armer un push serveur
    // (qui échouerait en 403 et créerait une incohérence RAM/serveur) sur un simple affichage.
    const lastUpdate = appData.kpiHistory._lastUpdate || 0;
    const now = Date.now();
    var _kpiReadOnly = typeof isReadOnlyMode === 'function' && isReadOnlyMode();
    if (!_kpiReadOnly && now - lastUpdate > 60000) { // 1 minute minimum entre updates
      addToHistory(appData.kpiHistory.conformity, conformityRate);
      addToHistory(appData.kpiHistory.controls, implementedCount);
      addToHistory(appData.kpiHistory.actions, activeActions);
      addToHistory(appData.kpiHistory.risks, criticalRisks);
      addToHistory(appData.kpiHistory.nc, openNCs);
      appData.kpiHistory._lastUpdate = now;
      // FIX-GLOBALMETA-SYNC (audit 2026-06-10) : pousser l'historique KPI au serveur
      window._csiGlobalMetaDirty = true;
    }

    // Dessiner les sparklines
    drawSparkline('sparkConformity', appData.kpiHistory.conformity, '#0f9b7a');
    drawSparkline('sparkControls', appData.kpiHistory.controls, '#22c55e');
    drawSparkline('sparkActions', appData.kpiHistory.actions, '#f59e0b');
    drawSparkline('sparkRisks', appData.kpiHistory.risks, '#ef4444', true);
    drawSparkline('sparkNC', appData.kpiHistory.nc, '#2563eb', true);
  }

  function drawSparkline(canvasId, data, color, invertTrend = false) {
    const canvas = document.getElementById(canvasId);
    // FIX-KPIHISTORY: guard defensif — data peut être undefined si kpiHistory.X manque
    // (ex: server-data.json généré à la main sans sous-clés conformity/controls/etc.)
    if (!canvas || !Array.isArray(data) || data.length < 2) return;

    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;
    const padding = 2;

    ctx.clearRect(0, 0, width, height);

    const min = Math.min(...data);
    const max = Math.max(...data);
    const range = max - min || 1;

    const points = data.map((val, i) => ({
      x: padding + (i / (data.length - 1)) * (width - padding * 2),
      y: height - padding - ((val - min) / range) * (height - padding * 2)
    }));

    // Ligne
    ctx.beginPath();
    ctx.strokeStyle = color;
    ctx.lineWidth = 1.5;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    points.forEach((p, i) => {
      if (i === 0) ctx.moveTo(p.x, p.y);
      else ctx.lineTo(p.x, p.y);
    });
    ctx.stroke();

    // Point final
    const last = points[points.length - 1];
    ctx.beginPath();
    ctx.fillStyle = color;
    ctx.arc(last.x, last.y, 2.5, 0, Math.PI * 2);
    ctx.fill();
  }
  function updateControlsLabel() {
    var navLabel = document.getElementById("controlsNavLabel");
    if (!navLabel) return;
    var ctrlFw = _getActiveControlsFramework();
    if (ctrlFw && ctrlFw.meta) {
      var isEn = (typeof currentLang !== 'undefined' && currentLang === 'en') || (document.documentElement.lang === 'en');
      navLabel.textContent = (isEn ? ctrlFw.meta.shortName || ctrlFw.meta.name : (ctrlFw.meta.nameFr || ctrlFw.meta.shortName || ctrlFw.meta.name));
      navLabel.removeAttribute('data-i18n');
      // Update page title and subtitle to match active framework
      // Remove data-i18n to prevent translatePage() from overwriting dynamic values
      var titleEl = document.querySelector('[data-i18n="controlsISO.title"]');
      var subtitleEl = document.querySelector('[data-i18n="controlsISO.subtitle"]');
      if (titleEl) {
        titleEl.textContent = isEn ? (ctrlFw.meta.name || 'Controls') : (ctrlFw.meta.nameFr || ctrlFw.meta.name);
        titleEl.removeAttribute('data-i18n');
      }
      if (subtitleEl) {
        var desc = isEn ? (ctrlFw.meta.description || '') : (ctrlFw.meta.descriptionFr || ctrlFw.meta.description || '');
        subtitleEl.textContent = desc;
        subtitleEl.removeAttribute('data-i18n');
      }
    } else {
      navLabel.textContent = t('nav.controlsISO27002', 'Contrôles ISO27002');
    }
  }
  function updateCharts() {
    if (typeof Chart === "undefined") {
      void 0;
      showToast(t('toasts.chartLibUnavailable', 'Erreur : bibliothèque de graphiques non disponible'), "error");
      return;
    }
    updateCapabilityRadarChart();
    if (window.CSI_FrameworkMapper && (CSI_FrameworkMapper.isEnabled('nis2') || CSI_FrameworkMapper.isEnabled('recyf2025'))) {
      charts.nis2Radar = updateNis2RadarChart();
    }
    updateActionsChart();
    updateRisksChart();
    updateCmmChart();
    updateRiskMatrix();
    // Nouveaux graphiques compacts
    updateActionsStatusChart();
    updateControlsDomainChart();
    updateProjectRisksChart();
  }

  // A11y: make chart canvas accessible with sr-only description
  function makeChartAccessible(canvas, chartTitle, dataDescription) {
    if (!canvas) return;
    canvas.setAttribute('aria-hidden', 'true');
    var descId = canvas.id + '-desc';
    var desc = document.getElementById(descId);
    if (!desc) {
      desc = document.createElement('div');
      desc.id = descId;
      desc.className = 'sr-only';
      desc.setAttribute('role', 'img');
      canvas.parentNode.insertBefore(desc, canvas.nextSibling);
    }
    desc.setAttribute('aria-label', chartTitle + ': ' + dataDescription);
  }

  // Graphique Statut des Actions (Doughnut)
  function updateActionsStatusChart() {
    const ctx = document.getElementById("actionsStatusChart");
    if (!ctx) return;
    if (charts.actionsStatus) charts.actionsStatus.destroy();
    const themeVars = getThemeVars();

    const statusCounts = {
      'planifie': 0,
      'en_cours': 0,
      'termine': 0,
      'annule': 0
    };

    (appData.actions || []).forEach(a => {
      if (statusCounts.hasOwnProperty(a.status)) {
        statusCounts[a.status]++;
      }
    });

    const data = [statusCounts.planifie, statusCounts.en_cours, statusCounts.termine, statusCounts.annule];
    const total = data.reduce((a, b) => a + b, 0);

    if (total === 0) {
      ctx.style.display = 'none';
      var emptyMsg = ctx.parentElement.querySelector('.chart-empty-msg');
      if (!emptyMsg) {
        emptyMsg = document.createElement('div');
        emptyMsg.className = 'chart-empty-msg';
        emptyMsg.style.cssText = 'display:flex;align-items:center;justify-content:center;height:100%;color:var(--color-text-light);font-size:0.875rem;';
        ctx.parentElement.appendChild(emptyMsg);
      }
      emptyMsg.textContent = t('dashboard.noActions', 'Aucune action');
      emptyMsg.style.display = 'flex';
      return;
    }
    ctx.style.display = '';
    var existingMsg = ctx.parentElement.querySelector('.chart-empty-msg');
    if (existingMsg) existingMsg.style.display = 'none';

    const colors = [
      withAlpha(themeVars.warning, 0.8),
      withAlpha(themeVars.info, 0.8),
      withAlpha(themeVars.success, 0.8),
      withAlpha('#9ca3af', 0.6)
    ];
    const labels = [t('actionStatuses.planifie', 'Planifié'), t('actionStatuses.enCours', 'En cours'), t('actionStatuses.termine', 'Terminé'), t('actionStatuses.annule', 'Annulé')];

    charts.actionsStatus = new Chart(ctx, {
      type: "doughnut",
      data: {
        labels: labels,
        datasets: [{
          data: data,
          backgroundColor: colors,
          borderWidth: 0
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: '60%',
        plugins: {
          legend: { display: false },
          tooltip: {
            callbacks: {
              label: (ctx) => `${ctx.label}: ${ctx.raw} (${Math.round(ctx.raw / total * 100)}%)`
            }
          }
        }
      }
    });

    // Mini-légende
    renderMiniLegend('actionsStatusLegend', labels, colors, data);
    makeChartAccessible(ctx, t('dashboard.actionsStatus', 'Statut actions'), labels.map(function(l, i) { return l + ': ' + data[i]; }).join(', '));
  }

  // Graphique Contrôles par Domaine (Bar horizontal)
  function updateControlsDomainChart() {
    const ctx = document.getElementById("controlsDomainChart");
    if (!ctx) return;
    if (charts.controlsDomain) charts.controlsDomain.destroy();
    const themeVars = getThemeVars();

    const domains = ['Organisationnel', 'Humain', 'Physique', 'Technologique'];
    // Apply SoA applicability filter to exclude non-applicable controls
    const soaMap = new Map((appData.soa || []).map(function(e) { return [e.controlId, e]; }));
    const domainData = domains.map(domain => {
      var _isoCtrlsDomain = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
      const controls = _isoCtrlsDomain.filter(c => {
        if (c.category !== domain) return false;
        var soaEntry = soaMap.get(c.id);
        if (soaEntry && soaEntry.applicable === false) return false;
        return true;
      });
      const implemented = controls.filter(c => ['implemente', 'verifie'].includes(c.status)).length;
      return {
        domain: domain.substring(0, 4) + '.',
        total: controls.length,
        implemented: implemented
      };
    });

    charts.controlsDomain = new Chart(ctx, {
      type: "bar",
      data: {
        labels: domainData.map(d => d.domain),
        datasets: [
          {
            label: t('dashboard.chartImplemented'),
            data: domainData.map(d => d.implemented),
            backgroundColor: withAlpha(themeVars.success, 0.8),
            borderRadius: 4
          },
          {
            label: 'Total',
            data: domainData.map(d => d.total - d.implemented),
            backgroundColor: withAlpha(themeVars.secondary, 0.3),
            borderRadius: 4
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        indexAxis: 'y',
        scales: {
          x: {
            stacked: true,
            display: false
          },
          y: {
            stacked: true,
            grid: { display: false },
            ticks: { color: themeVars.textColor, font: { size: 10 } }
          }
        },
        plugins: {
          legend: { display: false },
          tooltip: {
            callbacks: {
              title: (items) => domains[items[0].dataIndex],
              label: (ctx) => ctx.datasetIndex === 0
                ? `${t('dashboard.chartImplemented')}: ${ctx.raw}`
                : `${t('dashboard.chartRemaining')}: ${ctx.raw}`
            }
          }
        }
      }
    });

    // Mini-légende
    const totalImpl = domainData.reduce((a, d) => a + d.implemented, 0);
    const totalAll = domainData.reduce((a, d) => a + d.total, 0);
    renderMiniLegend('controlsDomainLegend',
      [t('dashboard.chartImplemented'), t('dashboard.chartRemaining')],
      [withAlpha(themeVars.success, 0.8), withAlpha(themeVars.secondary, 0.3)],
      [totalImpl, totalAll - totalImpl]
    );
    makeChartAccessible(ctx, t('dashboard.controlsByDomain', 'Contrôles par domaine'), domains.map(function(d, i) { return d + ': ' + domainData[i].implemented + '/' + domainData[i].total; }).join(', '));
  }

  // Graphique Projets Risques (Doughnut avec statuts)
  function updateProjectRisksChart() {
    const ctx = document.getElementById("projectRisksChart");
    if (!ctx) return;
    if (charts.projectRisks) charts.projectRisks.destroy();
    const themeVars = getThemeVars();

    const projectRisks = appData.projectRisks || [];
    const statusCounts = {
      'green': 0,
      'yellow': 0,
      'red': 0,
      'todo': 0,
      'suspended': 0
    };

    projectRisks.forEach(p => {
      if (statusCounts.hasOwnProperty(p.status)) {
        statusCounts[p.status]++;
      }
    });

    const data = [statusCounts.green, statusCounts.yellow, statusCounts.red, statusCounts.todo, statusCounts.suspended];
    const labels = [t('projectRiskStatuses.green', 'Vert'), t('statuses.inProgress', 'En cours'), t('projectRiskStatuses.red', 'Rouge'), t('statuses.todo', 'À faire'), t('statuses.suspended', 'Suspendu')];
    const colors = [
      withAlpha(themeVars.success, 0.85),
      withAlpha(themeVars.warning, 0.85),
      withAlpha(themeVars.danger, 0.85),
      withAlpha('#9e9e9e', 0.85),
      withAlpha('#ff9800', 0.85)
    ];
    const total = data.reduce((a, b) => a + b, 0);

    if (total === 0) {
      ctx.style.display = 'none';
      var emptyMsg2 = ctx.parentElement.querySelector('.chart-empty-msg');
      if (!emptyMsg2) {
        emptyMsg2 = document.createElement('div');
        emptyMsg2.className = 'chart-empty-msg';
        emptyMsg2.style.cssText = 'display:flex;align-items:center;justify-content:center;height:100%;color:var(--color-text-light);font-size:0.875rem;';
        ctx.parentElement.appendChild(emptyMsg2);
      }
      emptyMsg2.textContent = t('dashboard.noProjects', 'Aucun projet');
      emptyMsg2.style.display = 'flex';
      const legend = document.getElementById('projectRisksLegend');
      if (legend) legend.innerHTML = '';
      return;
    }
    ctx.style.display = '';
    var existingMsg2 = ctx.parentElement.querySelector('.chart-empty-msg');
    if (existingMsg2) existingMsg2.style.display = 'none';

    charts.projectRisks = new Chart(ctx, {
      type: "doughnut",
      data: {
        labels: labels,
        datasets: [{
          data: data,
          backgroundColor: colors,
          borderWidth: 0
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: '55%',
        plugins: {
          legend: { display: false },
          tooltip: {
            callbacks: {
              label: (ctx) => `${ctx.label}: ${ctx.raw} projet(s)`
            }
          }
        }
      }
    });

    // Mini-légende
    renderMiniLegend('projectRisksLegend', labels, colors, data);
    makeChartAccessible(ctx, t('dashboard.projectRisks', 'Risques projets'), labels.map(function(l, i) { return l + ': ' + data[i]; }).join(', '));
  }

  // Fonction helper pour générer les mini-légendes
  function renderMiniLegend(containerId, labels, colors, values) {
    const container = document.getElementById(containerId);
    if (!container) return;

    container.innerHTML = labels.map((label, i) => {
      if (values[i] === 0) return '';
      return `
        <span class="chart-mini-legend__item">
          <span class="chart-mini-legend__dot" style="background:${colors[i]}"></span>
          <span>${escapeHtml(label)}</span>
          <span class="chart-mini-legend__value">${values[i]}</span>
        </span>
      `;
    }).join('');
  }

  function getStatusLabel(status) {
    const labels = {
      'planifie': t('actionStatuses.planifie', 'Planifié'),
      'en_cours': t('actionStatuses.enCours', 'En cours'),
      'termine': t('actionStatuses.termine', 'Terminé'),
      'annule': t('actionStatuses.annule', 'Annulé')
    };
    return labels[status] || status || '';
  }

  function updateConformityChart() {
    const ctx = document.getElementById("conformityChart");
    if (!ctx) return;
    if (charts.conformity) charts.conformity.destroy();
    const themeVars = getThemeVars();
    const soaEntries = Array.isArray(appData.soa) ? appData.soa : [];
    const soaMapChart = new Map(soaEntries.map(e => [e.controlId, e]));
    const applicableControls = (appData.controls || []).filter(c => {
      const soaEntry = soaMapChart.get(c.id);
      return !soaEntry || soaEntry.applicable !== false;
    });
    const categories = [...new Set(applicableControls.map((c) => c.category))].filter(Boolean);
    if (categories.length === 0) {
      ctx.parentElement && (ctx.parentElement.dataset.empty = 'true');
      return;
    }
    const data = categories.map((category) => {
      const filtered = applicableControls.filter((c) => c.category === category);
      const implemented = filtered.filter((c) => ["implemente", "verifie"].includes(c.status)).length;
      return filtered.length > 0 ? Math.round(implemented / filtered.length * 100) : 0;
    });
    const counts = categories.map((cat) => applicableControls.filter((c) => c.category === cat).length);
    const labels = categories.map((cat, i) => `${cat} (${counts[i]})`);
    const palette = [themeVars.primary, themeVars.secondary, themeVars.warning, themeVars.success, themeVars.info];
    const colors = categories.map((_, i) => withAlpha(palette[i % palette.length], 0.75));
    charts.conformity = new Chart(ctx, {
      type: "doughnut",
      data: { labels, datasets: [{ data, backgroundColor: colors, borderWidth: 1, borderColor: themeVars.surface }] },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            position: "bottom",
            labels: { font: { size: 12 }, padding: 20 }
          },
          tooltip: {
            callbacks: {
              label: (context) => `${context.label}: ${context.raw}% de conformit\xE9`
            }
          }
        },
        cutout: "60%"
      }
    });
    makeChartAccessible(ctx, t('dashboard.conformityByCategory', 'Conformité par catégorie'), labels.map(function(l, i) { return l + ': ' + data[i] + '%'; }).join(', '));
  }
  function updateActionsChart() {
    const ctx = document.getElementById("actionsChart");
    if (!ctx) return;
    if (charts.actions) charts.actions.destroy();
    const themeVars = getThemeVars();
    const gridColor = withAlpha(themeVars.borderColor, 0.6);
    const priorities = ["critical", "high", "medium", "low"];
    const priorityLabels = [t('priorities.critical', 'Critique'), t('priorities.high', 'Haute'), t('priorities.medium', 'Moyenne'), t('priorities.low', 'Faible')];
    const currentData = priorities.map((p) => (appData.actions || []).filter((a) => a.priority === p).length);
    var months = [];
    var now = new Date();
    for (var i = 5; i >= 0; i--) {
      var d = new Date(now.getFullYear(), now.getMonth() - i, 1);
      months.push(d.toLocaleDateString(undefined, { month: 'short', year: '2-digit' }));
    }
    if (!appData.actionsHistory) appData.actionsHistory = {};
    // FIX-ACTIONS-CHART-SAMPLING (audit 2026-06-10) : l'axe X est étiqueté par MOIS mais
    // l'historique était échantillonné par JOUR (valeurs d'hier sous le mois d'il y a 5
    // mois). Harmonisation : un seul instantané par mois calendaire (comparaison sur
    // AAAA-MM), aligné sur les libellés des 6 derniers mois.
    var _ahLastKey = '_actionsHistoryLastSnapshot';
    var _ahToday = new Date().toISOString().slice(0, 7);
    var _ahLast = (appData.actionsHistory[_ahLastKey] || '').slice(0, 7);
    if (_ahLast !== _ahToday) {
      priorities.forEach((priority, index) => {
        if (!Array.isArray(appData.actionsHistory[priority])) appData.actionsHistory[priority] = [];
        appData.actionsHistory[priority].push(currentData[index]);
        if (appData.actionsHistory[priority].length > months.length) {
          appData.actionsHistory[priority].shift();
        }
      });
      appData.actionsHistory[_ahLastKey] = _ahToday;
    } else {
      priorities.forEach((priority, index) => {
        if (!Array.isArray(appData.actionsHistory[priority])) appData.actionsHistory[priority] = [];
        if (appData.actionsHistory[priority].length === 0) {
          appData.actionsHistory[priority].push(currentData[index]);
        } else {
          appData.actionsHistory[priority][appData.actionsHistory[priority].length - 1] = currentData[index];
        }
      });
    }
    // FIX-ACTIONS-CHART-EMPTY : l'historique s'accumule un instantané par mois
    // calendaire au fil des ouvertures réelles du dashboard. Tant qu'il n'existe pas au
    // moins 2 mois de données, une courbe à point unique donne une impression de bug.
    // On affiche alors un état vide explicite plutôt qu'un graphique trompeur.
    const _content = (ctx.closest && ctx.closest('.chart-card__content')) || ctx.parentElement;
    if (_content) {
      const _prevNote = _content.querySelector('.chart-empty-note');
      if (_prevNote) _prevNote.remove();
    }
    const _storedMonths = Math.max.apply(null, [0].concat(priorities.map(p => (appData.actionsHistory[p] || []).length)));
    if (_storedMonths < 2) {
      ctx.style.display = 'none';
      if (_content) {
        const _note = document.createElement('div');
        _note.className = 'chart-empty-note';
        _note.textContent = t('charts.actionsHistoryBuilding', "L'historique se construit au fil des mois. La courbe d'évolution apparaîtra dès qu'au moins deux mois de données auront été enregistrés.");
        _content.appendChild(_note);
      }
      makeChartAccessible(ctx, t('dashboard.actionsByPriority', 'Actions par priorité'), priorityLabels.map(function(l, i) { return l + ': ' + currentData[i]; }).join(', '));
      return;
    }
    ctx.style.display = '';
    const datasets = priorities.map((priority, index) => {
      const history = appData.actionsHistory[priority] || [];
      const colors = {
        critical: withAlpha(themeVars.danger, 0.75),
        high: withAlpha(themeVars.warning, 0.75),
        medium: withAlpha(themeVars.secondary, 0.75),
        low: withAlpha(themeVars.success, 0.75)
      };
      // FIX-ACTIONS-CHART-SAMPLING (audit 2026-06-10) : caler les valeurs à droite
      // (la plus récente sous le mois courant) en complétant à gauche par null quand
      // l'historique compte moins de 6 mois.
      const _histSlice = history.slice(-months.length);
      const _histPadded = new Array(months.length - _histSlice.length).fill(null).concat(_histSlice);
      return {
        label: priorityLabels[index],
        data: _histPadded,
        backgroundColor: colors[priority],
        borderColor: withAlpha(colors[priority], 1),
        borderWidth: 2,
        tension: 0.3
      };
    });
    charts.actions = new Chart(ctx, {
      type: "line",
      data: { labels: months, datasets },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { position: "bottom" },
          tooltip: { mode: "index", intersect: false }
        },
        scales: {
          y: {
            beginAtZero: true,
            ticks: { precision: 0, color: themeVars.textColor },
            grid: { color: gridColor },
            title: { display: true, text: t('charts.actionCount', "Nombre d'actions"), color: themeVars.textColor }
          },
          x: {
            ticks: { color: themeVars.textColor },
            grid: { color: gridColor },
            title: { display: true, text: t('charts.months', 'Mois'), color: themeVars.textColor }
          }
        }
      }
    });
    makeChartAccessible(ctx, t('dashboard.actionsByPriority', 'Actions par priorité'), priorityLabels.map(function(l, i) { return l + ': ' + currentData[i]; }).join(', '));
  }
  function updateRisksChart() {
    const ctx = document.getElementById("riskChart");
    if (!ctx) return;
    if (charts.risks) charts.risks.destroy();
    const themeVars = getThemeVars();
    const levels = ["Critique", "Grave", "Significatif", "Mineur", "Insignifiant"];
    const data = levels.map((level) => (appData.risks || []).filter((r) => r.level === level).length);
    const colors = [
      withAlpha(themeVars.danger, 0.75),
      withAlpha(themeVars.warning, 0.75),
      withAlpha(themeVars.primary, 0.7),
      withAlpha(themeVars.success, 0.7),
      withAlpha(themeVars.info, 0.7)
    ];
    charts.risks = new Chart(ctx, {
      type: "pie",
      data: { labels: levels, datasets: [{ data, backgroundColor: colors, borderWidth: 1, borderColor: themeVars.surface }] },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { position: "bottom" },
          tooltip: {
            callbacks: {
              label: (context) => `${context.label}: ${context.raw} risque(s)`
            }
          }
        }
      }
    });
    makeChartAccessible(ctx, t('dashboard.risksByLevel', 'Risques par niveau'), levels.map(function(l, i) { return l + ': ' + data[i]; }).join(', '));
  }

  function updateCmmChart() {
    const ctx = document.getElementById("cmmChart");
    if (!ctx) return;
    if (charts.cmm) charts.cmm.destroy();
    const themeVars = getThemeVars();
    const levels = ["Initial", "R\xE9p\xE9table", "D\xE9fini", "G\xE9r\xE9", "Optimis\xE9"];
    var applicableSoa = (appData.soa || []).filter(function(s) { return s.applicable !== false; });
    var applicableIds = new Set(applicableSoa.map(function(s) { return s.controlId; }));
    var _isoCtrlsCmm = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    var applicableControls = _isoCtrlsCmm.filter(function(c) {
      return applicableIds.size === 0 || applicableIds.has(c.id);
    });
    const data = levels.map((lvl) => applicableControls.filter((c) => c.cmm === lvl).length);
    const colors = [
      withAlpha(themeVars.primary, 0.7),
      withAlpha(themeVars.success, 0.7),
      withAlpha(themeVars.warning, 0.7),
      withAlpha(themeVars.secondary, 0.7),
      withAlpha(themeVars.danger, 0.7)
    ];
    charts.cmm = new Chart(ctx, {
      type: "pie",
      data: { labels: levels, datasets: [{ data, backgroundColor: colors, borderWidth: 1, borderColor: themeVars.surface }] },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { position: "bottom" } }
      }
    });
    makeChartAccessible(ctx, t('dashboard.cmmLevels', 'Niveaux de maturité'), levels.map(function(l, i) { return l + ': ' + data[i]; }).join(', '));
  }

  function updateCapabilityRadarChart() {
    const ctx = document.getElementById("capabilityRadarChart");
    if (!ctx) return;
    if (charts.capabilityRadar) charts.capabilityRadar.destroy();
    const themeVars = typeof getThemeVars === "function" ? getThemeVars() : (() => {
      const target = document.body || document.documentElement;
      const styles = getComputedStyle(target);
      return {
        textColor: styles.getPropertyValue("--color-text").trim() || "#2d3e4a",
        textSecondary: styles.getPropertyValue("--color-text-secondary").trim() || "#758592",
        borderColor: styles.getPropertyValue("--color-border").trim() || "#e9ecef",
        primary: styles.getPropertyValue("--color-primary").trim() || "#1fb8cd",
        warning: styles.getPropertyValue("--color-warning").trim() || "#ffc185"
      };
    })();
    const { textColor, textSecondary, borderColor, primary, warning } = themeVars;
    const gridColor = withAlpha(borderColor, 0.7);
    const capabilities = OPERATIONAL_CAPABILITIES;
    const cmmValues = {
      Initial: 1,
      "R\xE9p\xE9table": 2,
      "D\xE9fini": 3,
      "G\xE9r\xE9": 4,
      "Optimis\xE9": 5
    };
    var radarApplicableSoa = (appData.soa || []).filter(function(s) { return s.applicable !== false; });
    var radarApplicableIds = new Set(radarApplicableSoa.map(function(s) { return s.controlId; }));
    var _isoCtrlsRadar = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    const soaFilteredControls = _isoCtrlsRadar.filter(function(c) {
      return radarApplicableIds.size === 0 || radarApplicableIds.has(c.id);
    });
    const validControls = soaFilteredControls.filter(c =>
      Array.isArray(c.subcategory) ? c.subcategory.length > 0 : c.subcategory
    );
    const missingCount = soaFilteredControls.length - validControls.length;
    if (missingCount > 0) {
      void 0;
    }
    // Légende détaillée (numérotation assurée par <ol>)
    const renderLegend = typeof renderRadarLegend === "function" ? renderRadarLegend : () => {};
    renderLegend("capabilityRadarLegend", capabilities.map((cap) => cap.label));
    const data = capabilities.map((cap) => {
      const controls = validControls.filter((c) =>
        Array.isArray(c.subcategory) ? c.subcategory.includes(cap.key) : c.subcategory === cap.key
      );
      if (controls.length === 0) return 0;
      const sum = controls.reduce(
        (acc, c) => acc + (cmmValues[c.cmm] || 0),
        0
      );
      return +(sum / controls.length).toFixed(2);
    });
    const targetValue = Number(appData.targetMaturity?.iso27002) || 0;
    const targetData = capabilities.map(() => targetValue);
    charts.capabilityRadar = new Chart(ctx, {
      type: "radar",
      data: {
        labels: capabilities.map((cap) => cap.label),
        datasets: [
          {
            label: t('chartLabels.target'),
            data: targetData,
            backgroundColor: withAlpha(warning, 0.08),
            borderColor: withAlpha(warning, 0.55),
            borderWidth: 3,
            pointRadius: 2,
            borderDash: [4, 4],
            fill: true
          },
          {
            label: t('chartLabels.averageCMM'),
            data,
            backgroundColor: withAlpha(primary, 0.2),
            borderColor: withAlpha(primary, 1),
            borderWidth: 3,
            pointBackgroundColor: withAlpha(primary, 1),
            pointRadius: 3,
            pointHoverRadius: 5
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          r: {
            beginAtZero: true,
            suggestedMin: 0,
            suggestedMax: 5,
            ticks: {
              stepSize: 1,
              color: textColor,
              backdropColor: "transparent",
              font: { size: 12 }
            },
            grid: { color: gridColor },
            angleLines: { color: gridColor },
            pointLabels: {
              display: true,
              color: textSecondary || borderColor,
              font: { size: 11, weight: "400" },
              callback: (_label, index) => `${index + 1}`
            }
          }
        },
        plugins: { legend: { display: false } }
      }
    });
  }

  function updateNis2RadarChart() {
    const ctx = document.getElementById("nis2RadarChart");
    if (!ctx) return null;
    if (charts.nis2Radar) charts.nis2Radar.destroy();
    const themeVars = typeof getThemeVars === "function" ? getThemeVars() : (() => {
      const target = document.body || document.documentElement;
      const styles = getComputedStyle(target);
      return {
        textColor: styles.getPropertyValue("--color-text").trim() || "#2d3e4a",
        textSecondary: styles.getPropertyValue("--color-text-secondary").trim() || "#758592",
        borderColor: styles.getPropertyValue("--color-border").trim() || "#e9ecef",
        secondary: styles.getPropertyValue("--color-secondary").trim() || "#5d878f",
        warning: styles.getPropertyValue("--color-warning").trim() || "#ffc185"
      };
    })();
    const { textColor, textSecondary, borderColor, secondary, warning } = themeVars;
    const gridColor = withAlpha(borderColor, 0.7);
    // Radar référentiel-conscient : ReCyF (France) = conformité par objectif (0-100 %) ;
    // CyFun (Belgique) = maturité par catégorie (0-5). CyFun reste inchangé ci-dessous.
    var _radarCard = ctx.closest ? ctx.closest('.chart-card') : null;
    var _radarTitle = _radarCard ? _radarCard.querySelector('.chart-card__title') : null;
    var _isRecyfRadar = (typeof appData !== 'undefined' && appData.nis2Referential === 'recyf2025');
    if (_radarTitle) {
      if (_isRecyfRadar) {
        // Retire data-i18n pour que translatePage() ne réécrase pas le titre ReCyF.
        _radarTitle.removeAttribute('data-i18n');
        _radarTitle.textContent = t('recyfControls.radarTitle', 'Conformité ReCyF par objectif');
      } else {
        _radarTitle.setAttribute('data-i18n', 'nis2Program.maturity');
        _radarTitle.textContent = t('nis2Program.maturity', 'Maturité NIS2');
      }
    }
    if (_isRecyfRadar) return _renderRecyfObjectiveRadar(ctx, themeVars);
    const categories = CYFUN_CATEGORIES.length > 0 ? CYFUN_CATEGORIES : Object.keys(appData.targetMaturity?.nis2 || {});
    if (categories.length === 0) return null;

    const labelsFull = categories.map((k) => {
      const full = CATEGORY_LABELS[k] || k;
      return full.split(" (")[0].trim();
    });
    const renderLegend = typeof renderRadarLegend === "function" ? renderRadarLegend : () => {};
    renderLegend("nis2RadarLegend", labelsFull);

    // Compute per-category doc and impl averages from controls via FrameworkMapper
    var FM = window.CSI_FrameworkMapper;
    var catMaturity = FM ? FM.getCategoryMaturity('cyfun2025') : null;
    const docData = [];
    const implData = [];
    if (catMaturity) {
      categories.forEach(function(catId) {
        var cm = catMaturity[catId];
        docData.push(cm ? cm.docAvg : 0);
        implData.push(cm ? cm.implAvg : 0);
      });
    } else {
      // Fallback: manual computation
      var nis2Controls = appData.nis2Controls || [];
      categories.forEach(function(catId) {
        var controls = nis2Controls.filter(function(c) { return c.categoryId === catId; });
        var docSum = 0, implSum = 0, count = 0;
        controls.forEach(function(c) {
          var d = c.docMaturity || 0;
          var i = c.implMaturity || 0;
          if (d > 0 || i > 0) { docSum += d; implSum += i; count++; }
        });
        docData.push(count > 0 ? +(docSum / count).toFixed(2) : 0);
        implData.push(count > 0 ? +(implSum / count).toFixed(2) : 0);
      });
    }

    const globalTarget = Number(appData.targetMaturity?.nis2Target) || 0;
    const globalData = categories.map(() => globalTarget);
    const fontSize = categories.length > 20 ? 11 : 14;

    charts.nis2Radar = new Chart(ctx, {
      type: "radar",
      data: {
        labels: labelsFull,
        datasets: [
          {
            label: t('chartLabels.globalTarget', 'Cible globale'),
            data: globalData,
            backgroundColor: withAlpha(warning, 0.08),
            borderColor: withAlpha(warning, 0.55),
            borderWidth: 3,
            pointRadius: 2,
            borderDash: [4, 4],
            fill: true
          },
          {
            label: t('chartLabels.docMaturity', 'Documentation'),
            data: docData,
            backgroundColor: withAlpha('#3b82f6', 0.12),
            borderColor: withAlpha('#3b82f6', 0.85),
            borderWidth: 2,
            pointRadius: 3,
            pointBackgroundColor: withAlpha('#3b82f6', 1),
            fill: true
          },
          {
            label: t('chartLabels.implMaturity', 'Implémentation'),
            data: implData,
            backgroundColor: withAlpha(secondary, 0.18),
            borderColor: withAlpha(secondary, 1),
            borderWidth: 3,
            pointBackgroundColor: withAlpha(secondary, 1),
            pointRadius: 3,
            pointHoverRadius: 5,
            fill: true
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          r: {
            beginAtZero: true,
            suggestedMin: 0,
            suggestedMax: 5,
            ticks: {
              stepSize: 1,
              color: textColor,
              backdropColor: "transparent",
              font: { size: 12 }
            },
            grid: { color: gridColor },
            angleLines: { color: gridColor },
            pointLabels: {
              display: true,
              color: textSecondary || borderColor,
              font: { size: fontSize, weight: "400" },
              callback: (_label, index) => `${index + 1}`
            }
          }
        },
        plugins: { legend: { display: true, position: 'bottom', labels: { usePointStyle: true, padding: 12 } } }
      }
    });
    return charts.nis2Radar;
  }

  // Radar/araignée ReCyF : taux de conformité par objectif de sécurité (périmètre du degré courant).
  function _renderRecyfObjectiveRadar(ctx, themeVars) {
    if (typeof ensureRecyfControls === 'function') ensureRecyfControls();
    var byObj = (typeof computeRecyfConformityByObjective === 'function') ? computeRecyfConformityByObjective() : [];
    var renderLegend = typeof renderRadarLegend === 'function' ? renderRadarLegend : function() {};
    if (byObj.length === 0) {
      renderLegend('nis2RadarLegend', []);
      return null;
    }
    var blue = '#0055A4';
    var gridColor = withAlpha(themeVars.borderColor, 0.7);
    var labels = byObj.map(function(b) { return b.objectiveId; });
    // M-RC3 : radar coverage-aware (percentApplicable), cohérent avec le KPI global du dashboard.
    var data = byObj.map(function(b) { return (typeof b.percentApplicable === 'number') ? b.percentApplicable : b.percent; });
    renderLegend('nis2RadarLegend', byObj.map(function(b) { return 'Obj. ' + b.objectiveId + ' — ' + (b.objectiveLabel || ''); }));
    var fontSize = byObj.length > 18 ? 11 : 13;
    charts.nis2Radar = new Chart(ctx, {
      type: 'radar',
      data: {
        labels: labels,
        datasets: [{
          label: t('recyfControls.conformity', 'Conformité') + ' (%)',
          data: data,
          backgroundColor: withAlpha(blue, 0.15),
          borderColor: withAlpha(blue, 0.9),
          borderWidth: 2,
          pointRadius: 3,
          pointBackgroundColor: withAlpha(blue, 1),
          pointHoverRadius: 5,
          fill: true
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          r: {
            beginAtZero: true,
            suggestedMin: 0,
            suggestedMax: 100,
            ticks: {
              stepSize: 20,
              color: themeVars.textColor,
              backdropColor: 'transparent',
              font: { size: 12 },
              callback: function(v) { return v + '%'; }
            },
            grid: { color: gridColor },
            angleLines: { color: gridColor },
            pointLabels: {
              display: true,
              color: themeVars.textSecondary || themeVars.borderColor,
              font: { size: fontSize, weight: '400' },
              callback: function(_label, index) { return byObj[index] ? byObj[index].objectiveId : (index + 1); }
            }
          }
        },
        plugins: {
          legend: { display: true, position: 'bottom', labels: { usePointStyle: true, padding: 12 } },
          tooltip: { callbacks: { label: function(c) {
            var b = byObj[c.dataIndex];
            if (!b) return c.formattedValue + '%';
            return 'Obj. ' + b.objectiveId + ' : ' + b.percent + '% (' + b.evaluated + '/' + (b.total - b.na) + ')';
          } } }
        }
      }
    });
    return charts.nis2Radar;
  }
  function updateRiskMatrix() {
    const matrix = document.getElementById("matrixGrid");
    if (!matrix) return;
    matrix.innerHTML = "";
    // Use risk thresholds if available (from risk-analysis module)
    const hasThresholds = window.CSI_RiskThresholds && typeof window.CSI_RiskThresholds.getActiveThreshold === 'function';
    let threshold = null;
    if (hasThresholds) {
      try { threshold = window.CSI_RiskThresholds.getActiveThreshold(); } catch(e) { threshold = null; }
    }
    // Single-pass: bucket risk counts by impact-probability key (O(N) instead of O(25*N))
    var riskCounts = {};
    var risks = appData.risks || [];
    for (var ri = 0; ri < risks.length; ri++) {
      var rk = (risks[ri].impact || 0) + '-' + (risks[ri].probability || 0);
      riskCounts[rk] = (riskCounts[rk] || 0) + 1;
    }
    for (let impact = 5; impact >= 1; impact--) {
      for (let prob = 1; prob <= 5; prob++) {
        const cell = document.createElement("div");
        cell.className = "matrix-cell";
        const score = impact * prob;
        if (threshold) {
          // Use threshold colors directly
          const lvl = window.CSI_RiskThresholds.getRiskLevelForCell(prob, impact, threshold);
          cell.style.backgroundColor = lvl.color;
          cell.title = `${IMPACT_LEVEL_LABELS[impact]} / ${PROBABILITY_LEVEL_LABELS[prob]} — ${lvl.label}`;
        } else {
          // Fallback: legacy 5-level classification
          let level = "";
          if (score >= 20) level = "critique";
          else if (score >= 15) level = "grave";
          else if (score >= 10) level = "significatif";
          else if (score >= 5) level = "mineur";
          else level = "insignifiant";
          cell.classList.add(`matrix-cell--${level}`);
          cell.title = `${IMPACT_LEVEL_LABELS[impact]} / ${PROBABILITY_LEVEL_LABELS[prob]}`;
        }
        const label = document.createElement("div");
        label.className = "matrix-cell__label";
        label.textContent = score;
        cell.appendChild(label);
        const count = riskCounts[impact + '-' + prob] || 0;
        if (count > 0) {
          const countElem = document.createElement("div");
          countElem.className = "matrix-cell__count";
          countElem.textContent = count;
          cell.appendChild(countElem);
        }
        matrix.appendChild(cell);
      }
    }
  }
  window.updateDashboard = updateDashboard;
  window.updateRiskMatrix = updateRiskMatrix;

  // modules/search.js
  function switchModule(moduleId) {
    const navBtn = document.querySelector(`.nav__item[data-tab="${moduleId}"]`);
    if (navBtn) {
      navBtn.click();
    } else {
      loadModuleContent(moduleId);
    }
  }
  // --- Deep Search Engine ---
  // Flatten any value (string, array, object) into a single searchable string
  function _flattenForSearch(val, depth) {
    if (depth === undefined) depth = 0;
    if (depth > 3 || val == null) return "";
    if (typeof val === "string") return val;
    if (typeof val === "number" || typeof val === "boolean") return String(val);
    if (Array.isArray(val)) return val.map(function(v) { return _flattenForSearch(v, depth + 1); }).join(" ");
    if (typeof val === "object") {
      var parts = [];
      for (var k in val) {
        if (val.hasOwnProperty(k) && k !== "id" && k !== "history") {
          parts.push(_flattenForSearch(val[k], depth + 1));
        }
      }
      return parts.join(" ");
    }
    return "";
  }

  function _pushEntry(entries, type, label, searchFields, module, action) {
    var searchParts = [];
    for (var i = 0; i < searchFields.length; i++) {
      var text = _flattenForSearch(searchFields[i]);
      if (text) searchParts.push(text);
    }
    var _searchText = searchParts.join(" ");
    entries.push({
      type: type,
      label: label || "",
      searchText: _searchText,
      // (audit global 2026-07-03, perf) formes normalisées NFD calculées UNE fois à la
      // construction de l'index (au lieu de NORMALIZE par frappe sur chaque enregistrement).
      nSearch: NORMALIZE(_searchText),
      nLabel: NORMALIZE(label || ""),
      module: module,
      action: action
    });
  }

  // Registry of known indexed appData keys (prevents double-indexing in auto-discovery)
  var _INDEXED_KEYS = {};

  // (audit global 2026-07-03, perf) l'index de recherche était RECONSTRUIT intégralement à
  // CHAQUE frappe (parcours de tout appData + i18n + flatten récursif + NFD par enregistrement).
  // On le met en CACHE, invalidé au seul point de mutation (saveData/loadData/changement de
  // langue) via _csiInvalidateSearchIndex — les données ne changent pas entre deux saveData.
  var _searchIndexCache = null;
  window._csiInvalidateSearchIndex = function() { _searchIndexCache = null; };
  function getSearchIndex() {
    if (_searchIndexCache) return _searchIndexCache;
    var entries = [];
    _INDEXED_KEYS = {};

    // --- Actions ---
    _INDEXED_KEYS.actions = true;
    (appData.actions || []).forEach(function(a) {
      _pushEntry(entries, t('searchResults.action'), a.title,
        [a.title, a.description, a.assignedTo, a.status, a.priority, a.owner, a.phase,
         (Array.isArray(a.comments) ? a.comments.map(function(c) { return c && c.text; }).join(' ') : a.comments)],
        "actions", function() { editAction(a.id); });
    });

    // --- Risks ---
    _INDEXED_KEYS.risks = true;
    (appData.risks || []).forEach(function(r) {
      _pushEntry(entries, t('searchResults.risk'), r.title,
        [r.title, r.description, r.treatment, r.level, r.comments],
        "risks", function() { editRisk(r.id); });
    });

    // --- Threats ---
    _INDEXED_KEYS.threats = true;
    (appData.threats || []).forEach(function(th) {
      _pushEntry(entries, t('searchResults.threat'), th.title,
        [th.title, th.description, th.category, th.level, th.comments],
        "threats-section", function() { editThreat(th.id); });
    });

    // --- Non-conformities ---
    _INDEXED_KEYS.nonconformities = true;
    (appData.nonconformities || []).forEach(function(nc) {
      _pushEntry(entries, t('searchResults.nc'), nc.title,
        [nc.title, nc.description, nc.rootCause, nc.correctiveAction, nc.status, nc.assignedTo, nc.type],
        "nonconformities", function() { editNC(nc.id); });
    });

    // --- Audits ---
    _INDEXED_KEYS.audits = true;
    (appData.audits || []).forEach(function(a) {
      _pushEntry(entries, t('searchResults.audit'), a.title,
        [a.title, a.description, a.scope, a.auditor, a.findings, a.recommendations, a.status, a.type],
        "audits", function() { editAudit(a.id); });
    });

    // --- Management Reviews ---
    _INDEXED_KEYS.reviews = true;
    (appData.reviews || []).forEach(function(r) {
      _pushEntry(entries, t('searchResults.review'), r.inputs || r.decisions || r.participants,
        [r.participants, r.inputs, r.decisions, r.comments],
        "managementReviews", function() { editReview(r.id); });
    });

    // --- Stakeholders ---
    _INDEXED_KEYS.stakeholders = true;
    (appData.stakeholders || []).forEach(function(s) {
      _pushEntry(entries, t('searchResults.stakeholder'), s.name,
        [s.name, s.role, s.contact, s.notes, s.category, s.expectations],
        "stakeholders", function() { editStakeholder(s.id); });
    });

    // --- Documents ---
    _INDEXED_KEYS.documents = true;
    (appData.documents || []).forEach(function(d) {
      _pushEntry(entries, t('searchResults.document'), d.title || d.type,
        [d.title, d.type, d.reference, d.owner, d.notes, d.status, d.location, d.version],
        "documents", function() { editDocument(d.id); });
    });

    // --- ISO 27002 Controls ---
    _INDEXED_KEYS.controls = true;
    var _FM = window.CSI_FrameworkMapper;
    var _searchIsoControls = _FM ? _FM.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    _searchIsoControls.forEach(function(c) {
      _pushEntry(entries, t('searchResults.control'), isoCtrlTitle(c),
        [c.title, c.titleEn, c.measure, c.description, c.descriptionEn, c.reference, c.category, c.status, c.cmm, c.comments, c.evidence, c.justificationControl],
        "controls", function() { editControl(c.id); });
    });

    // --- NIS2 Controls ---
    _INDEXED_KEYS.nis2Controls = true;
    var _searchNis2Controls = _FM ? _FM.getAppDataControls('cyfun2025') : (appData.nis2Controls || []);
    _searchNis2Controls.forEach(function(c) {
      _pushEntry(entries, t('searchResults.nis2Control'), c.description || c.categorie,
        [c.description, c.fonction, c.categorie, c.status, c.cmm, c.justification],
        "nis2-controls", function() { if (typeof editNis2Control === "function") editNis2Control(c.id); });
    });

    // --- ReCyF Controls (NIS2 France) ---
    // FIX-SEARCH-RECYF (audit 2026-06-10) : les mesures ReCyF étaient invisibles dans la
    // recherche globale — l'auto-découverte les écartait (aucun champ title/name/label/
    // description/reference dans les objets créés par loadRecyfControls). Indexation
    // explicite ref + objectif + contenu, avec navigation vers l'onglet recyf-controls.
    _INDEXED_KEYS.recyfControls = true;
    var _searchRecyfControls = _FM ? _FM.getAppDataControls('recyf2025') : (appData.recyfControls || []);
    _searchRecyfControls.forEach(function(c) {
      var _recyfLabel = (c.ref ? c.ref + ' — ' : '') + (c.objectiveLabel || c.theme || c.content || '');
      _pushEntry(entries, t('searchResults.recyfControl', 'Mesure ReCyF'), _recyfLabel,
        [c.ref, c.objectiveLabel, c.theme, c.content, c.conformity, c.justification],
        "recyf-controls", function() { if (typeof editRecyfControl === "function") editRecyfControl(c.id); });
    });

    // --- KPIs ---
    _INDEXED_KEYS.kpis = true;
    (appData.kpis || []).forEach(function(k) {
      _pushEntry(entries, "KPI", k.title,
        [k.title, k.description, k.unit, k.frequency, k.trend],
        "kpis", function() { editKpi(k.id); });
    });

    // --- Critical Assets ---
    _INDEXED_KEYS.criticalAssets = true;
    (appData.criticalAssets || []).forEach(function(a) {
      _pushEntry(entries, t('searchResults.criticalAsset'), getCriticalAssetLabel(a),
        [a.productName, a.productCode, a.beneficiary, a.productTags, a.availabilityNote, a.mfaDetail],
        "critical-assets", function() { openCriticalAssetModal(a.id); });
    });

    // --- Project Risks ---
    _INDEXED_KEYS.projectRisks = true;
    (appData.projectRisks || []).forEach(function(r) {
      _pushEntry(entries, t('searchResults.projectRisk'), r.projectName || r.reportNumber,
        [r.projectName, r.reportNumber, r.beneficiary, r.description, r.status, r.priority, r.recommendations],
        "project-risks", function() { openProjectRiskModal(r.id); });
    });

    // --- SOA (Statement of Applicability) ---
    _INDEXED_KEYS.soa = true;
    (appData.soa || []).forEach(function(s) {
      var _soaIsoControls = _FM ? _FM.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
      var ctrl = _soaIsoControls.find(function(c) { return c.id === s.controlId; });
      var label = ctrl ? isoCtrlTitle(ctrl) : s.controlId;
      _pushEntry(entries, "SOA", label,
        [label, s.justification, s.exclusionReason, s.owner, s.evidenceRef, s.implStatus, s.source],
        "soa", null);
    });

    // --- Document Review (normative) ---
    _INDEXED_KEYS.documentReview = true;
    (appData.documentReview || []).forEach(function(d) {
      if (!d.justification && !d.tool) return;
      _pushEntry(entries, "Revue normative", d.reference,
        [d.reference, d.justification, d.tool],
        "docReview", null);
    });

    // --- Budget Cybersécurité : lignes budgétaires ---
    _INDEXED_KEYS.nis2Plan = true;
    if (appData.nis2Plan && Array.isArray(appData.nis2Plan.budgetLines)) {
      appData.nis2Plan.budgetLines.forEach(function(line) {
        if (!line || !line.label) return;
        _pushEntry(entries, t('nav.nis2Program', 'Budget Cybersécurité'), line.label,
          [line.label, line.owner, line.category],
          "nis2-program", null);
      });
    }

    // --- SOCLE Pillars ---
    _INDEXED_KEYS.soclePillars = true;
    (appData.soclePillars || []).forEach(function(p) {
      _pushEntry(entries, "Socle NIS2", p.title,
        [p.title, p.subtitle, p.items, p.owner],
        "nis2-socle", null);
    });

    // --- Risk Analyses ---
    _INDEXED_KEYS.riskAnalyses = true;
    (appData.riskAnalyses || []).forEach(function(a) {
      var meta = a.meta || {};
      var ctx = a.context || {};
      var ident = a.identity || {};
      _pushEntry(entries, "Analyse de risques", meta.title || "Analyse",
        [meta.title, meta.reportNumber, ctx.perimeter, ctx.description, ident.beneficiary, ident.encoder, ident.validator, a.finalRecs],
        "risk-analysis", null);
    });

    // --- Impact Matrices ---
    _INDEXED_KEYS.impactMatrices = true;
    if (appData.impactMatrices && Array.isArray(appData.impactMatrices.matrices)) {
      appData.impactMatrices.matrices.forEach(function(m) {
        _pushEntry(entries, "Matrice d'impacts", m.name,
          [m.name, m.description],
          "impact-matrices", null);
      });
    }

    // --- Skip non-searchable keys ---
    _INDEXED_KEYS.riskThresholds = true;
    _INDEXED_KEYS.title = true;
    _INDEXED_KEYS.settings = true;
    _INDEXED_KEYS.nextActionId = true;
    _INDEXED_KEYS.targetMaturity = true;
    _INDEXED_KEYS.criticalAssetsMeta = true;
    _INDEXED_KEYS.kpiHistory = true;
    _INDEXED_KEYS.actionsHistory = true;

    // --- Auto-discovery: index unknown arrays in appData ---
    // Ensures future sections are automatically searchable
    for (var key in appData) {
      if (!appData.hasOwnProperty(key) || _INDEXED_KEYS[key]) continue;
      var val = appData[key];
      if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object") {
        val.forEach(function(item) {
          var label = item.title || item.name || item.label || item.description || item.reference || "";
          if (!label) return;
          _pushEntry(entries, key, label, [item], key, null);
        });
      }
    }

    // (audit 2026-07-03, revue effets de bord M/N/O) peupler RÉELLEMENT le cache : sans cette
    // ligne, _searchIndexCache restait null (jamais assigné) → l'index était reconstruit à chaque
    // frappe malgré le test en tête (le gain de perf annoncé était inopérant). Corollaire :
    // désormais que le cache VIT, il DOIT être invalidé à tout point de mutation d'appData — y
    // compris les pulls serveur (_pullAndApplyTab, bascule d'onglet, parachute 409) qui écrivent
    // appData hors saveData/loadData ; ces invalidations sont ajoutées à ces 3 sites.
    _searchIndexCache = entries;
    return entries;
  }

  function renderSearchResults(results, query) {
    var container = document.getElementById("globalSearchResults");
    if (!container) return;
    container.innerHTML = "";
    // === CSI-ME === (Lot 3c, §11.4) : la recherche ne couvre QUE le périmètre chargé —
    // l'afficher supprime la fausse négation silencieuse (« introuvable » devant un
    // auditeur alors que l'objet vit dans une autre partition).
    var _emSearch = window.CSI_EntityManager;
    if (_emSearch && _emSearch.isMultiEntity && _emSearch.isMultiEntity()) {
      var _scopeLbl = null;
      if (_emSearch.activeScope()) {
        _scopeLbl = _emSearch.activeScope();
        _emSearch.entities().forEach(function(e) { if (e && e.id === _scopeLbl) _scopeLbl = e.name || e.id; });
      } else if (_emSearch.isSubEntityUser()) {
        _scopeLbl = (_emSearch.info() && _emSearch.info().entityName) || null;
      } else if (_emSearch.isGroupUser()) {
        _scopeLbl = t('entities.mainEntity', 'Entité principale');
      }
      if (_scopeLbl) {
        var _scopeNote = document.createElement("div");
        _scopeNote.className = "csi-search-scope";
        _scopeNote.textContent = t('search.scopeNote', 'Recherche limitée au périmètre :') + ' ' + _scopeLbl;
        container.appendChild(_scopeNote);
      }
    }
    if (!results.length) {
      container.appendChild(document.createTextNode(t('search.noResults', 'Aucun résultat trouvé')));
      return;
    }
    var terms = NORMALIZE(query || "").split(/\s+/).filter(Boolean);
    results.slice(0, 30).forEach(function(res) {
      var btn = document.createElement("button");
      btn.type = "button";
      btn.className = "global-search__item";
      // Highlight matched terms in label
      var labelHtml = escapeHtml(res.label);
      if (terms.length) {
        terms.forEach(function(term) {
          var re = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
          labelHtml = labelHtml.replace(re, '<mark>$1</mark>');
        });
      }
      // Build match context: snippet from a field other than the label
      var contextHtml = "";
      if (res.matchField) {
        var ctx = res.matchField.length > 80 ? res.matchField.substring(0, 80) + "\u2026" : res.matchField;
        var ctxEsc = escapeHtml(ctx);
        if (terms.length) {
          terms.forEach(function(term) {
            var re = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
            ctxEsc = ctxEsc.replace(re, '<mark>$1</mark>');
          });
        }
        contextHtml = '<span class="global-search__context">' + ctxEsc + '</span>';
      }
      btn.innerHTML =
        '<span class="global-search__type">' + escapeHtml(res.type) + '</span>' +
        '<span class="global-search__label">' + labelHtml + contextHtml + '</span>' +
        '<span class="global-search__module">' + escapeHtml(res.module) + '</span>';
      btn.addEventListener("click", function() {
        switchModule(res.module);
        setTimeout(function() {
          if (typeof res.action === "function") res.action();
        }, 200);
      });
      container.appendChild(btn);
    });
  }

  function performGlobalSearch(query) {
    var q = NORMALIZE(query).trim();
    if (!q) {
      renderSearchResults([]);
      return;
    }
    var terms = q.split(/\s+/).filter(Boolean);
    var index = getSearchIndex();
    var scored = [];
    for (var i = 0; i < index.length; i++) {
      var entry = index[i];
      var nSearch = entry.nSearch !== undefined ? entry.nSearch : NORMALIZE(entry.searchText);
      var nLabel = entry.nLabel !== undefined ? entry.nLabel : NORMALIZE(entry.label);
      // All terms must appear somewhere in the full search text
      var allMatch = true;
      for (var j = 0; j < terms.length; j++) {
        if (nSearch.indexOf(terms[j]) === -1) { allMatch = false; break; }
      }
      if (!allMatch) continue;
      // Score: title/label match = highest, other fields = lower
      var score = 0;
      var labelMatched = false;
      for (var k = 0; k < terms.length; k++) {
        if (nLabel.indexOf(terms[k]) !== -1) { score += 10; labelMatched = true; }
        else score += 1;
      }
      // Exact phrase in label bonus
      if (nLabel.indexOf(q) !== -1) score += 20;
      // Type match bonus
      if (NORMALIZE(entry.type).indexOf(terms[0]) !== -1) score += 5;
      // Find match context: snippet from searchText beyond the label
      var matchField = "";
      if (!labelMatched || terms.length > 1) {
        var afterLabel = entry.searchText.substring(entry.label.length).trim();
        for (var m = 0; m < terms.length; m++) {
          var idx = NORMALIZE(afterLabel).indexOf(terms[m]);
          if (idx !== -1) {
            var start = Math.max(0, idx - 20);
            var end = Math.min(afterLabel.length, idx + terms[m].length + 60);
            matchField = (start > 0 ? "\u2026" : "") + afterLabel.substring(start, end).trim();
            break;
          }
        }
      }
      scored.push({
        type: entry.type,
        label: entry.label,
        module: entry.module,
        action: entry.action,
        score: score,
        matchField: matchField
      });
    }
    scored.sort((a, b) => SortUtils.numeric(b.score, a.score));
    renderSearchResults(scored, query);
  }

  function setupGlobalSearch() {
    var input = document.getElementById("globalSearchInput");
    var btn = document.getElementById("globalSearchBtn");
    if (!input || !btn) return;
    var debounceTimer;
    input.addEventListener("input", function() {
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(function() { performGlobalSearch(input.value); }, 150);
    });
    input.addEventListener("keydown", function(e) {
      if (e.key === "Enter") {
        e.preventDefault();
        clearTimeout(debounceTimer);
        performGlobalSearch(input.value);
      }
      if (e.key === "Escape") {
        input.value = "";
        renderSearchResults([]);
      }
    });
    btn.addEventListener("click", function() { performGlobalSearch(input.value); });
  }

  // modules/readOnlyMode.js
  const EDIT_PASSWORD_PROMPT = null; // deprecated: now uses i18n via t('header.editPasswordPrompt')
  const EDIT_PASSWORD_HASH = "15a4c04ed176b88620ebbe26e1f730cfdfa4d9e2bef419ec5cc2e438e80c1b51";
  function toUtf8Bytes(value) {
    if (typeof TextEncoder !== "undefined") {
      return new TextEncoder().encode(value);
    }
    const encoded = unescape(encodeURIComponent(value));
    const result = new Uint8Array(encoded.length);
    for (let i = 0; i < encoded.length; i++) {
      result[i] = encoded.charCodeAt(i);
    }
    return result;
  }
  function rotr(value, bits) {
    return value >>> bits | value << 32 - bits;
  }
  function sha256Fallback(bytes) {
    const K = [
      0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
      0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
      0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
      0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
      0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
      0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
      0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
      0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
    ];
    const H = [
      0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
      0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
    ];
    const bitLen = bytes.length * 8;
    const withOne = bytes.length + 1;
    let paddedLength = withOne;
    while (paddedLength % 64 !== 56) paddedLength++;
    const totalLength = paddedLength + 8;
    const msg = new Uint8Array(totalLength);
    msg.set(bytes);
    msg[bytes.length] = 0x80;
    const view = new DataView(msg.buffer);
    const high = Math.floor(bitLen / 0x100000000);
    const low = bitLen >>> 0;
    view.setUint32(totalLength - 8, high, false);
    view.setUint32(totalLength - 4, low, false);
    const W = new Uint32Array(64);
    for (let i = 0; i < msg.length; i += 64) {
      for (let j = 0; j < 16; j++) {
        W[j] = view.getUint32(i + j * 4, false);
      }
      for (let j = 16; j < 64; j++) {
        const s0 = rotr(W[j - 15], 7) ^ rotr(W[j - 15], 18) ^ W[j - 15] >>> 3;
        const s1 = rotr(W[j - 2], 17) ^ rotr(W[j - 2], 19) ^ W[j - 2] >>> 10;
        W[j] = (W[j - 16] + s0 + W[j - 7] + s1) >>> 0;
      }
      let a = H[0];
      let b = H[1];
      let c = H[2];
      let d = H[3];
      let e = H[4];
      let f = H[5];
      let g = H[6];
      let h = H[7];
      for (let j = 0; j < 64; j++) {
        const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
        const ch = e & f ^ ~e & g;
        const temp1 = (h + S1 + ch + K[j] + W[j]) >>> 0;
        const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
        const maj = a & b ^ a & c ^ b & c;
        const temp2 = (S0 + maj) >>> 0;
        h = g;
        g = f;
        f = e;
        e = (d + temp1) >>> 0;
        d = c;
        c = b;
        b = a;
        a = (temp1 + temp2) >>> 0;
      }
      H[0] = (H[0] + a) >>> 0;
      H[1] = (H[1] + b) >>> 0;
      H[2] = (H[2] + c) >>> 0;
      H[3] = (H[3] + d) >>> 0;
      H[4] = (H[4] + e) >>> 0;
      H[5] = (H[5] + f) >>> 0;
      H[6] = (H[6] + g) >>> 0;
      H[7] = (H[7] + h) >>> 0;
    }
    return Array.from(H).map((val) => val.toString(16).padStart(8, "0")).join("");
  }
  async function hashEditPassword(raw) {
    const data = toUtf8Bytes(raw || "");
    const cryptoObj = (typeof crypto !== "undefined" && crypto) || (typeof window !== "undefined" ? window.crypto : undefined);
    if (cryptoObj && cryptoObj.subtle && typeof cryptoObj.subtle.digest === "function") {
      const hashBuffer = await cryptoObj.subtle.digest("SHA-256", data);
      return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
    }
    return sha256Fallback(data);
  }
  function hashEditPasswordSync(raw) {
    return sha256Fallback(toUtf8Bytes(raw || ""));
  }
  const EDIT_PASSWORD_ALLOWED_HASHES = [EDIT_PASSWORD_HASH, "06898c84c85f504230dcf2664c054e62899be54a9d8daa319ef0cca8f6b2ae3d"].filter(Boolean);
  const EDIT_PASSWORD_STORAGE_KEY = "smsi_edit_password_hash";
  const MIN_EDIT_PASSWORD_LENGTH = 18;
  const EDIT_PASSWORD_REGEX = {
    upper: /[A-Z]/,
    lower: /[a-z]/,
    special: /[^A-Za-z0-9\s]/
  };
  let editPasswordUnlocked = false;
  function getEditPasswordPolicyStatus(password = "", minLength = MIN_EDIT_PASSWORD_LENGTH) {
    const value = String(password || "");
    const checks = [
      { key: "length", label: t('passwordPolicy.minLength', 'Au moins {{min}} caractères').replace('{{min}}', minLength), ok: value.length >= minLength },
      { key: "upper", label: t('passwordPolicy.uppercase', 'Au moins une majuscule'), ok: EDIT_PASSWORD_REGEX.upper.test(value) },
      { key: "lower", label: t('passwordPolicy.lowercase', 'Au moins une minuscule'), ok: EDIT_PASSWORD_REGEX.lower.test(value) },
      { key: "special", label: t('passwordPolicy.special', 'Au moins un caractère spécial'), ok: EDIT_PASSWORD_REGEX.special.test(value) }
    ];
    const missing = checks.filter((check) => !check.ok).map((check) => check.label);
    return { checks, missing, isValid: missing.length === 0 };
  }
  function createPasswordPolicyList(minLength = MIN_EDIT_PASSWORD_LENGTH) {
    const wrap = document.createElement("div");
    wrap.className = "password-policy";
    const title = document.createElement("p");
    title.className = "password-policy__title";
    title.textContent = t('passwordPolicy.title', 'Exigences du mot de passe :');
    const list = document.createElement("ul");
    list.className = "password-policy__list";
    getEditPasswordPolicyStatus("", minLength).checks.forEach((check) => {
      const item = document.createElement("li");
      item.className = "password-policy__item";
      item.dataset.policyKey = check.key;
      item.dataset.label = check.label;
      list.appendChild(item);
    });
    wrap.append(title, list);
    updatePasswordPolicyList(list, "", minLength);
    return { wrap, list };
  }
  function updatePasswordPolicyList(list, password, minLength = MIN_EDIT_PASSWORD_LENGTH) {
    if (!list) return false;
    const status = getEditPasswordPolicyStatus(password, minLength);
    status.checks.forEach((check) => {
      const item = list.querySelector(`[data-policy-key="${check.key}"]`);
      if (!item) return;
      const label = item.dataset.label || check.label;
      item.textContent = check.ok ? (t('passwordPolicy.ok', 'OK') + ': ' + label) : (t('passwordPolicy.missing', 'Manque') + ': ' + label);
      item.classList.toggle("password-policy__item--ok", check.ok);
      item.classList.toggle("password-policy__item--missing", !check.ok);
    });
    return status.isValid;
  }
  function getStoredEditPasswordHash() {
    try {
      const stored = localStorage.getItem(EDIT_PASSWORD_STORAGE_KEY);
      return typeof stored === "string" ? stored : "";
    } catch (e) {
      return "";
    }
  }
  function setStoredEditPasswordHash(hash) {
    try {
      if (hash) localStorage.setItem(EDIT_PASSWORD_STORAGE_KEY, hash);
      else localStorage.removeItem(EDIT_PASSWORD_STORAGE_KEY);
    } catch (e) {
      // Ignore storage errors
    }
  }
  function getAllowedEditPasswordHashes() {
    const stored = getStoredEditPasswordHash();
    if (stored && stored.length >= 64) return [stored];
    return EDIT_PASSWORD_ALLOWED_HASHES;
  }
  function isLocalhost() {
    try {
      return ["localhost", "127.0.0.1", "", "::1"].includes(window.location.hostname);
    } catch (e) {
      return false;
    }
  }
  async function verifyEditPassword(pwd) {
    try {
      const hash = await hashEditPassword(pwd);
      const allowed = getAllowedEditPasswordHashes();
      if (!allowed.includes(hash)) return false;
      // Check if matched a factory hash (not a user-set custom one)
      const storedHash = getStoredEditPasswordHash();
      const isFactory = (!storedHash || storedHash.length < 64) && EDIT_PASSWORD_ALLOWED_HASHES.includes(hash);
      if (isFactory) {
        verifyEditPassword._lastMatchedFactory = true;
      } else {
        verifyEditPassword._lastMatchedFactory = false;
      }
      return true;
    } catch (e) {
      void 0;
      return false;
    }
  }
  function shouldPromptEditPasswordSetup() {
    if (typeof shouldAutoUnlockForTests === "function" && shouldAutoUnlockForTests()) return false;
    // Mode serveur : l'auth JWT gère l'accès édition — pas besoin du mot de passe local Phase 1
    if (typeof isServerModeUnlocked === "function" && isServerModeUnlocked()) return false;
    return !getStoredEditPasswordHash();
  }
  function queueEditPasswordSetup() {
    if (!shouldPromptEditPasswordSetup()) return;
    setTimeout(() => {
      if (!shouldPromptEditPasswordSetup()) return;
      openEditPasswordSetupModal({ allowSkip: false });
    }, 250);
  }
  function openEditPasswordSetupModal(options = {}) {
    if (!shouldPromptEditPasswordSetup()) return;
    const overlay = document.getElementById("modalOverlay");
    if (overlay && overlay.classList.contains("modal-overlay--visible")) return;
    const minLength = typeof options.minLength === "number" ? options.minLength : MIN_EDIT_PASSWORD_LENGTH;
    const allowSkip = options.allowSkip !== false;
    const cancelLabel = allowSkip ? t('common.later', 'Plus tard') : t('common.cancel', 'Annuler');
    openModal(t('modals.setPassword'), [
      { name: "password", label: t('auth.newPassword', 'Nouveau mot de passe'), type: "password", value: "" },
      { name: "confirm", label: t('auth.confirmPassword', 'Confirmer le mot de passe'), type: "password", value: "" }
    ], (data) => {
      const password = data.password || "";
      const confirm = data.confirm || "";
      if (!password) {
        showToast2(t('toasts.enterPassword'), "warning");
        return false;
      }
      const policy = getEditPasswordPolicyStatus(password, minLength);
      if (!policy.isValid) {
        const detail = policy.missing.length ? t('passwordPolicy.missingRequirements', 'Exigences manquantes') + ': ' + policy.missing.join(", ") + '.' : "";
        showToast2((t('settings.passwordNonCompliant', 'Mot de passe non conforme.') + ' ' + detail).trim(), "warning");
        return false;
      }
      if (password !== confirm) {
        showToast2(t('toasts.passwordMismatch'), "error");
        return false;
      }
      const hash = hashEditPasswordSync(password);
      const isFirstPasswordSetup = !localStorage.getItem("smsi_password_setup_done");
      setStoredEditPasswordHash(hash);
      editPasswordUnlocked = true;
      showToast2(t('toasts.passwordSaved'), "success");
      if (isFirstPasswordSetup) {
        localStorage.setItem("smsi_password_setup_done", "1");
        setTimeout(() => {
          showToast2(t('toasts.readOnlyStart', "L'application démarre en mode lecture seule. Cliquez sur le cadenas pour passer en mode édition."), "info", 8000);
        }, 2000);
      }
      return true;
    }, null, {
      allowReadOnly: true,
      saveLabel: t('common.save', 'Enregistrer'),
      cancelLabel,
      hideCancel: !allowSkip,
      hideClose: !allowSkip
    });
    const modalBody = document.getElementById("modalBody");
    const passwordInput = document.getElementById("modal-input-password");
    if (modalBody && passwordInput) {
      const { wrap, list } = createPasswordPolicyList(minLength);
      modalBody.appendChild(wrap);
      const update = () => updatePasswordPolicyList(list, passwordInput.value, minLength);
      passwordInput.addEventListener("input", update);
      update();
    }
  }
  let editModeEnabled = false;
  const HISTORY_KEY = "smsi_history";
  // Mode serveur : historique en RAM uniquement (perdu au rechargement)
  var _ramHistorySnapshots = [];
  const HISTORY_LIMIT_DEFAULT = 10;
  const HISTORY_LIMIT_STORAGE_KEY = "smsi_history_limit";
  const WELCOME_STORAGE_KEY = "smsi_welcome_seen";
  const NORMALIZE = (str) => (str || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
  function shortenLegendLabel(label) {
    const clean = (label || "").replace(/\s+/g, " ").trim();
    const maxLen = 38;
    if (clean.length <= maxLen) return clean;
    const truncated = clean.slice(0, maxLen);
    const lastSpace = truncated.lastIndexOf(" ");
    const base = lastSpace > 18 ? truncated.slice(0, lastSpace) : truncated;
    return `${base.trim()}…`;
  }
  function renderRadarLegend(targetId, items) {
    const container = document.getElementById(targetId);
    if (!container) return;
    if (!Array.isArray(items) || items.length === 0) {
      container.innerHTML = "";
      return;
    }
    container.classList.add("chart-legend--two-cols");
    const list = document.createElement("ol");
    items.forEach((text) => {
      const li = document.createElement("li");
      li.textContent = shortenLegendLabel(text);
      list.appendChild(li);
    });
    container.innerHTML = "";
    container.appendChild(list);
  }
  function attachLegendTooltip(targetId) {
    const btn = document.querySelector(`[data-legend-toggle="${targetId}"]`);
    const legend = document.getElementById(targetId);
    if (!btn || !legend) return;
    const show = () => legend.removeAttribute("hidden");
    const hide = () => legend.setAttribute("hidden", "");
    ["mouseenter", "focus"].forEach((evt) => btn.addEventListener(evt, show));
    ["mouseleave", "blur"].forEach((evt) => btn.addEventListener(evt, hide));
    ["mouseenter", "mouseleave"].forEach((evt) => legend.addEventListener(evt, (e) => {
      if (evt === "mouseenter") show();
      else hide();
    }));
    hide();
  }
  const EDIT_LOCK_IDS = [
    "addControlBtn",
    "addActionBtn",
    "manageBudgetCategoriesBtn",
    "manageBudgetYearsBtn",
    "addBudgetLineBtn",
    "addRiskBtn",
    "addThreatBtn",
    "addNcBtn",
    "addAuditBtn",
    "addReviewBtn",
    "addStakeholderBtn",
    "addDocumentBtn",
    "addKpiBtn",
    "addProjectRiskBtn",
    "serverSyncBtn",
    "settingsImport",
    "settingsReset",
    "settingsDemo",
    "serverSyncDownload",
    "serverSyncUpload"
  ];
  function isReadOnlyMode() {
    // === CSI-CO === _csiClusterReadOnly : en co-édition par grappes, l'onglet AFFICHÉ
    // appartient à une grappe détenue par un autre utilisateur → cet écran est en lecture
    // seule SANS quitter le mode édition (les grappes détenues restent éditables ailleurs).
    // Toujours false hors co-édition (posé/levé uniquement par _csiEnsureClusterForTab).
    return !editModeEnabled || !!window._csiClusterReadOnly;
  }

  // === CSI-BUILD === Bandeau « nouvelle version disponible » (appelé par lock-manager
  // quand le serverBuild du presence-ping diffère de CSI_CLIENT_BUILD : onglet resté ouvert
  // pendant une mise à jour serveur). Un SEUL affichage par build (pas de harcèlement) ;
  // jamais de reload forcé — le bouton sauvegarde, draine les pushes, libère les verrous
  // PUIS recharge. Si un rechargement a déjà été tenté pour ce build et que le client est
  // toujours périmé (cache coincé/proxy), le message escalade vers le rechargement forcé
  // (Ctrl+F5) — le « vider le cache » n'est jamais demandé en premier recours.
  var _csiBuildNotified = null;
  function _csiNotifyServerBuild(serverBuild) {
    if (_csiBuildNotified === serverBuild) return;           // déjà signalé pour ce build
    if (document.getElementById('csiUpdateBanner')) return;  // bandeau déjà affiché
    _csiBuildNotified = serverBuild;
    var attemptedKey = 'csi_reload_attempted_' + serverBuild;
    var alreadyTried = false;
    try { alreadyTried = !!localStorage.getItem(attemptedKey); } catch (e) { /* storage indispo */ }

    var bar = document.createElement('div');
    bar.id = 'csiUpdateBanner';
    bar.className = 'update-banner';
    bar.setAttribute('role', 'status');
    var msg = document.createElement('span');
    msg.textContent = alreadyTried
      ? t('updateBanner.hardReload', 'La mise à jour n\'a pas été prise en compte — faites un rechargement forcé (Ctrl+F5, ou Cmd+Maj+R sur Mac).')
      : t('updateBanner.newVersion', 'Une nouvelle version de l\'application est disponible.');
    bar.appendChild(msg);
    if (!alreadyTried) {
      var btn = document.createElement('button');
      btn.className = 'btn btn--primary btn--sm';
      btn.textContent = t('updateBanner.reload', 'Mettre à jour');
      btn.onclick = async function () {
        btn.disabled = true;
        try { if (!isReadOnlyMode() && typeof saveData === 'function') saveData(); } catch (e) { /* best effort */ }
        try { if (typeof _flushServerPushes === 'function') await _flushServerPushes(15000); } catch (e) { /* best effort */ }
        try {
          var ELMu = window.CSI_EditLockManager;
          if (ELMu && ELMu.release) await ELMu.release();
        } catch (e) { /* best effort */ }
        try { localStorage.setItem(attemptedKey, '1'); } catch (e) { /* storage indispo */ }
        window.location.reload();
      };
      bar.appendChild(btn);
    }
    var close = document.createElement('button');
    close.className = 'update-banner__close';
    close.setAttribute('aria-label', t('updateBanner.dismiss', 'Ignorer'));
    close.textContent = '×';
    close.onclick = function () { if (bar.parentNode) bar.parentNode.removeChild(bar); };
    bar.appendChild(close);
    document.body.appendChild(bar);
  }
  window._csiNotifyServerBuild = _csiNotifyServerBuild;

  // === CSI-CO === Libellé humain d'une grappe (le bandeau ne dit jamais « grappe »).
  function _csiClusterLabel(cluster) {
    var map = {
      coeur: t('clusters.coeur', 'Pilier SMSI'),
      pilotage: t('clusters.pilotage', 'Pilotage & documents'),
      analyse: t('clusters.analyse', 'Analyses de risques'),
      config: t('clusters.config', 'Configuration')
    };
    return map[cluster] || cluster || '';
  }

  // === CSI-CO === (courtoisie §12, INV-C9) Vrai si la grappe a des onglets avec des
  // modifications locales non confirmées par le serveur (dirty) ou des pushes à rejouer
  // (pending) — une telle grappe ne doit JAMAIS être relâchée par courtoisie.
  function _csiClusterHasPendingWork(cluster) {
    var ELM = window.CSI_EditLockManager;
    var mgr = window.CSI_PresenceManager || window.CSI_LockManager;
    var map = (mgr && mgr.getTabDataKeys) ? mgr.getTabDataKeys() : null;
    if (!map || !ELM || !ELM.clusterForTab) return true; // dans le doute : ne pas relâcher
    for (var tb in map) {
      if (!map.hasOwnProperty(tb)) continue;
      if (ELM.clusterForTab(tb) !== cluster) continue;
      if (_csiDirtyTabs[tb] || _pendingPushTabs[tb]) return true;
    }
    return false;
  }

  // === CSI-CO === À chaque affichage d'onglet en mode édition : garantit la grappe de
  // l'onglet (acquisition silencieuse si libre). Refus → l'écran passe en lecture seule
  // (bandeau orange nominatif), les grappes déjà détenues restent éditables. No-op complet
  // hors co-édition (flag serveur off / mode local) — zéro changement de comportement.
  // (courtoisie §12) AVANT l'acquisition (INV-C10), relâche silencieusement les grappes
  // détenues NON touchées (aucune édition réelle) et sans push en attente — le module
  // redevient immédiatement disponible pour un collègue. Une grappe travaillée reste
  // détenue jusqu'à la sortie d'édition (INV-C9). Aucun message utilisateur.
  function _csiEnsureClusterForTab(moduleId) {
    var ELM = window.CSI_EditLockManager;
    if (!ELM || !ELM.isCoedit || !ELM.isCoedit()) { window._csiClusterReadOnly = false; return; }
    if (!editModeEnabled) { window._csiClusterReadOnly = false; return; }
    var _courtesy = Promise.resolve();
    if (ELM.releaseClusters && ELM.heldClusters) {
      var _keep = (ELM.isNeutralTab && ELM.isNeutralTab(moduleId)) ? null : ELM.clusterForTab(moduleId);
      var _candidates = ELM.heldClusters().filter(function (c) {
        return c !== _keep && !_csiClusterHasPendingWork(c);
      });
      // releaseClusters re-filtre à l'envoi (grappe de l'onglet courant, grappes touchées) —
      // double garde anti-course R4 ; best-effort silencieux (échec = grappe conservée).
      if (_candidates.length) _courtesy = ELM.releaseClusters(_candidates);
    }
    _courtesy.then(function () { return ELM.ensureClusterFor(moduleId); }).then(function (res) {
      // L'utilisateur a pu re-naviguer pendant l'acquisition : n'appliquer qu'à l'onglet courant.
      if (moduleId !== _currentActiveTab) return;
      var wasRo = !!window._csiClusterReadOnly;
      window._csiClusterReadOnly = !res.editable;
      if (!res.editable && !wasRo) {
        var holderName = (res.holder && (res.holder.userName || res.holder.userId)) || '?';
        showToast2(t('modeBanner.clusterBusy', 'Module « {{module}} » en cours d\'édition par {{name}} — lecture seule ici.')
          .replace('{{module}}', _csiClusterLabel(ELM.clusterForTab(moduleId)))
          .replace('{{name}}', holderName), 'warning');
      }
      if (window._csiClusterReadOnly !== wasRo) refreshReadOnlyLocks();
      updateModeBannerUi();
      updateEditModeButtonUi();
    }).catch(function () { /* best-effort — l'état sera réévalué au prochain ping */ });
  }
  function showReadOnlyNotice(actionLabel = "") {
    try {
      showToast2(t('header.readOnlyNotice'), "warning");
    } catch (e) {
      void 0;
    }
  }
  function refreshReadOnlyLocks() {
    const disabled = isReadOnlyMode();
    document.body.classList.toggle("mode-readonly", disabled);
    document.body.classList.toggle("mode-edit", !disabled);
    document.querySelectorAll("[data-readonly-lock]").forEach((el) => {
      const isPermanent = el.dataset?.permanentReadonly === "true";
      if (isPermanent) {
        el.disabled = true;
        return;
      }
      if ("disabled" in el) {
        el.disabled = disabled;
      } else if (disabled) {
        el.setAttribute("aria-disabled", "true");
      } else if (el.getAttribute("aria-disabled") === "true") {
        el.removeAttribute("aria-disabled");
      }
    });
  }
  // Timer pour le compte à rebours du bandeau "édité par autre"
  let _modeBannerCountdownTimer = null;
  function _stopModeBannerCountdown() {
    if (_modeBannerCountdownTimer) {
      clearInterval(_modeBannerCountdownTimer);
      _modeBannerCountdownTimer = null;
    }
  }
  function _clearChildren(node) {
    while (node && node.firstChild) node.removeChild(node.firstChild);
  }
  function _setBannerStatusDom(target, iconClass, leadText, strongText) {
    _clearChildren(target);
    if (iconClass) {
      const ic = document.createElement("i");
      ic.className = iconClass;
      ic.setAttribute("aria-hidden", "true");
      target.appendChild(ic);
      target.appendChild(document.createTextNode(" "));
    }
    if (leadText) target.appendChild(document.createTextNode(leadText));
    if (strongText) {
      const s = document.createElement("strong");
      s.textContent = strongText;
      target.appendChild(s);
    }
  }
  function _setBannerCountdownDom(target, seconds, suffixText) {
    _clearChildren(target);
    if (seconds <= 0) return;
    const cd = document.createElement("span");
    cd.className = "mode-banner__countdown";
    cd.textContent = seconds + "s";
    target.appendChild(cd);
    target.appendChild(document.createTextNode(" " + suffixText));
  }
  function updateModeBannerUi() {
    const banner = document.getElementById("modeBanner");
    const status = document.getElementById("modeBannerStatus");
    const subtitle = document.getElementById("modeBannerSubtitle");
    const takeoverBtn = document.getElementById("modeBannerTakeoverBtn");
    if (!banner) return;

    // En mode local : bandeau masqué — comportement historique préservé
    const inServerMode = isServerModeUnlocked();
    if (!inServerMode) {
      banner.style.display = "none";
      _stopModeBannerCountdown();
      return;
    }
    banner.style.display = "";

    const ELM = window.CSI_EditLockManager;
    const heldByOther = !!(ELM && ELM.isHeldByOther && ELM.isHeldByOther());
    const editing = !isReadOnlyMode();

    banner.classList.remove("mode-banner--readonly", "mode-banner--edit", "mode-banner--locked-by-other");

    if (heldByOther) {
      banner.classList.add("mode-banner--locked-by-other");
      const holder = ELM.getCurrentHolder() || {};
      const holderName = holder.userName || holder.userId || "?";
      const secondsLeft = Math.max(0, parseInt(holder.secondsRemaining, 10) || 0);

      // === CSI-CO === co-édition : le bandeau parle du MODULE affiché (« Module Pilotage —
      // édité par Alice »), jamais du système. Hors co-édition : libellé historique inchangé.
      var _coLockLead = t('modeBanner.editingByOther', 'Édité par') + " ";
      if (ELM.isCoedit && ELM.isCoedit() && ELM.clusterForTab) {
        _coLockLead = t('modeBanner.moduleEditedBy', 'Module {{module}} — édité par')
          .replace('{{module}}', _csiClusterLabel(ELM.clusterForTab(_currentActiveTab))) + " ";
      }
      if (status) _setBannerStatusDom(status, "fas fa-user-pen", _coLockLead, holderName);
      if (subtitle) {
        if (secondsLeft > 0) {
          _setBannerCountdownDom(subtitle, secondsLeft, t('modeBanner.untilExpiry', "avant expiration du verrou"));
        } else {
          subtitle.textContent = t('modeBanner.lockExpiringSoon', "verrou en attente d'expiration");
        }
      }
      if (takeoverBtn) {
        const isAdmin = !!(window.CSI_AuthManager && window.CSI_AuthManager.isAdmin && window.CSI_AuthManager.isAdmin());
        takeoverBtn.style.display = isAdmin ? "" : "none";
      }
      // Compte à rebours rafraîchi toutes les secondes côté client (la valeur de référence
      // est rafraîchie toutes les ~30s par le presence-ping)
      if (!_modeBannerCountdownTimer) {
        _modeBannerCountdownTimer = setInterval(function () {
          if (!ELM || !ELM.isHeldByOther || !ELM.isHeldByOther()) {
            _stopModeBannerCountdown();
            updateModeBannerUi();
            return;
          }
          const h = ELM.getCurrentHolder() || {};
          if (typeof h.secondsRemaining === 'number' && h.secondsRemaining > 0) {
            h.secondsRemaining = Math.max(0, h.secondsRemaining - 1);
          }
          const sub = document.getElementById("modeBannerSubtitle");
          if (sub) {
            const left = parseInt(h.secondsRemaining, 10) || 0;
            if (left > 0) {
              _setBannerCountdownDom(sub, left, t('modeBanner.untilExpiry', "avant expiration du verrou"));
            } else {
              sub.textContent = t('modeBanner.lockExpiringSoon', "verrou en attente d'expiration");
            }
          }
        }, 1000);
      }
    } else if (editing) {
      banner.classList.add("mode-banner--edit");
      _stopModeBannerCountdown();
      if (status) _setBannerStatusDom(status, "fas fa-pen", t('modeBanner.editingByYou', 'Mode édition actif'), null);
      // === CSI-CO === co-édition : préciser le module éditable de l'écran courant.
      // Onglet NEUTRE (dashboard/settings, INV-C11) : sous-titre historique — aucun module
      // n'y est verrouillé, afficher un nom de grappe serait mensonger.
      if (subtitle) {
        if (ELM && ELM.isCoedit && ELM.isCoedit() && ELM.clusterForTab
            && !(ELM.isNeutralTab && ELM.isNeutralTab(_currentActiveTab))) {
          subtitle.textContent = t('modeBanner.editingModule', 'Module : {{module}}')
            .replace('{{module}}', _csiClusterLabel(ELM.clusterForTab(_currentActiveTab)));
        } else {
          subtitle.textContent = t('header.editModeSubtitle');
        }
      }
      if (takeoverBtn) takeoverBtn.style.display = "none";
    } else {
      banner.classList.add("mode-banner--readonly");
      _stopModeBannerCountdown();
      if (status) _setBannerStatusDom(status, "fas fa-lock", t('header.readOnlyMode'), null);
      if (subtitle) subtitle.textContent = "";
      if (takeoverBtn) takeoverBtn.style.display = "none";
    }
  }
  // Refonte header 2026-07 : le bouton porte une icône <i> + un <span> libellé (le span
  // est masqué en palier étroit ≤1120px → bouton icône seule). Cette aide pose le libellé
  // sans écraser l'icône, gère data-i18n (sur le span, pour translatePage) et pose
  // aria-label + title (title seulement quand le libellé visuel est masqué, ou déjà fourni).
  function _setEditModeBtnFace(btn, iconClass, label, i18nKey) {
    var iconEl = btn.querySelector("i");
    if (iconEl) iconEl.className = "fas " + iconClass;
    var span = btn.querySelector("span");
    if (span) {
      span.textContent = label;
      if (i18nKey) span.setAttribute("data-i18n", i18nKey);
      else span.removeAttribute("data-i18n");
    } else {
      btn.textContent = label;
      if (i18nKey) btn.setAttribute("data-i18n", i18nKey);
      else btn.removeAttribute("data-i18n");
    }
    btn.setAttribute("aria-label", label);
    var narrow = false;
    try { narrow = !!(window.matchMedia && window.matchMedia("(max-width: 1120px)").matches); } catch (e) { /* file:// vieux moteurs */ }
    if (!btn.title && narrow) btn.title = label;
  }
  function updateEditModeButtonUi() {
    const btn = document.getElementById("editModeBtn");
    if (!btn) return;
    // Viewer role: permanently disabled read-only button
    if (window.CSI_AuthManager && window.CSI_AuthManager.isViewer()) {
      btn.classList.remove("btn--danger", "btn--success", "btn--primary", "btn--warning");
      btn.classList.add("btn--outline");
      btn.disabled = true;
      btn.title = t('header.viewerReadOnly', 'Votre rôle est Lecteur — mode lecture seule permanent.');
      btn.setAttribute("aria-pressed", "false");
      _setEditModeBtnFace(btn, "fa-eye", t('header.readOnlyMode', 'Mode lecture seule'), 'header.readOnlyMode');
      return;
    }

    // Mode serveur : si le verrou d'édition est détenu par un autre utilisateur
    const ELM = window.CSI_EditLockManager;
    const heldByOther = isServerModeUnlocked() && ELM && ELM.isHeldByOther && ELM.isHeldByOther();
    if (heldByOther) {
      const holder = ELM.getCurrentHolder() || {};
      const holderName = holder.userName || holder.userId || "?";
      btn.classList.remove("btn--danger", "btn--success", "btn--primary");
      btn.classList.add("btn--outline");
      // === CSI-CO === co-édition : bouton CLIQUABLE — le clic retente l'acquisition réelle
      // (423 frais nominatif si toujours occupé) au lieu d'attendre le prochain presence-ping
      // (jusqu'à 30 s). Hors co-édition : désactivé, comportement historique inchangé.
      var _coClickable = !!(ELM.isCoedit && ELM.isCoedit());
      btn.disabled = !_coClickable;
      btn.title = t('modeBanner.editingByOther', 'Édité par') + " " + holderName
        + (_coClickable ? ' — ' + t('modeBanner.clickToRetry', 'cliquez pour réessayer') : '');
      btn.setAttribute("aria-pressed", "false");
      _setEditModeBtnFace(btn, "fa-lock", t('modeBanner.lockedShort', 'Verrouillé'), null);
      return;
    }

    btn.disabled = false;
    btn.title = "";
    if (isReadOnlyMode()) {
      btn.classList.remove("btn--danger", "btn--primary", "btn--outline", "btn--warning");
      btn.classList.add("btn--success");
      btn.setAttribute("aria-pressed", "false");
      _setEditModeBtnFace(btn, "fa-pen", t('header.editMode'), 'header.editMode');
    } else {
      btn.classList.remove("btn--success", "btn--primary", "btn--outline", "btn--warning");
      btn.classList.add("btn--danger");
      btn.setAttribute("aria-pressed", "true");
      _setEditModeBtnFace(btn, "fa-xmark", t('header.exitEditMode'), 'header.exitEditMode');
    }
  }
  function setEditMode(enabled, options = {}) {
    // Defense in depth: viewers can never enter edit mode
    if (enabled && window.CSI_AuthManager && window.CSI_AuthManager.isViewer()) {
      enabled = false;
    }
    // === CSI-ME === (Lot 3c, anti-confusion §11.3) : première entrée en édition
    // après une bascule de périmètre → confirmation explicite (le bandeau seul est
    // insuffisant — cécité d'accoutumance). Une fois confirmé, plus redemandé
    // jusqu'à la prochaine bascule.
    if (enabled && window._csiConfirmScopeEdit) {
      // (audit, mineur) Ne demander confirmation que si on est TOUJOURS sur une
      // sous-entité : un re-ingest (/me, refresh) a pu remettre le périmètre au
      // principal (_activeScope=null) sans nettoyer ce drapeau → sinon confirm parasite.
      var _emEdit = window.CSI_EntityManager;
      if (_emEdit && _emEdit.activeScope && _emEdit.activeScope()) {
        var _scopeName = window._csiConfirmScopeEdit;
        if (!window.confirm(t('entities.confirmScopeEdit', 'Vous êtes sur le périmètre « {{name}} ».\n\nActiver le mode édition sur CE périmètre ?').replace('{{name}}', _scopeName))) {
          return;
        }
      }
      window._csiConfirmScopeEdit = null;
    }
    editModeEnabled = !!enabled;
    refreshReadOnlyLocks();
    updateModeBannerUi();
    updateEditModeButtonUi();
    if (typeof renderNis2Plan === "function") {
      try {
        renderNis2Plan();
      } catch (e) {
        void 0;
      }
    }
    // Resynchronise l'onglet Budget Cybersécurité (boutons edit/suppr. selon read-only)
    // UNIQUEMENT s'il est l'onglet actif — sinon on reconstruirait inutilement deux
    // graphiques Chart.js à chaque bascule de mode depuis n'importe quel module.
    var _budgetModule = document.getElementById("nis2-program");
    if (typeof renderBudgetTab === "function" && _budgetModule && _budgetModule.classList.contains("module--active")) {
      try {
        renderBudgetTab();
      } catch (e) {
        void 0;
      }
    }
    if (options.silent) return;
    showToast2(enabled ? t('toasts.editModeEnabled', 'Mode édition activé') : t('toasts.editModeDisabled', 'Mode lecture seule activé'), enabled ? "success" : "info");
  }
  function shouldAutoUnlockForTests() {
    return isLocalhost() && !!(typeof navigator !== "undefined" && navigator.webdriver);
  }

  // Modale sécurisée pour la saisie du mot de passe (masqué)
  function showPasswordModal(title, message) {
    return new Promise((resolve) => {
      const overlay = document.getElementById("modalOverlay");
      const modalTitle = document.getElementById("modalTitle");
      const modalBody = document.getElementById("modalBody");
      const saveBtn = document.getElementById("modalSave");
      const cancelBtn = document.getElementById("modalCancel");
      const closeBtn = document.getElementById("modalClose");
      const deleteBtn = document.getElementById("modalDelete");

      if (!overlay || !modalTitle || !modalBody || !saveBtn || !cancelBtn) {
        resolve(null);
        return;
      }

      modalTitle.textContent = title;
      modalBody.innerHTML = "";

      // Message d'instruction
      if (message) {
        const messageEl = document.createElement("p");
        messageEl.className = "modal__message";
        messageEl.textContent = message;
        modalBody.appendChild(messageEl);
      }

      // Champ mot de passe
      const label = document.createElement("label");
      label.textContent = t('header.passwordLabel');
      label.htmlFor = "modal-password-input";
      label.className = "modal__label";

      const inputWrapper = document.createElement("div");
      inputWrapper.className = "password-input-wrapper";
      inputWrapper.style.cssText = "position: relative; display: flex; align-items: center;";

      const input = document.createElement("input");
      input.type = "password";
      input.id = "modal-password-input";
      input.className = "form-control";
      input.autocomplete = "current-password";
      input.placeholder = t('header.passwordLabel');
      input.style.paddingRight = "2.5rem";

      // Bouton pour afficher/masquer le mot de passe
      const toggleBtn = document.createElement("button");
      toggleBtn.type = "button";
      toggleBtn.className = "password-toggle-btn";
      toggleBtn.setAttribute("aria-label", "Afficher le mot de passe");
      toggleBtn.style.cssText = "position: absolute; right: 0.5rem; background: none; border: none; cursor: pointer; padding: 0.25rem; color: var(--color-text-muted, #6c757d); display: flex; align-items: center; justify-content: center;";
      toggleBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>';

      toggleBtn.addEventListener("click", () => {
        const isPassword = input.type === "password";
        input.type = isPassword ? "text" : "password";
        toggleBtn.setAttribute("aria-label", isPassword ? "Masquer le mot de passe" : "Afficher le mot de passe");
        toggleBtn.innerHTML = isPassword
          ? '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>'
          : '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
        input.focus();
      });

      inputWrapper.appendChild(input);
      inputWrapper.appendChild(toggleBtn);

      modalBody.appendChild(label);
      modalBody.appendChild(inputWrapper);

      // Configuration des boutons
      saveBtn.textContent = t('header.validate');
      saveBtn.disabled = false;
      cancelBtn.textContent = t('common.cancel');
      cancelBtn.style.display = "";
      if (closeBtn) closeBtn.style.display = "";
      if (deleteBtn) deleteBtn.style.display = "none";

      // Fermer la modale
      function close(result) {
        overlay.classList.remove("modal-overlay--visible");
        document.body.style.overflow = '';
        overlay.onclick = null;
        modalTitle.textContent = "";
        modalBody.innerHTML = "";
        saveBtn.replaceWith(saveBtn.cloneNode(true));
        cancelBtn.replaceWith(cancelBtn.cloneNode(true));
        if (closeBtn) closeBtn.replaceWith(closeBtn.cloneNode(true));
        if (deleteBtn) deleteBtn.style.display = '';
        resolve(result);
      }

      // Événements
      document.getElementById("modalSave").addEventListener("click", () => {
        close(input.value || null);
      });

      document.getElementById("modalCancel").addEventListener("click", () => {
        close(null);
      });

      if (closeBtn) {
        document.getElementById("modalClose").addEventListener("click", () => {
          close(null);
        });
      }

      // Entrée pour valider
      input.addEventListener("keydown", (e) => {
        if (e.key === "Enter") {
          e.preventDefault();
          close(input.value || null);
        } else if (e.key === "Escape") {
          e.preventDefault();
          close(null);
        }
      });

      // Cleanup any stale keydown handler from a previous openModal
      if (_modalKeydownHandler && overlay) {
        overlay.removeEventListener("keydown", _modalKeydownHandler);
        _modalKeydownHandler = null;
      }

      // Afficher la modale
      overlay.classList.add("modal-overlay--visible");

      // Focus sur le champ après ouverture
      setTimeout(() => input.focus(), 100);
    });
  }

  async function requireEditUnlock() {
    // Mode DEMO: pas de mot de passe requis
    if (DEMO_MODE) {
      editPasswordUnlocked = true;
      setEditMode(true, { silent: true });
      return true;
    }
    if (shouldAutoUnlockForTests()) {
      editPasswordUnlocked = true;
      setEditMode(true, { silent: true });
      return true;
    }
    if (editPasswordUnlocked) return true;
    // Server mode with JWT: JWT auth is sufficient, skip local password
    if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn()) {
      editPasswordUnlocked = true;
      return true;
    }
    if (typeof document === "undefined") {
      showReadOnlyNotice();
      return false;
    }
    const input = await showPasswordModal(t('header.authRequired'), t('header.editPasswordPrompt'));
    if (!input) {
      showReadOnlyNotice();
      return false;
    }
    const ok = await verifyEditPassword(input);
    if (!ok) {
      showToast2(t('toasts.wrongPassword'), "error");
      return false;
    }
    editPasswordUnlocked = true;
    const syncPwdInput = document.getElementById("serverSyncPassword");
    if (syncPwdInput) syncPwdInput.value = input;
    // Warn if using factory default password
    if (verifyEditPassword._lastMatchedFactory) {
      setTimeout(function() {
        showToast2(t('toasts.factoryPasswordWarning') || "Vous utilisez le mot de passe par d\u00e9faut. Veuillez le changer dans les param\u00e8tres de s\u00e9curit\u00e9.", "warning");
      }, 500);
    }
    return true;
  }
  function ensureEditMode(actionLabel = "edit") {
    if (!isReadOnlyMode()) return true;
    showReadOnlyNotice(actionLabel);
    return false;
  }
  function initReadOnlyBanner() {
    EDIT_LOCK_IDS.forEach((id) => {
      const el = document.getElementById(id);
      if (el) el.dataset.readonlyLock = "true";
    });
    // En mode DEMO : édition active par défaut (pas de mot de passe)
    // Sinon : démarrage par défaut en mode lecture seule
    if (DEMO_MODE) {
      setEditMode(true, { silent: true });
    } else {
      setEditMode(false, { silent: true });
    }
    updateModeBannerUi();
    updateEditModeButtonUi();
  }
  function showCorporateLocalLicenseWall() {
    var existing = document.getElementById('csiLicenseWall');
    if (existing) return;

    var _t = window.CSI_i18n ? window.CSI_i18n.t.bind(window.CSI_i18n) : function(k, fb) { return fb || k; };

    var overlay = document.createElement('div');
    overlay.id = 'csiLicenseWall';
    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:999999;';

    var modal = document.createElement('div');
    modal.style.cssText = 'background:var(--color-surface,#fff);border-radius:12px;padding:32px;max-width:500px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.3);text-align:center;';

    var iconDiv = document.createElement('div');
    iconDiv.style.cssText = 'font-size:48px;margin-bottom:16px;color:var(--color-danger,#dc3545);';
    var iconEl = document.createElement('i');
    iconEl.className = 'fas fa-lock';
    iconDiv.appendChild(iconEl);

    var title = document.createElement('h2');
    title.style.cssText = 'margin:0 0 8px 0;color:var(--color-text,#333);';
    title.textContent = _t('license.wallTitle', 'Période d\'essai terminée');

    var desc = document.createElement('p');
    desc.style.cssText = 'margin:0 0 20px 0;color:var(--color-text-secondary,#666);';
    desc.textContent = _t('license.wallDesc', 'Veuillez entrer votre clé de licence CSI2-... pour continuer à utiliser l\'application.');

    var input = document.createElement('textarea');
    input.id = 'csiLicenseWallKey';
    input.placeholder = 'CSI2-...';
    input.style.cssText = 'width:100%;min-height:80px;padding:12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-family:monospace;font-size:0.85rem;resize:vertical;box-sizing:border-box;';

    var msg = document.createElement('div');
    msg.id = 'csiLicenseWallMsg';
    msg.style.cssText = 'margin-bottom:12px;font-size:0.9rem;display:none;padding:8px;border-radius:6px;';

    var btn = document.createElement('button');
    btn.style.cssText = 'padding:12px 24px;border:none;background:var(--color-primary,#3b82f6);color:white;border-radius:6px;cursor:pointer;font-size:1rem;font-weight:600;width:100%;';
    btn.textContent = _t('license.activate', 'Activer la licence');

    btn.addEventListener('click', function() {
        var key = (input.value || '').trim();
        if (!key || key.indexOf('CSI2-') !== 0) {
            msg.style.display = 'block';
            msg.style.background = 'var(--color-danger-bg,#fef2f2)';
            msg.style.color = 'var(--color-danger,#dc3545)';
            msg.textContent = _t('license.error', 'Clé de licence invalide');
            return;
        }
        btn.disabled = true;
        btn.textContent = '...';
        msg.style.display = 'none';

        var TM = window.CSI_TrialManager;
        if (TM) {
            TM.activate(key).then(function(result) {
                msg.style.display = 'block';
                msg.style.background = 'var(--color-success-bg,#f0fdf4)';
                msg.style.color = 'var(--color-success,#22c55e)';
                msg.textContent = _t('license.success', 'Licence activée !') + (result.organization ? ' — ' + result.organization : '');
                setTimeout(function() { location.reload(); }, 1500);
            }).catch(function(err) {
                btn.disabled = false;
                btn.textContent = _t('license.activate', 'Activer la licence');
                msg.style.display = 'block';
                msg.style.background = 'var(--color-danger-bg,#fef2f2)';
                msg.style.color = 'var(--color-danger,#dc3545)';
                msg.textContent = err.message || _t('license.error', 'Clé de licence invalide');
            });
        }
    });

    modal.appendChild(iconDiv);
    modal.appendChild(title);
    modal.appendChild(desc);
    modal.appendChild(input);
    modal.appendChild(msg);
    modal.appendChild(btn);
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    setTimeout(function() { input.focus(); }, 100);
  }

  function initTrialBanner() {
    var TM = window.CSI_TrialManager;
    var LTM = window.CSI_LocalTrialManager;
    var ed = window._csiEdition;
    var isCorporateLocal = ed && ed.id === 'corporate-local';

    // Edition sans trial/licence (Community) → pas de bannière
    if (ed && !ed.trial && !ed.license) return;

    // Déterminer le manager à interroger
    var manager = isCorporateLocal ? LTM : TM;
    if (!manager || !manager.isDetected()) return;

    // Remove existing banner if any
    var existing = document.getElementById('csiTrialBanner');
    if (existing) existing.remove();

    if (manager.isLicensed() && !manager.isLicenseExpired()) return;

    // Mur de licence complet pour Corporate Local expiré
    if (isCorporateLocal && manager.needsLicense()) {
        showCorporateLocalLicenseWall();
        return;
    }

    var _t = window.CSI_i18n ? window.CSI_i18n.t.bind(window.CSI_i18n) : function(k, fb) { return fb || k; };
    var banner = document.createElement('div');
    banner.id = 'csiTrialBanner';

    if (manager.isTrialActive()) {
      banner.className = 'csi-trial-banner csi-trial-banner--active';
      var icon = document.createElement('i');
      icon.className = 'fas fa-clock';
      icon.style.marginRight = '6px';
      banner.appendChild(icon);
      banner.appendChild(document.createTextNode(
        _t('license.trialBannerText', 'Essai gratuit \u2014 {days} jours restants sur 14').replace('{days}', manager.getDaysLeft())
      ));
      var link = document.createElement('a');
      link.href = '#';
      link.textContent = ' | ' + _t('license.pasteKey', 'Entrer une clé');
      link.style.cssText = 'color:inherit;text-decoration:underline;cursor:pointer;margin-left:4px;';
      link.addEventListener('click', function(e) {
        e.preventDefault();
        if (window.CSI_LicenseManager) window.CSI_LicenseManager.showActivationModal();
      });
      banner.appendChild(link);
    } else if (manager.isTrialExpired() || manager.isLicenseExpired()) {
      banner.className = 'csi-trial-banner csi-trial-banner--expired';
      banner.textContent = manager.isLicenseExpired()
        ? _t('license.renewNeeded', 'Licence expirée — entrez une nouvelle clé pour réactiver')
        : _t('license.readOnlyNotice', 'Mode lecture seule — entrez une clé de licence pour réactiver');
      var link2 = document.createElement('a');
      link2.href = '#';
      link2.textContent = ' ' + _t('license.activate', 'Activer');
      link2.style.cssText = 'color:inherit;text-decoration:underline;cursor:pointer;margin-left:4px;font-weight:600;';
      link2.addEventListener('click', function(e) {
        e.preventDefault();
        if (window.CSI_LicenseManager) window.CSI_LicenseManager.showActivationModal({ required: true });
      });
      banner.appendChild(link2);
    } else {
      return; // No trial state to show
    }

    document.body.insertBefore(banner, document.body.firstChild);
    // Fallback for browsers without :has() support (e.g., Firefox <121)
    document.body.classList.add('has-trial-banner');
  }

  async function handleEditModeButton() {
    if (isReadOnlyMode()) {
      // Viewer role: read-only — block edit mode with feedback
      if (window.CSI_AuthManager && window.CSI_AuthManager.isViewer()) {
        showToast2(t('header.viewerReadOnly', 'Votre rôle est Lecteur — mode lecture seule permanent.'), 'warning');
        return;
      }
      // En mode serveur, l'utilisateur est déjà authentifié via le login wall
      if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn()) {
        // Tenter d'acquérir le verrou d'édition global avant de basculer en mode édition.
        // Si refus (verrou détenu par un autre), afficher un toast et ne pas changer d'état.
        const ELM = window.CSI_EditLockManager;
        // === CSI-CO === (INV-C11) entrer en édition depuis un onglet NEUTRE (dashboard,
        // settings) n'acquiert AUCUNE grappe — rien n'y est éditable ; l'acquisition se fait
        // à la première navigation vers un vrai module (_csiEnsureClusterForTab).
        var _coNeutralEntry = !!(ELM && ELM.isCoedit && ELM.isCoedit()
          && ELM.isNeutralTab && ELM.isNeutralTab(_currentActiveTab));
        if (ELM && ELM.acquire && !_coNeutralEntry) {
          try {
            // === CSI-CO === co-édition par grappes : acquérir la grappe de l'onglet AFFICHÉ
            // (les autres modules restent disponibles pour les collègues). Hors co-édition :
            // acquire() sans argument = verrou unique historique, comportement inchangé.
            var _coClusters = (ELM.isCoedit && ELM.isCoedit() && ELM.clusterForTab)
              ? [ELM.clusterForTab(_currentActiveTab)] : undefined;
            const result = await ELM.acquire(_coClusters);
            if (!result || !result.granted) {
              const holder = result && result.lock ? result.lock : null;
              const holderName = holder ? (holder.userName || holder.userId || '?') : '?';
              showToast2(t('modeBanner.acquireFailed', 'Édition impossible : {{name}} édite déjà').replace('{{name}}', holderName), 'warning');
              updateEditModeButtonUi();
              updateModeBannerUi();
              return;
            }
            window._csiClusterReadOnly = false; // === CSI-CO === grappe de l'écran acquise
          } catch (e) {
            void 0;
            // FIX-LOCK-ACQUIRE-FAIL (audit 2026-06-10, bundle.js:5637) : ne JAMAIS entrer
            // en édition sans verrou confirmé. L'ancienne « dégradation gracieuse »
            // permettait l'édition concurrente silencieuse (et, avec EDIT_LOCK_ENFORCED,
            // toutes les écritures seraient refusées en 423).
            showToast2(t('modeBanner.acquireNetworkError', "Impossible de vérifier le verrou d'édition (problème réseau). Réessayez."), 'error');
            return;
          }
        }
        // FIX-EDITLOCK-REPULL (audit 2026-07-02, C1) : re-puller l'état serveur autoritaire
        // À L'ACQUISITION du verrou, AVANT toute édition. Sans ce refresh, un client resté
        // ouvert avec des données périmées (ex. un collègue connecté AVANT un import réalisé
        // par un autre admin) poussait ensuite ses vieilles données par-dessus l'état frais
        // → écrasement silencieux (aucun 409 : après un import les tab_*.json sont purgés, la
        // révision est inconnue → data-push legacy accepté). On recharge via loadData() (même
        // normalisation que le boot) puis on re-rend l'onglet courant. Best-effort : en cas
        // d'échec réseau on continue (comportement pré-fix), le verrou étant déjà détenu.
        try {
          if (typeof loadData === 'function') {
            // B2 : sauvegarder les modifs non confirmées (push refusé en 423) AVANT
            // que loadData() ne remplace appData par l'état serveur autoritaire.
            var _bkCount = 0;
            try { _bkCount = _csiBackupDirtyTabs('editlock-repull'); }
            catch (e0) { void 0; }
            await loadData();
            if (typeof _csiResetTabRevisions === 'function') _csiResetTabRevisions();
            if (_bkCount > 0) {
              // Les onglets sauvegardés ont été remplacés par l'état serveur : purger
              // leurs marqueurs dirty (le fichier de secours conserve la version locale).
              try {
                _csiDirtyTabs = {}; _pendingPushTabs = {};
                if (typeof _updateUnsavedFlag === 'function') _updateUnsavedFlag();
              } catch (e1) { /* best-effort */ }
              showToast2(t('toasts.dirtyBackedUpBeforeRepull',
                '{{n}} modification(s) non enregistrée(s) ont été sauvegardées dans un fichier de secours avant le rechargement de l\'état du serveur.').replace('{{n}}', _bkCount),
                'warning', 12000);
            }
            if (typeof updateDashboard === 'function') updateDashboard();
            var _reCur = (typeof _currentActiveTab !== 'undefined' && _currentActiveTab) ? _currentActiveTab : 'dashboard';
            if (typeof loadModuleContent === 'function') loadModuleContent(_reCur);
          }
        } catch (e) {
          void 0;
        }
        setEditMode(true);
        return;
      }
      // Mode local: mot de passe édition classique
      const ok = await requireEditUnlock();
      if (!ok) return;
      setEditMode(true);
    } else {
      // Quitter le mode édition → sauvegarder les données puis libérer le verrou
      try { saveData(); } catch(e) { void 0; }
      window._csiClusterReadOnly = false; // === CSI-CO === plus d'édition → plus d'override
      setEditMode(false);
      const ELM = window.CSI_EditLockManager;
      if (ELM && ELM.release && isServerModeUnlocked()) {
        // FIX-LOCK-RELEASE-RACE (audit 2026-06-10, bundle.js:12732) : attendre la fin des
        // push en vol/en attente AVANT de libérer le verrou — sinon, verrou appliqué
        // (EDIT_LOCK_ENFORCED), le push du saveData ci-dessus arriverait après le release
        // et serait refusé en 423.
        try {
          if (typeof _flushServerPushes === 'function') await _flushServerPushes(15000);
        } catch (e) { /* best effort */ }
        try { await ELM.release(); } catch (e) { /* best effort */ }
        updateModeBannerUi();
        updateEditModeButtonUi();
      }
    }
  }

  /**
   * Reprise admin du verrou d'édition (modal de confirmation).
   * Appelé depuis le bouton #modeBannerTakeoverBtn.
   */
  async function handleEditLockTakeover() {
    const ELM = window.CSI_EditLockManager;
    if (!ELM || !ELM.takeover) return;
    if (!window.CSI_AuthManager || !window.CSI_AuthManager.isAdmin || !window.CSI_AuthManager.isAdmin()) {
      showToast2(t('modeBanner.takeoverDenied', 'Action réservée aux administrateurs'), 'warning');
      return;
    }
    const holder = ELM.getCurrentHolder() || {};
    const holderName = holder.userName || holder.userId || '?';
    const msg = t('modeBanner.takeoverConfirm', 'Forcer la libération du verrou détenu par {{name}} ? Ses modifications non enregistrées seront perdues.').replace('{{name}}', holderName);
    if (!window.confirm(msg)) return;

    try {
      // === CSI-CO === co-édition : reprendre la grappe du module AFFICHÉ (pas tout le SI).
      var _coTkClusters = (ELM.isCoedit && ELM.isCoedit() && ELM.clusterForTab)
        ? [ELM.clusterForTab(_currentActiveTab)] : undefined;
      const result = await ELM.takeover(_coTkClusters);
      if (result && result.granted) {
        showToast2(t('modeBanner.takeoverSuccess', 'Verrou repris avec succès'), 'success');
        window._csiClusterReadOnly = false; // === CSI-CO ===
        setEditMode(true);
      } else {
        showToast2(t('modeBanner.takeoverFailed', 'Impossible de reprendre le verrou'), 'error');
      }
    } catch (e) {
      void 0;
      showToast2(t('modeBanner.takeoverFailed', 'Impossible de reprendre le verrou'), 'error');
    }
    updateEditModeButtonUi();
    updateModeBannerUi();
  }
  // ============================================================
  // Système de licence serveur
  // ============================================================

  // LIC-003: Legacy HMAC secret key removed — only ECDSA v2.0 accepted

  // Clé publique ECDSA P-256 pour la validation des licences v2.0
  // Seule la clé publique est présente côté client (vérification uniquement)
  const LICENSE_ECDSA_PUBLIC_KEY = {
    kty: "EC",
    x: "mBme0rIcZa0ihwS-oxYuUr9KJX_4_U0C0GpW9ViO2wY",
    y: "i2kRXa86VJvwg191sJetv1W6uYldDnIKlIl13c_fCt4",
    crv: "P-256"
  };

  /**
   * Vérifie une signature ECDSA P-256 via Web Crypto API (version 2.0)
   * @param {string} content - Le contenu signé
   * @param {string} signatureHex - Signature en hexadécimal
   * @param {Object} publicKeyJwk - Clé publique au format JWK
   * @returns {Promise<boolean>}
   */
  async function ecdsaVerify(content, signatureHex, publicKeyJwk) {
    const encoder = new TextEncoder();
    const data = encoder.encode(content);
    const sigBytes = new Uint8Array(signatureHex.match(/.{1,2}/g).map(b => parseInt(b, 16)));

    const key = await crypto.subtle.importKey(
      'jwk',
      publicKeyJwk,
      { name: 'ECDSA', namedCurve: 'P-256' },
      false,
      ['verify']
    );

    return crypto.subtle.verify(
      { name: 'ECDSA', hash: 'SHA-256' },
      key,
      sigBytes,
      data
    );
  }

  /**
   * Valide la signature d'un fichier de licence
   * LIC-003: Only ECDSA P-256 v2.0 accepted (legacy HMAC paths removed)
   * @param {Object} licenseData - Données de la licence
   * @param {string} signature - Signature à vérifier
   * @param {Object} meta - Métadonnées de la licence (optionnel)
   * @returns {Promise<boolean>}
   */
  async function validateLicenseSignature(licenseData, signature, meta) {
    try {
      // Reconstruit le contenu signé (ordre déterministe)
      const content = JSON.stringify({
        type: licenseData.type,
        organization: licenseData.organization,
        issuedAt: licenseData.issuedAt,
        expiresAt: licenseData.expiresAt || null,
        features: licenseData.features || ['server-sync']
      });

      // LIC-003: Only accept ECDSA P-256 signatures
      const signatureType = meta && meta.signatureType;
      const version = meta && meta.version;
      if (signatureType !== 'ecdsa-p256' && version !== '2.0') {
        void 0;
        return false;
      }

      return await ecdsaVerify(content, signature, LICENSE_ECDSA_PUBLIC_KEY);
    } catch (e) {
      void 0;
      return false;
    }
  }

  /**
   * Vérifie si une licence est expirée
   * @param {Object} licenseData
   * @returns {boolean}
   */
  function isLicenseExpired(licenseData) {
    if (!licenseData.expiresAt) return false; // Licence permanente
    return new Date(licenseData.expiresAt) < new Date();
  }

  /**
   * Vérifie si le mode serveur est débloqué (licence valide)
   * Interroge CSI_LicenseManager d'abord, fallback localStorage legacy
   * @returns {boolean}
   */
  function isServerModeUnlocked() {
    // Garde: en file:// le mode serveur est impossible par définition.
    // Sans ce guard, un localStorage legacy (smsi_server_license_activated)
    // pouvait faire passer le boot dans la branche serveur → AuthManager stub
    // appelle showLoginModal(cb=false) → Promise jamais résolue → spinner figé.
    if (window.location.protocol === 'file:') return false;

    // Source 1 : licence serveur auto-détectée depuis config/license.json
    var LM = window.CSI_LicenseManager;
    if (LM && LM.isDetected() && LM.isServerMode()) return true;

    // [SUPPRIMÉ — FIX-CORPLOCAL-HTTP, audit 2026-06-10] Source 2 : Corporate Local avec
    // licence active. Cette édition est définie avec serverMode:false et son package ne
    // contient AUCUN backend PHP : la branche ne pouvait jamais se déclencher en file://
    // (guard ci-dessus) et, servie en http(s)/localhost (contournement documenté du
    // problème Firefox+file://), elle enfermait l'application derrière un login JWT
    // pointant vers un users.php inexistant (modal de setup non fermable, spinner figé,
    // données localStorage inaccessibles).
    var ed = window._csiEdition;

    // Source 3 : Corporate Server (edition.js)
    if (ed && ed.id === 'corporate-server') return true;

    // Fallback legacy : licence dans localStorage (sera retiré après Phase 4)
    var Storage = window.CSI_Storage;
    if (!Storage) return false;
    if (!Storage.isServerLicenseActivated()) return false;
    var license = Storage.getServerLicense();
    if (!license) return false;
    if (isLicenseExpired(license)) {
      Storage.deactivateServerLicense();
      return false;
    }
    return true;
  }

  // [REMOVED] showLicenseUploadModal — licence désormais auto-détectée via config/license.json
  // [REMOVED] handleServerSyncButton — sync panel supprimé, remplacé par auto-sync (Phase 3)

  // Stub pour compatibilité temporaire
  function handleServerSyncButton() {}

  // Exposer les fonctions pour usage externe
  window.isServerModeUnlocked = isServerModeUnlocked;
  window.validateLicenseSignature = validateLicenseSignature;
  window.isLicenseExpired = isLicenseExpired;

  window.refreshReadOnlyLocks = refreshReadOnlyLocks;
  window.isReadOnlyMode = isReadOnlyMode;
  window.ensureEditMode = ensureEditMode;
  window.showReadOnlyNotice = showReadOnlyNotice;
  window.handleEditModeButton = handleEditModeButton;
  window.handleServerSyncButton = handleServerSyncButton;
  window.getThemeVars = typeof getThemeVars === "function"
    ? getThemeVars
    : () => ({ textColor: "#2d3e4a", borderColor: "#e9ecef", surface: "#ffffff" });


  // modules/modal.js
  var _modalKeydownHandler = null;
  var _modalMandatory = false;
  var _previousFocusElement = null;
  function openModal(title, fields, onSave, onDelete = null, options = {}) {
    _previousFocusElement = document.activeElement;
    const overlay = document.getElementById("modalOverlay");
    // Cleanup previous keydown handler to prevent accumulation
    if (_modalKeydownHandler && overlay) {
      overlay.removeEventListener("keydown", _modalKeydownHandler);
      _modalKeydownHandler = null;
    }
    const modalTitle = document.getElementById("modalTitle");
    const modalBody = document.getElementById("modalBody");
    const saveBtn = document.getElementById("modalSave");
    const cancelBtn = document.getElementById("modalCancel");
    const closeBtn = document.getElementById("modalClose");
    const deleteBtn = document.getElementById("modalDelete");
    const allowReadOnly = options.allowReadOnly === true;
    const saveLabel = options.saveLabel || t('common.save', 'Enregistrer');
    const cancelLabel = options.cancelLabel || t('common.cancel', 'Annuler');
    const deleteLabel = options.deleteLabel || t('common.delete', 'Supprimer');
    const hideCancel = options.hideCancel === true;
    const hideClose = options.hideClose === true;
    _modalMandatory = (hideCancel && hideClose);
    const message = options.message || "";
    const lockedInitially = isReadOnlyMode() && !allowReadOnly;
    if (!overlay || !modalTitle || !modalBody || !saveBtn || !cancelBtn || !closeBtn || !deleteBtn) {
      void 0;
      return;
    }
    void 0;
    // Nettoyer les handlers onclick stale laissés par showConfirmModal
    saveBtn.onclick = null;
    cancelBtn.onclick = null;
    closeBtn.onclick = null;
    deleteBtn.onclick = null;
    modalTitle.textContent = title;
    modalBody.innerHTML = "";
    if (message) {
      const messageEl = document.createElement("p");
      messageEl.className = "modal__message";
      messageEl.textContent = message;
      modalBody.appendChild(messageEl);
    }
    fields.forEach((f) => {
      const label = document.createElement("label");
      label.textContent = f.label;
      label.htmlFor = `modal-input-${f.name}`;
      label.className = f.required ? "modal__label modal__label--required" : "modal__label";
      let input;
      let fieldWrap;
      let datalistEl = null;
      if (f.type === "textarea") {
        input = document.createElement("textarea");
        input.value = f.value != null ? f.value : "";
      } else if (f.type === "select" || f.type === "multiselect") {
        input = document.createElement("select");
        if (f.type === "multiselect") input.multiple = true;
        (f.options || []).forEach((opt) => {
          const option = document.createElement("option");
          if (typeof opt === "object") {
            option.value = opt.value;
            option.textContent = opt.label;
          } else {
            option.value = opt;
            option.textContent = opt;
          }
          const val = Array.isArray(f.value) ? f.value : [f.value];
          if (val && val.includes(option.value)) option.selected = true;
          input.appendChild(option);
        });
      } else if (f.type === "checkbox") {
        input = document.createElement("input");
        input.type = "checkbox";
        input.checked = !!f.value;
        input.className = "modal__checkbox";
        label.classList.add("modal__label--checkbox");
        fieldWrap = document.createElement("div");
        fieldWrap.className = "modal__checkbox-row";
      } else if (f.type === "tags") {
        // Tags chips field
        var tagsContainer = document.createElement("div");
        tagsContainer.className = "tags-input-container";
        tagsContainer.id = "modal-tags-wrap-" + f.name;
        input = document.createElement("input");
        input.type = "text";
        input.className = "tags-input";
        input.placeholder = f.placeholder || t('projectRisks.tagPlaceholder', 'Ajouter un tag...');
        input.setAttribute("list", "modal-tags-list-" + f.name);
        var datalist = document.createElement("datalist");
        datalist.id = "modal-tags-list-" + f.name;
        (f.suggestions || []).forEach(function(s) { var opt = document.createElement("option"); opt.value = s; datalist.appendChild(opt); });
        // Init existing tags
        var initTags = Array.isArray(f.value) ? f.value : (f.value ? String(f.value).split(",").map(function(t) { return t.trim(); }).filter(Boolean) : []);
        function addTagChip(tag, container, inp) {
          if (!tag) return;
          var existingChips = container.querySelectorAll(".tag-chip");
          for (var i = 0; i < existingChips.length; i++) {
            if (existingChips[i].getAttribute("data-tag") === tag) return;
          }
          var chip = document.createElement("span");
          chip.className = "tag-chip";
          chip.setAttribute("data-tag", tag);
          chip.innerHTML = escapeHtml(tag) + ' <button type="button" class="tag-chip__remove">&times;</button>';
          chip.querySelector(".tag-chip__remove").addEventListener("click", function(e) { e.stopPropagation(); chip.remove(); });
          container.insertBefore(chip, inp);
        }
        initTags.forEach(function(tag) { addTagChip(tag, tagsContainer, input); });
        // Lock tags in readonly mode
        var tagsLocked = (lockedInitially && !f.allowInReadOnly) || f.readOnly;
        if (tagsLocked) {
          input.disabled = true;
          tagsContainer.style.pointerEvents = "none";
          tagsContainer.style.opacity = "0.6";
        }
        input.addEventListener("keydown", function(e) {
          if (e.key === "Enter" || e.key === ",") {
            e.preventDefault();
            var val = input.value.trim().replace(/,$/,"");
            if (val) { addTagChip(val, tagsContainer, input); input.value = ""; }
          }
          if (e.key === "Backspace" && !input.value) {
            var chips = tagsContainer.querySelectorAll(".tag-chip");
            if (chips.length) chips[chips.length - 1].remove();
          }
        });
        tagsContainer.appendChild(input);
        tagsContainer.appendChild(datalist);
        tagsContainer.addEventListener("click", function() { input.focus(); });
        fieldWrap = tagsContainer;
      } else if (f.type === "searchable-select") {
        // Searchable Select: delegates to factory function buildSearchableSelectWidget
        var ssLocked = (lockedInitially && !f.allowInReadOnly) || f.readOnly;
        var ssWidget = buildSearchableSelectWidget(f.name, f.groups || [], f.value || "", f.placeholder, ssLocked);
        input = ssWidget.hiddenInput;
        modalBody.appendChild(label);
        modalBody.appendChild(ssWidget.container);
        return; // Skip normal field append logic
      } else if (f.type === "html") {
        // HTML content field (read-only display)
        var htmlContainer = document.createElement("div");
        htmlContainer.className = "modal__html-content";
        htmlContainer.id = "modal-input-" + f.name;
        htmlContainer.innerHTML = f.value || '';
        // Create a hidden input for the data collection
        input = document.createElement("input");
        input.type = "hidden";
        input.id = "modal-input-" + f.name + "-hidden";
        input.value = f.value || '';
        input.dataset.htmlField = "true";
        modalBody.appendChild(label);
        modalBody.appendChild(htmlContainer);
        modalBody.appendChild(input);
        return; // Skip the normal field append logic below
      } else {
        input = document.createElement("input");
        input.type = f.type || "text";
        input.value = f.value != null ? f.value : "";
      }
      input.dataset.readonlyLock = "true";
      if (f.readOnly) input.dataset.permanentReadonly = "true";
      const lockedField = (lockedInitially && !f.allowInReadOnly) || f.readOnly;
      if (lockedField) input.disabled = true;
      if (f.help) input.title = f.help;
      // Validation côté client
      if (f.required) input.required = true;
      if (f.minLength) input.minLength = f.minLength;
      if (f.maxLength) input.maxLength = f.maxLength;
      if (f.pattern) input.pattern = f.pattern;
      if (f.min !== undefined) input.min = f.min;
      if (f.max !== undefined) input.max = f.max;
      if (f.placeholder) input.placeholder = f.placeholder;
      if (f.suggestions && input && input.tagName === "INPUT" && input.type !== "checkbox" && input.type !== "hidden") {
        var suggestionId = "modal-input-" + f.name + "-suggestions";
        input.setAttribute("list", suggestionId);
        datalistEl = document.createElement("datalist");
        datalistEl.id = suggestionId;
        var seenSuggestion = {};
        (f.suggestions || []).forEach(function(suggestion) {
          var value = typeof suggestion === "string" ? suggestion.trim() : "";
          if (!value || seenSuggestion[value]) return;
          seenSuggestion[value] = true;
          var option = document.createElement("option");
          option.value = value;
          datalistEl.appendChild(option);
        });
      }
      input.id = `modal-input-${f.name}`;
      if (f.type === "checkbox") {
        if (fieldWrap) {
          fieldWrap.append(input, label);
          modalBody.appendChild(fieldWrap);
        }
      } else if (f.type === "tags") {
        modalBody.appendChild(label);
        modalBody.appendChild(fieldWrap);
      } else {
        input.className = "form-control";
        modalBody.appendChild(label);
        modalBody.appendChild(input);
      }
      if (datalistEl) modalBody.appendChild(datalistEl);
      // Hint d'aide sous le champ
      if (f.type !== "checkbox" && f.help) {
        const hint = document.createElement("small");
        hint.className = "form-hint";
        hint.textContent = f.help;
        modalBody.appendChild(hint);
      }
    });
    // M14 (audit 2026-07-10) : suivi d'un formulaire modifié pour éviter la perte silencieuse
    // de saisie (fiche longue : justification SoA, plan de traitement) sur un clic accidentel
    // hors-modale ou un Échap réflexe.
    var _isDirty = false;
    function close() {
      overlay.classList.remove("modal-overlay--visible");
      document.body.style.overflow = '';
      overlay.removeEventListener("keydown", handleKeydown);
      overlay.onclick = null;
      if (modalBody) { modalBody.oninput = null; modalBody.onchange = null; }
      _isDirty = false;
      _modalKeydownHandler = null;
      saveBtn.onclick = null;
      cancelBtn.onclick = null;
      closeBtn.onclick = null;
      deleteBtn.onclick = null;
    }
    // M14 : fermeture GARDÉE pour les chemins ACCIDENTELS (Échap, clic sur le fond). Si le
    // formulaire a été modifié, demander confirmation avant d'abandonner. Les boutons
    // explicites (Annuler, ✕) restent une fermeture directe (intention claire de l'utilisateur).
    function closeGuarded() {
      if (_isDirty && !window.confirm(t('common.discardConfirm', 'Abandonner les modifications non enregistrées ?'))) return;
      close();
    }
    function handleSave() {
      if (isReadOnlyMode() && !allowReadOnly) {
        showReadOnlyNotice("enregistrer");
        return;
      }
      // T3-004: Prevent double-submit
      if (saveBtn && saveBtn.disabled) return;
      if (saveBtn) saveBtn.disabled = true;
      _isDirty = false; // M14 : sauvegarde en cours → plus de garde d'abandon
      const data = {};
      fields.forEach((f) => {
        const inp = document.getElementById(`modal-input-${f.name}`);
        if (!inp) {
          data[f.name] = "";
          return;
        }
        if (f.type === "html") {
          // Read from the hidden input for html fields
          var hiddenInp = document.getElementById("modal-input-" + f.name + "-hidden");
          data[f.name] = hiddenInp ? hiddenInp.value : (inp.innerHTML || '');
          return;
        } else if (f.type === "checkbox") {
          data[f.name] = inp.checked;
        } else if (f.type === "multiselect" || (inp.tagName === "SELECT" && inp.multiple)) {
          data[f.name] = Array.from(inp.selectedOptions).map((o) => o.value);
        } else if (f.type === "tags") {
          var wrap = document.getElementById("modal-tags-wrap-" + f.name);
          var chips = wrap ? wrap.querySelectorAll(".tag-chip") : [];
          data[f.name] = Array.from(chips).map(function(c) { return c.getAttribute("data-tag"); }).filter(Boolean);
          // Also grab any text in the input
          if (inp && inp.value.trim()) {
            data[f.name].push(inp.value.trim().replace(/,$/,""));
          }
        } else {
          data[f.name] = inp.value.trim();
        }
      });
      // FIX-MODAL-REQUIRED (audit 2026-06-10) : appliquer réellement les champs required.
      // openModal posait l'attribut HTML (input.required = true) mais handleSave appelait
      // onSave sans checkValidity ni <form> : un titre/une date vidé(e) corrompait l'état
      // en mémoire et bloquait toutes les sauvegardes suivantes (validateBeforeSave).
      const missingRequired = fields.find((f) => {
        if (!f.required) return false;
        const v = data[f.name];
        if (f.type === "checkbox") return !v;
        if (Array.isArray(v)) return v.length === 0;
        return v === undefined || v === null || String(v).trim() === "";
      });
      if (missingRequired) {
        showToast2(t('toasts.requiredFields', 'Veuillez remplir les champs obligatoires.') + ' (' + missingRequired.label + ')', 'error');
        const missingInput = document.getElementById(`modal-input-${missingRequired.name}`);
        if (missingInput && typeof missingInput.focus === 'function') missingInput.focus();
        if (saveBtn) saveBtn.disabled = false;
        return;
      }
      // PASS2-307: Also check _lastSaveFailed — if saveData() failed inside onSave, don't close
      _lastSaveFailed = false;
      if (onSave(data) !== false && !_lastSaveFailed) {
        close();
      } else {
        if (saveBtn) saveBtn.disabled = false;
      }
    }
    function handleDelete() {
      if (lockedInitially && !allowReadOnly) {
        showReadOnlyNotice("delete");
        return;
      }
      close();
      if (onDelete) onDelete();
    }
    cancelBtn.textContent = cancelLabel;
    deleteBtn.textContent = deleteLabel;
    if (hideCancel) {
      cancelBtn.classList.add("hidden");
      cancelBtn.disabled = true;
    } else {
      cancelBtn.classList.remove("hidden");
      cancelBtn.disabled = false;
    }
    if (hideClose) {
      closeBtn.classList.add("hidden");
      closeBtn.disabled = true;
    } else {
      closeBtn.classList.remove("hidden");
      closeBtn.disabled = false;
    }
    if (lockedInitially) {
      saveBtn.disabled = true;
      deleteBtn.disabled = true;
      saveBtn.textContent = "Lecture seule";
    } else {
      saveBtn.disabled = false;
      saveBtn.textContent = saveLabel;
    }
    // Focus trap - garder le focus dans la modale
    function handleKeydown(e) {
      if (e.key === "Escape") {
        if (_modalMandatory) return;
        closeGuarded();
        return;
      }
      if (e.key === "Tab") {
        const modal = overlay.querySelector(".modal");
        const focusable = modal.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
        const first = focusable[0];
        const last = focusable[focusable.length - 1];
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    }
    _modalKeydownHandler = handleKeydown;
    overlay.addEventListener("keydown", handleKeydown);
    // Use .onclick to auto-replace previous handlers (prevents accumulation)
    saveBtn.onclick = handleSave;
    cancelBtn.onclick = close;
    closeBtn.onclick = close;
    if (onDelete) {
      deleteBtn.classList.remove("hidden");
      deleteBtn.style.display = '';
      deleteBtn.disabled = lockedInitially;
      deleteBtn.onclick = handleDelete;
    } else {
      deleteBtn.classList.add("hidden");
      deleteBtn.disabled = true;
      deleteBtn.onclick = null;
    }
    overlay.classList.add("modal-overlay--visible");
    document.body.style.overflow = 'hidden';
    // Click-outside-to-close: click on overlay background (not on .modal content) closes the modal
    overlay.onclick = function(evt) {
      if (evt.target === overlay) {
        closeGuarded();
      }
    };
    // M14 : marquer le formulaire « modifié » à la première saisie/changement.
    if (modalBody) {
      modalBody.oninput = function() { _isDirty = true; };
      modalBody.onchange = function() { _isDirty = true; };
    }
    // Focus sur le premier champ de formulaire
    requestAnimationFrame(() => {
      const firstInput = modalBody.querySelector("input:not([type='checkbox']), textarea, select");
      if (firstInput) {
        firstInput.focus();
      }
      // Callback afterOpen pour du contenu personnalisé
      if (typeof options.afterOpen === "function") {
        options.afterOpen();
      }
    });
  }
  function closeModal() {
    const overlay = document.getElementById("modalOverlay");
    const modalTitle = document.getElementById("modalTitle");
    const modalBody = document.getElementById("modalBody");
    const saveBtn = document.getElementById("modalSave");
    const cancelBtn = document.getElementById("modalCancel");
    const closeBtn = document.getElementById("modalClose");
    const deleteBtn = document.getElementById("modalDelete");
    if (!overlay || !modalTitle || !modalBody) return;
    overlay.classList.remove("modal-overlay--visible");
    document.body.style.overflow = '';
    overlay.onclick = null;
    _modalMandatory = false;
    // Cleanup keydown handler
    if (_modalKeydownHandler) {
      overlay.removeEventListener("keydown", _modalKeydownHandler);
      _modalKeydownHandler = null;
    }
    // Restore focus to the element that triggered the modal
    if (_previousFocusElement && typeof _previousFocusElement.focus === 'function') {
      try { _previousFocusElement.focus(); } catch(e) {}
      _previousFocusElement = null;
    }
    modalTitle.textContent = "";
    while (modalBody.firstChild) modalBody.removeChild(modalBody.firstChild);
    // Nettoyer les handlers onclick pour éviter les interférences
    if (saveBtn) saveBtn.onclick = null;
    if (cancelBtn) cancelBtn.onclick = null;
    if (closeBtn) closeBtn.onclick = null;
    if (deleteBtn) deleteBtn.onclick = null;
  }
  window.openModal = openModal;
  window.closeModal = closeModal;
  window.showPasswordModal = showPasswordModal;
  // Custom confirm modal — replaces native confirm()
  function showConfirmModal(title, message, onConfirm, options) {
    _previousFocusElement = document.activeElement;
    options = options || {};
    var type = options.type || 'warning';
    var icon = options.icon || (type === 'danger' ? 'fa-triangle-exclamation' : type === 'warning' ? 'fa-exclamation-circle' : 'fa-info-circle');
    var confirmLabel = options.confirmLabel || t('common.confirm', 'Confirmer');
    var cancelLabel = options.cancelLabel || t('common.cancel', 'Annuler');
    var overlay = document.getElementById("modalOverlay");
    var modalTitle = document.getElementById("modalTitle");
    var modalBody = document.getElementById("modalBody");
    var saveBtn = document.getElementById("modalSave");
    var cancelBtn = document.getElementById("modalCancel");
    var closeBtn = document.getElementById("modalClose");
    var deleteBtn = document.getElementById("modalDelete");
    if (!overlay || !modalTitle || !modalBody || !saveBtn || !cancelBtn) return;
    modalTitle.textContent = title;
    modalBody.innerHTML = '<div class="confirm-modal__icon confirm-modal__icon--' + type + '"><i class="fas ' + icon + '"></i></div>' +
      '<div class="confirm-modal__message">' + escapeHtml(message) + '</div>';
    saveBtn.textContent = confirmLabel;
    saveBtn.className = 'btn ' + (type === 'danger' ? 'btn--danger' : 'btn--primary');
    saveBtn.style.display = '';
    saveBtn.disabled = false;
    cancelBtn.textContent = cancelLabel;
    cancelBtn.style.display = '';
    cancelBtn.disabled = false;
    // FIX-CONFIRM-CANCEL-HIDDEN (audit 2026-06-10) : openModal({hideCancel/hideClose})
    // pose la classe 'hidden' (display:none) sur ces boutons partagés ; le style inline
    // vide ne la surclasse pas — retirer la classe pour restaurer Annuler et la croix.
    cancelBtn.classList.remove('hidden');
    if (closeBtn) { closeBtn.style.display = ''; closeBtn.disabled = false; closeBtn.classList.remove('hidden'); }
    if (deleteBtn) deleteBtn.style.display = 'none';
    // Cleanup any stale keydown handler from a previous modal
    if (_modalKeydownHandler && overlay) {
      overlay.removeEventListener("keydown", _modalKeydownHandler);
      _modalKeydownHandler = null;
    }
    var _confirmEscHandler = function(evt) {
      if (evt.key === 'Escape') {
        overlay.removeEventListener('keydown', _confirmEscHandler);
        _modalKeydownHandler = null;
        closeModal();
        if (options.onCancel) options.onCancel();
      }
    };
    _modalKeydownHandler = _confirmEscHandler;
    overlay.addEventListener('keydown', _confirmEscHandler);
    saveBtn.onclick = function() { overlay.removeEventListener('keydown', _confirmEscHandler); _modalKeydownHandler = null; closeModal(); if (onConfirm) onConfirm(); };
    cancelBtn.onclick = function() { overlay.removeEventListener('keydown', _confirmEscHandler); _modalKeydownHandler = null; closeModal(); if (options.onCancel) options.onCancel(); };
    if (closeBtn) closeBtn.onclick = function() { overlay.removeEventListener('keydown', _confirmEscHandler); _modalKeydownHandler = null; closeModal(); if (options.onCancel) options.onCancel(); };
    overlay.classList.add("modal-overlay--visible");
    document.body.style.overflow = 'hidden';
    // Click-outside-to-close: click on overlay background acts as cancel
    overlay.onclick = function(evt) {
      if (evt.target === overlay) {
        overlay.removeEventListener('keydown', _confirmEscHandler);
        _modalKeydownHandler = null;
        closeModal();
        if (options.onCancel) options.onCancel();
      }
    };
  }
  window.showConfirmModal = showConfirmModal;

  // modules/kpis.js
  function loadKpiTable() {
    const tbody = document.getElementById("kpiTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('kpis')) return;
    tbody.innerHTML = "";
    const _s = SectionToolbar.getSort("kpis");
    const _sortKey = _s ? _s.key : "name";
    const _sortDir = _s ? _s.dir : "asc";
    const sortedKpis = [...appData.kpis].sort((a, b) => {
      var diff = (_sortKey === "value" || _sortKey === "progress") ? SortUtils.numeric(a.progress, b.progress) : SortUtils.text(a.title, b.title);
      return _sortDir === "desc" ? -diff : diff;
    });
    sortedKpis.forEach((kpi) => {
      const row = document.createElement("tr");
      row.innerHTML = `
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(kpi.title)}</div></td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(kpi.description)}</div></td>
            <td><input type="text" class="form-control" data-readonly-lock="true" ${isReadOnlyMode() ? "disabled" : ""} value="${escapeHtml(kpi.comments)}" onchange="updateKpiComment('${escapeHtml(kpi.id)}', this.value)"></td>
            <td><div class="progress-bar" role="progressbar" aria-valuenow="${kpi.progress}" aria-valuemin="0" aria-valuemax="100" aria-label="${escapeHtml(kpi.title)} ${kpi.progress}%"><div class="progress-bar__fill" style="width: ${Math.max(0, Math.min(100, kpi.progress))}%"></div></div><span>${kpi.progress}%</span></td>
        `;
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger")) return;
        // Ne pas ouvrir la modale quand on interagit avec le champ commentaire inline
        // (ni tout autre champ de saisie ajouté ultérieurement dans la ligne).
        if (e.target.closest("input, textarea, select, a, button")) return;
        editKpi(kpi.id);
      });
      tbody.appendChild(row);
    });
  }
  function updateKpiComment(id, value) {
    if (!ensureEditMode("edit a KPI")) {
      refreshReadOnlyLocks();
      return;
    }
    const kpi = appData.kpis.find((k) => k.id === id);
    if (kpi) {
      kpi.comments = value;
      saveData();
    }
  }
  function editKpi(id) {
    const kpi = appData.kpis.find((k) => k.id === id);
    if (!kpi) return;
    openModal(t('modals.editKpi'), [
      { name: "title", label: t('fields.title'), value: kpi.title },
      { name: "description", label: t('fields.description'), value: kpi.description },
      { name: "comments", label: t('fields.comment'), value: kpi.comments },
      { name: "progress", label: "Progression (%)", type: "number", value: kpi.progress }
    ], (data) => {
      kpi.title = data.title;
      kpi.description = data.description;
      kpi.comments = data.comments;
      kpi.progress = Math.max(0, Math.min(100, parseInt(data.progress, 10) || 0));
            addAuditEntry('edit', 'kpis', kpi.title);
      saveData();
      loadKpiTable();
    }, () => deleteKpi(id));
  }
  function addKpi() {
    openModal(t('modals.addKpi'), [
      { name: "title", label: t('fields.title') },
      { name: "description", label: t('fields.description') },
      { name: "comments", label: t('fields.comment') },
      { name: "progress", label: "Progression (%)", type: "number", value: 0 }
    ], (data) => {
      if (!data.title) return false;
      const id = generateId("kpi");
      appData.kpis.push({
        id,
        title: data.title,
        description: data.description || "",
        comments: data.comments || "",
        progress: Math.max(0, Math.min(100, parseInt(data.progress, 10) || 0))
      });
            addAuditEntry('add', 'kpis', data.title);
      saveData();
      loadKpiTable();
      updateDashboard();
    });
  }
  function deleteKpi(id) {
    showConfirmModal(t('confirms.deleteKpi'), t('confirms.deleteKpi'), function() {
      var _kpi = appData.kpis.find(function(k) { return k.id === id; });
      appData.kpis = appData.kpis.filter((k) => k.id !== id);
            addAuditEntry('delete', 'kpis', _kpi ? _kpi.title : id);
      saveData();
      loadKpiTable();
      updateDashboard(); // PASS2-306: Refresh dashboard after KPI delete
    }, { type: 'danger' });
  }
  window.loadKpiTable = loadKpiTable;
  window.updateKpiComment = updateKpiComment;
  window.editKpi = editKpi;
  window.addKpi = addKpi;
  window.deleteKpi = deleteKpi;
  window.toggleTheme = toggleTheme;

  // modules/utils.js
  function escapeHtml(str) {
    if (str === null || str === void 0) return "";
    return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
  }
  function isSafeUrl(url) {
    if (!url || typeof url !== 'string') return false;
    return /^https?:\/\//i.test(url.trim());
  }
  function fetchWithTimeout(url, options, timeoutMs) {
    timeoutMs = timeoutMs || 8000;
    options = options || {};
    var controller = new AbortController();
    var timer = setTimeout(function() { controller.abort(); }, timeoutMs);
    options.signal = controller.signal;
    return fetch(url, options).finally(function() { clearTimeout(timer); });
  }
  const CSV_DELIMITER = ";";
  const CSV_BOM = "\uFEFF";
  const EXCEL_XML_BOM = "\uFEFF";
  let _exportBusy = false;
  function encodeUtf16Le(text) {
    const buffer = new ArrayBuffer(text.length * 2);
    const view = new DataView(buffer);
    for (let i = 0; i < text.length; i++) {
      view.setUint16(i * 2, text.charCodeAt(i), true);
    }
    return buffer;
  }
  // === CSI-ME === (audit import-export 2026-07-03, K6) suffixe de périmètre pour TOUS les
  // fichiers exportés — deux exports de deux entités étaient indiscernables par leur nom.
  // Chaîne vide hors mode multi-entités (noms historiques inchangés).
  function _csiExportScopeSlug() {
    try {
      if (appData._csiEntity && appData._csiEntity.id) {
        var nm = String(appData._csiEntity.name || appData._csiEntity.id);
        return '_' + (NORMALIZE(nm).replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || String(appData._csiEntity.id));
      }
      if (window.CSI_EntityManager && window.CSI_EntityManager.isMultiEntity && window.CSI_EntityManager.isMultiEntity()) {
        return '_' + t('entities.exportGroupTag', 'entite-principale');
      }
    } catch (e) { /* hors mode */ }
    return '';
  }
  function normalizeExportFilename(label) {
    const safe = NORMALIZE(label || "export").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
    return (safe || "export") + _csiExportScopeSlug();
  }
  function getExportDateStamp() {
    return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
  }
  function getExportLocale() {
    var lang = (window.CSI_i18n && typeof window.CSI_i18n.getLanguage === 'function')
      ? window.CSI_i18n.getLanguage() : 'fr';
    return { fr: 'fr-FR', en: 'en-GB', nl: 'nl-NL', es: 'es-ES', de: 'de-DE' }[lang] || 'fr-FR';
  }
  function cleanExportText(value) {
    if (value === null || value === void 0) return "";
    return String(value).replace(/\s+/g, " ").trim();
  }
  function escapeCsvValue(value) {
    const clean = cleanExportText(value);
    if (!clean) return "";
    // Anti-injection de formule Excel/CSV (CWE-1236) : neutralise les cellules
    // commençant par =,+,-,@ — réutilise la même protection que l'export .xls
    // (sanitizeExcelString), qui n'était appliquée qu'au chemin XML, pas au CSV.
    const text = sanitizeExcelString(clean);
    const needsQuotes = text.includes(CSV_DELIMITER) || text.includes('"') || /[\r\n]/.test(text);
    const escaped = text.replace(/"/g, '""');
    return needsQuotes ? `"${escaped}"` : escaped;
  }
  function buildCsvContent(headers, rows) {
    const lines = [];
    lines.push(`sep=${CSV_DELIMITER}`);
    if (headers.length) lines.push(headers.map(escapeCsvValue).join(CSV_DELIMITER));
    rows.forEach((row) => {
      lines.push(row.map(escapeCsvValue).join(CSV_DELIMITER));
    });
    return CSV_BOM + lines.join("\r\n");
  }
  function downloadCsvFile(label, headers, rows) {
    // Bloquer l'export en mode démo
    if (DEMO_MODE) {
      showDemoBlockedMessage('L\'export Excel/CSV');
      return;
    }
    if (!rows.length) {
      showToast2(t('toasts.noDataToExport'), "warning");
      return;
    }
    try {
      const csvContent = buildCsvContent(headers, rows);
      const filename = `${normalizeExportFilename(label)}_${getExportDateStamp()}.csv`;
      const blob = new Blob([encodeUtf16Le(csvContent)], { type: "text/csv;charset=utf-16le;" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      showToast2(t('toasts.excelExportSuccess'), "success");
    } catch (error) {
      void 0;
      showToast2(t('toasts.excelExportError'), "error");
    }
  }
  function buildMultiSectionCsvContent(sections) {
    const lines = [];
    lines.push(`sep=${CSV_DELIMITER}`);
    (sections || []).forEach((section, index) => {
      if (!section || !Array.isArray(section.headers)) return;
      if (index > 0) lines.push("");
      lines.push(escapeCsvValue(section.title || "Section"));
      lines.push(section.headers.map(escapeCsvValue).join(CSV_DELIMITER));
      (section.rows || []).forEach((row) => {
        lines.push(row.map(escapeCsvValue).join(CSV_DELIMITER));
      });
    });
    return CSV_BOM + lines.join("\r\n");
  }
  function downloadCsvContent(label, csvContent) {
    if (!csvContent) {
      showToast2(t('toasts.noDataToExport'), "warning");
      return;
    }
    try {
      const filename = `${normalizeExportFilename(label)}_${getExportDateStamp()}.csv`;
      const blob = new Blob([encodeUtf16Le(csvContent)], { type: "text/csv;charset=utf-16le;" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      showToast2(t('toasts.excelExportSuccess'), "success");
    } catch (error) {
      void 0;
      showToast2(t('toasts.excelExportError'), "error");
    }
  }
  function stripInvalidXmlChars(text) {
    return String(text).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
  }
  function escapeExcelXml(value) {
    return stripInvalidXmlChars(value)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }
  function sanitizeExcelString(value) {
    const text = String(value);
    return /^[=+\-@]/.test(text) ? `'${text}` : text;
  }
  function sanitizeSheetName(name) {
    const cleaned = cleanExportText(name || "Section").replace(/[\\/?*\[\]:]/g, " ").trim();
    const safe = cleaned || "Section";
    return safe.length > 31 ? safe.slice(0, 31) : safe;
  }
  function getExcelCellData(value) {
    if (value === null || value === void 0) return { type: "String", value: "" };
    if (typeof value === "number" && Number.isFinite(value)) return { type: "Number", value: String(value) };
    if (typeof value === "boolean") return { type: "Boolean", value: value ? "1" : "0" };
    if (value instanceof Date && Number.isFinite(value.getTime())) {
      return { type: "DateTime", value: value.toISOString() };
    }
    const text = sanitizeExcelString(String(value));
    return { type: "String", value: text };
  }
  function buildExcelRowXml(values, columnCount, isHeader) {
    const rowValues = Array.isArray(values) ? values : [];
    const padCount = Math.max(0, columnCount - rowValues.length);
    const padded = rowValues.concat(Array(padCount).fill(""));
    const cells = padded.map((cellValue) => {
      const cell = getExcelCellData(cellValue);
      const style = isHeader ? ' ss:StyleID="sHeader"' : "";
      return `<Cell${style}><Data ss:Type="${cell.type}">${escapeExcelXml(cell.value)}</Data></Cell>`;
    }).join("");
    return `<Row>${cells}</Row>`;
  }
  function buildExcelWorkbookXml(sections) {
    const usedNames = new Set();
    const workbookParts = [];
    workbookParts.push('<?xml version="1.0" encoding="UTF-8"?>');
    workbookParts.push('<?mso-application progid="Excel.Sheet"?>');
    workbookParts.push('<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40">');
    workbookParts.push('<Styles><Style ss:ID="sHeader"><Font ss:Bold="1"/><Interior ss:Color="#D9E1F2" ss:Pattern="Solid"/></Style></Styles>');
    (sections || []).forEach((section) => {
      if (!section || !Array.isArray(section.headers)) return;
      const baseName = sanitizeSheetName(section.title || "Section");
      let sheetName = baseName;
      if (usedNames.has(sheetName)) {
        let index = 2;
        while (index < 1000) {
          const suffix = ` (${index})`;
          const candidateBase = baseName.slice(0, 31 - suffix.length);
          const candidate = `${candidateBase}${suffix}`;
          if (!usedNames.has(candidate)) {
            sheetName = candidate;
            break;
          }
          index += 1;
        }
      }
      usedNames.add(sheetName);
      const rows = Array.isArray(section.rows) ? section.rows : [];
      const rowLengths = rows.map((row) => Array.isArray(row) ? row.length : 0);
      const columnCount = Math.max(1, section.headers.length || 0, ...rowLengths);
      const totalRows = (section.headers.length ? 1 : 0) + rows.length;
      const rowCount = Math.max(1, totalRows);
      workbookParts.push(`<Worksheet ss:Name="${escapeExcelXml(sheetName)}">`);
      workbookParts.push(`<Table ss:ExpandedColumnCount="${columnCount}" ss:ExpandedRowCount="${rowCount}" x:FullColumns="1" x:FullRows="1">`);
      if (section.headers.length) {
        workbookParts.push(buildExcelRowXml(section.headers, columnCount, true));
      }
      rows.forEach((row) => {
        workbookParts.push(buildExcelRowXml(row, columnCount, false));
      });
      workbookParts.push("</Table>");
      workbookParts.push('<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel"><FreezePanes/><FrozenNoSplit/><SplitHorizontal>1</SplitHorizontal><TopRowBottomPane>1</TopRowBottomPane><ActivePane>2</ActivePane></WorksheetOptions>');
      workbookParts.push("</Worksheet>");
    });
    workbookParts.push("</Workbook>");
    return workbookParts.join("");
  }
  function downloadExcelWorkbook(label, sections) {
    if (DEMO_MODE) {
      showDemoBlockedMessage('L\'export Excel');
      return;
    }
    if (!sections || !sections.length) {
      showToast2(t('toasts.noDataToExport'), "warning");
      return;
    }
    try {
      const xmlContent = buildExcelWorkbookXml(sections);
      const filename = `${normalizeExportFilename(label)}_${getExportDateStamp()}.xls`;
      const blob = new Blob([EXCEL_XML_BOM + xmlContent], { type: "application/vnd.ms-excel;charset=utf-8;" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      showToast2(t('toasts.excelExportSuccess'), "success");
    } catch (error) {
      void 0;
      showToast2(t('toasts.excelExportError'), "error");
    }
  }
  function isEmptyExportRow(row) {
    if (!row) return true;
    if (row.querySelector(".nis2-empty")) return true;
    const cells = row.querySelectorAll("td");
    if (cells.length === 1 && cells[0].getAttribute("colspan")) return true;
    return cells.length === 0;
  }
  function exportTableToCsv(tableId, label) {
    const table = document.getElementById(tableId);
    if (!table) {
      showToast2(t('toasts.exportTableNotFound', "Table d'export introuvable"), "error");
      return;
    }
    const headerCells = Array.from(table.querySelectorAll("thead th"));
    const headers = headerCells.map((cell) => cleanExportText(cell.textContent));
    // M8 (audit 2026-07-10) : une cellule dont le contenu est un champ de saisie
    // (<input>/<select>/<textarea>, ex. « Date CODIR » des risques, « Commentaire » des KPI)
    // a un textContent vide → colonne exportée vide pour toutes les lignes. On lit la valeur
    // du champ en repli (le libellé de l'option pour un select).
    const _cellExportText = (cell) => {
      const field = cell.querySelector("input, select, textarea");
      if (field) {
        if (field.tagName === "SELECT") {
          const opt = field.options && field.options[field.selectedIndex];
          return cleanExportText(opt ? opt.text : field.value);
        }
        return cleanExportText(field.value);
      }
      return cleanExportText(cell.textContent);
    };
    const rows = Array.from(table.querySelectorAll("tbody tr"))
      .filter((row) => !isEmptyExportRow(row))
      .map((row) => Array.from(row.querySelectorAll("td")).map((cell) => _cellExportText(cell)));
    downloadCsvFile(label, headers, rows);
  }
  function getNis2ProgramExportData() {
    ensureNis2Plan();
    // Export Budget Cybersécurité : lignes budgétaires autonomes, colonnes d'années
    // dynamiques, sous-totaux par catégorie + total général (cohérent avec l'écran).
    const years = getBudgetYears();
    // 1re colonne « Type » (Ligne / Sous-total / Total) : permet de filtrer les lignes de
    // détail dans Excel et d'éviter qu'une somme de colonne ne double-compte les totaux.
    const headers = [
      t('nis2Program.rowType', 'Type'),
      t('nis2Program.lineLabel', 'Intitulé'),
      t('nis2Program.lineCategory', 'Catégorie'),
      t('nis2Program.lineOwner', 'Responsable'),
      t('nis2Program.lineStatus', 'Statut')
    ].concat(years.slice()).concat([t('nis2Program.lineTotal', 'Total')]);
    const lines = getBudgetLines();
    const cats = getBudgetCategoriesList();
    const order = {};
    cats.forEach((c, i) => { order[c.value] = i; });
    const groups = {};
    lines.forEach((l) => { const c = l.category || 'divers'; (groups[c] = groups[c] || []).push(l); });
    const groupKeys = Object.keys(groups).sort((a, b) => (order[a] != null ? order[a] : 999) - (order[b] != null ? order[b] : 999));
    const rows = [];
    const grandYear = years.map(() => 0);
    let grandTotal = 0;
    groupKeys.forEach((catKey) => {
      const groupLines = groups[catKey];
      const subYear = years.map(() => 0);
      let subTotal = 0;
      groupLines.forEach((line) => {
        const amts = years.map((yr) => getBudgetLineYearAmount(line, yr));
        const total = amts.reduce((a, b) => a + b, 0);
        amts.forEach((v, i) => { subYear[i] += v; grandYear[i] += v; });
        subTotal += total; grandTotal += total;
        rows.push([
          t('nis2Program.rowTypeLine', 'Ligne'),
          line.label || '',
          budgetCategoryLabel(catKey),
          line.owner || '',
          budgetStatusMeta(line.status).label
        ].concat(amts).concat([total]));
      });
      rows.push([
        t('nis2Program.rowTypeSubtotal', 'Sous-total'),
        budgetCategoryLabel(catKey),
        '', '', ''
      ].concat(subYear).concat([subTotal]));
    });
    rows.push([
      t('nis2Program.rowTypeTotal', 'Total'),
      t('nis2Program.grandTotal', 'Total général'),
      '', '', ''
    ].concat(grandYear).concat([grandTotal]));
    return { headers, rows };
  }
  function exportNis2ProgramExcel(label) {
    const { headers, rows } = getNis2ProgramExportData();
    downloadCsvFile(label, headers, rows);
  }
  function getNis2SocleExportData() {
    const headers = [t('excelHeaders.soclePillar'), t('excelHeaders.socleTitle'), t('excelHeaders.socleSubtitle'), t('excelHeaders.socleElement'), t('excelHeaders.socleOwner')];
    const pillars = normalizeSoclePillars(appData.soclePillars);
    const rows = [];
    pillars.forEach((pillar, index) => {
      const pillarLabel = t('labels.pillar', {number: index + 1});
      const items = Array.isArray(pillar.items) ? pillar.items : [];
      if (!items.length) {
        rows.push([
          pillarLabel,
          pillar.title || "",
          pillar.subtitle || "",
          "",
          pillar.owner || ""
        ]);
        return;
      }
      items.forEach((item) => {
        rows.push([
          pillarLabel,
          pillar.title || "",
          pillar.subtitle || "",
          item,
          pillar.owner || ""
        ]);
      });
    });
    return { headers, rows };
  }
  function exportNis2SocleExcel(label) {
    const { headers, rows } = getNis2SocleExportData();
    downloadCsvFile(label, headers, rows);
  }
  function exportCustomExcel(key, label) {
    switch (key) {
      case "nis2-program":
        exportNis2ProgramExcel(label);
        break;
      case "nis2-socle":
        exportNis2SocleExcel(label);
        break;
      default:
        showToast2(t('exportLabels.exportNotSupported'), "error");
        break;
    }
  }
  function setupExcelExportButtons() {
    document.querySelectorAll("[data-export-table]").forEach((btn) => {
      btn.addEventListener("click", () => {
        const tableId = btn.dataset.exportTable || "";
        const label = btn.dataset.exportName || "export";
        if (tableId === 'recyfControlsTable' && typeof exportRecyfControlsCsv === 'function') {
          exportRecyfControlsCsv(label);
        } else if (_exportSectionDataDriven(tableId, label)) {
          // M7 : export piloté par les données (colonnes complètes) — voir helper.
        } else {
          exportTableToCsv(tableId, label);
        }
      });
    });
    document.querySelectorAll("[data-export-custom]").forEach((btn) => {
      btn.addEventListener("click", () => {
        const key = btn.dataset.exportCustom || "";
        const label = btn.dataset.exportName || "export";
        exportCustomExcel(key, label);
      });
    });
  }
  // M7 (audit 2026-07-10) : exporter une section depuis le MODÈLE (getAllSectionExportData)
  // plutôt qu'en scrapant le DOM (exportTableToCsv). Depuis la refonte « ≤7 colonnes », l'export
  // de section perdait les colonnes retirées de l'écran (Risques 7 vs 12, NIS2 7 vs 12 :
  // justification, preuves, fréquence de revue absentes), sortait les textes tronqués à l'écran,
  // et restreignait silencieusement les lignes à un filtre/recherche actif. On aligne donc les
  // boutons de section sur les données complètes (même source que l'export Excel global).
  // Retourne true si la section a été exportée en data-driven, false → repli DOM (exportTableToCsv).
  var _EXPORT_TABLE_TO_SECTION = {
    controlsTable: 'controls', nis2ControlsTable: 'nis2-controls', soaTable: 'soa',
    docReviewTable: 'docReview', actionsTable: 'actions', risksTable: 'risks',
    threatsTable: 'threats', ncTable: 'nonconformities', auditsTable: 'audits',
    reviewsTable: 'reviews', stakeholdersTable: 'stakeholders', documentsTable: 'documents',
    kpiTable: 'kpis', criticalAssetsTable: 'critical-assets', projectRisksTable: 'project-risks'
  };
  function _exportSectionDataDriven(tableId, label) {
    var key = _EXPORT_TABLE_TO_SECTION[tableId];
    if (!key) return false;
    try {
      var sections = getAllSectionExportData();
      var sec = sections.find(function(s) { return s.key === key; });
      if (!sec || !Array.isArray(sec.headers) || !Array.isArray(sec.rows)) return false;
      downloadCsvFile(label, sec.headers, sec.rows);
      return true;
    } catch (e) {
      void 0;
      return false;
    }
  }
  function getAllSectionExportData() {
    ensureNis2Plan();
    ensureCriticalAssets();
    ensureProjectRisks();
    const getOptionLabel = (options, value) => {
      if (!Array.isArray(options)) return value || "";
      const match = options.find((opt) => {
        if (!opt) return false;
        if (typeof opt === "object") return opt.value === value;
        return opt === value;
      });
      if (!match) return value || "";
      return typeof match === "object" ? match.label : match;
    };
    const formatDate = (value, fallback = "N/A") => {
      if (!value) return fallback;
      const date = new Date(value);
      return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString();
    };
    const formatProjectRiskDate = (value) => {
      if (!value) return "—";
      const date = new Date(value);
      return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString();
    };
    const getCapabilityLabel = (key) => {
      if (!key) return "";
      const cap = OPERATIONAL_CAPABILITIES.find((c) => c.key === key);
      return cap ? cap.label : String(key).replace(/_/g, " ");
    };
    const formatCapabilities = (value) => {
      if (Array.isArray(value)) {
        return value.map((item) => getCapabilityLabel(item)).filter(Boolean).join(", ");
      }
      return getCapabilityLabel(value);
    };
    const assetLookup = getCriticalAssetLookup();
    const getCriticalAssetLabels = (ids) => {
      const list = Array.isArray(ids) ? ids.filter(Boolean) : [];
      if (!list.length) return [];
      return list.map((id) => {
        const asset = assetLookup.get(id);
        return asset ? getCriticalAssetLabel(asset) : id;
      }).filter(Boolean);
    };
    const getActionLinkLabels = (action) => {
      if (!action) return [];
      const type = action.linkType || (action.controlId ? "control" : "");
      const ids = getActionLinkIds(action);
      if (type === "control") {
        const id = ids[0];
        if (!id) return [];
        const resolved = resolveControlById(id);
        return [resolved ? resolved.label : id];
      }
      if (type === "risk") {
        return ids.map((id) => {
          const risk = (appData.risks || []).find((r) => r.id === id);
          return risk ? risk.title : id;
        }).filter(Boolean);
      }
      if (type === "nc") {
        const id = ids[0];
        if (!id) return [];
        const nc = appData.nonconformities.find((n) => n.id === id);
        return [nc ? nc.title : id];
      }
      if (type === "audit") {
        const id = ids[0];
        if (!id) return [];
        const audit = appData.audits.find((a) => a.id === id);
        return [audit ? audit.title : id];
      }
      if (type === "threat") {
        const id = ids[0];
        if (!id) return [];
        const threat = appData.threats.find((t) => t.id === id);
        return [threat ? threat.title : id];
      }
      if (type === "continuous") {
        return ids.length ? ids.map(() => t('excelHeaders.continuousImprovement')) : [];
      }
      return [];
    };
    const sections = [];
    var _excelIsoControls = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    const sortedControls = [..._excelIsoControls].sort((a, b) => SortUtils.reference(a.reference, b.reference));
    const controlRows = sortedControls.map((control) => {
      const actionIds = appData.actions
        .filter((a) => a.linkType === "control" && a.linkId === control.id)
        .map((a) => a.id)
        .join(", ");
      return [
        control.numero || "",
        isoCtrlTitle(control),
        isoCtrlDesc(control),
        formatCapabilities(control.subcategory),
        getOptionLabel(CONTROL_STATUS_OPTIONS, control.status),
        actionIds || t('excelHeaders.none'),
        control.cmm || ""
      ];
    });
    sections.push({
      key: "controls",
      title: t('reportSections.controls'),
      headers: [t('excelHeaders.controlNum'), t('excelHeaders.controlTitle'), t('excelHeaders.controlDescription'), t('excelHeaders.controlCapability'), t('excelHeaders.controlStatus'), t('excelHeaders.controlLinkedActions'), t('excelHeaders.controlCmm')],
      rows: controlRows
    });
    // FIX-EXCEL-RECYF (audit 2026-06-10) : la section NIS2 de l'export global suit le
    // référentiel ACTIF (modèle mutuellement exclusif) — ReCyF actif → feuille
    // « Mesures ReCyF » avec les évaluations réelles ; sinon feuille CyFun inchangée.
    if ((appData.nis2Referential || 'cyfun2025') === 'recyf2025' && typeof recyfControlsInScope === 'function') {
      if (typeof ensureRecyfControls === 'function') ensureRecyfControls();
      const recyfRows = recyfControlsInScope().map((c) => [
        c.ref || "",
        'Obj. ' + c.objectiveId + ' — ' + (c.objectiveLabel || ""),
        c.theme || "",
        c.content || "",
        c.owner || "",
        c.reviewer || "",
        c.reviewFrequency || "",
        c.lastEvaluation || "",
        c.nextReviewDate || "",
        (c.level === 'essentielle') ? 'EE' : 'EI/EE',
        _recyfConformityLabel(c.conformity || 'non_evalue'),
        c.justification || "",
        c.evidence || ""
      ]);
      sections.push({
        key: "nis2-controls",
        title: t('nav.recyfControls', 'Mesures ReCyF'),
        headers: [t('recyfControls.ref', 'Réf.'), t('recyfControls.objective', 'Objectif'), t('recyfControls.theme', 'Thématique'), t('recyfControls.measure', 'Mesure'), t('recyfControls.owner', 'Responsable'), t('recyfControls.reviewer', 'Relecteur'), t('recyfControls.reviewFrequency', 'Fréquence de revue'), t('recyfControls.lastEvaluation', 'Dernière évaluation'), t('recyfControls.nextReviewDate', 'Prochaine revue'), t('recyfControls.target', 'Cible'), t('recyfControls.conformity', 'Conformité'), t('recyfControls.justification', 'Justification'), t('recyfControls.evidence', 'Preuves / Artefacts')],
        rows: recyfRows
      });
    } else {
      var _excelNis2Controls = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls('cyfun2025') : (appData.nis2Controls || []);
      const nis2Rows = _excelNis2Controls.map((control) => [
        control.fonction || "",
        control.categorie || "",
        control.description || "",
        control.owner || "",
        control.reviewer || "",
        control.reviewFrequency || "",
        control.lastEvaluation || "",
        control.nextReviewDate || "",
        getOptionLabel(CONTROL_STATUS_OPTIONS, control.status),
        // M-CF1 (audit fond 2026-07-11) : exporter les VRAIES entrées du scoring CCB —
        // maturité documentation (1-5), maturité implémentation (1-5) et flag mesure clé —
        // au lieu du seul CMM dérivé. Sans elles, l'export plat CyFun ne permettait pas de
        // reconstituer/auditer le score de conformité CCB.
        (control.docMaturity != null ? control.docMaturity : 0),
        (control.implMaturity != null ? control.implMaturity : 0),
        (control.isKeyMeasure ? t('common.yes', 'Oui') : t('common.no', 'Non')),
        control.cmm || "",
        control.justification || "",
        control.evidence || ""
      ]);
      sections.push({
        key: "nis2-controls",
        title: t('reportSections.nis2Controls'),
        headers: [t('excelHeaders.nis2Function'), t('excelHeaders.nis2Category'), t('excelHeaders.nis2Description'), t('controlsNIS2.owner', 'Responsable'), t('controlsNIS2.reviewer', 'Relecteur'), t('controlsNIS2.reviewFrequency', 'Fréquence de revue'), t('controlsNIS2.lastEvaluation', 'Dernière évaluation'), t('controlsNIS2.nextReviewDate', 'Prochaine revue'), t('excelHeaders.nis2Status'), t('cyfun.docMaturity', 'Maturité documentation'), t('cyfun.implMaturity', 'Maturité implémentation'), t('cyfun.keyMeasure', 'Mesure clé'), t('excelHeaders.nis2Cmm', 'Maturité (dérivée)'), t('excelHeaders.nis2Justification'), t('cyfun.evidence', 'Preuves / Artefacts')],
        rows: nis2Rows
      });
    }
    const soaEntries = Array.isArray(appData.soa) ? appData.soa : [];
    const soaRows = sortedControls.map((control) => {
      const entry = soaEntries.find((e) => e.controlId === control.id);
      const applicable = entry ? entry.applicable : true;
      return [
        control.reference || "",
        isoCtrlTitle(control),
        applicable ? t('excelHeaders.soaApplicable') : t('excelHeaders.soaNonApplicable'),
        (entry && entry.justification) || ""
      ];
    });
    sections.push({
      key: "soa",
      title: t('reportSections.soa'),
      headers: [t('excelHeaders.soaReference'), t('excelHeaders.soaTitle'), t('excelHeaders.soaApplicability'), t('excelHeaders.soaJustification')],
      rows: soaRows
    });
    const docEntries = Array.isArray(appData.documentReview) ? appData.documentReview : [];
    const docRows = [];
    var _xlMgmtFw = _getActiveManagementFramework();
    var _xlIsEn = (typeof currentLang !== 'undefined' && currentLang === 'en') || (document.documentElement.lang === 'en');
    DOCUMENT_REVIEW_ITEMS.forEach((item) => {
      if (item.type === "chapter") {
        var _xlChTitle = item.title || "";
        if (_xlMgmtFw && _xlMgmtFw.chapters) {
          var _xlChId = (item.title || "").match(/^(\d+)/);
          if (_xlChId) {
            var _xlCh = _xlMgmtFw.chapters.find(function(c) { return c.id === _xlChId[1]; });
            if (_xlCh) _xlChTitle = (_xlIsEn && _xlCh.titleEn) ? _xlCh.titleEn : _xlCh.title;
          }
        }
        docRows.push([_xlChTitle, "", "", ""]);
        return;
      }
      const entry = docEntries.find((e) => e.reference === item.reference) || {};
      var _xlClauseNum = (item.reference || "").split(' ')[0];
      var _xlFwClause = _xlMgmtFw ? (_xlMgmtFw.clauses || []).find(function(c) { return c.reference === _xlClauseNum; }) : null;
      var _xlRef = (_xlFwClause && _xlIsEn && _xlFwClause.titleEn) ? (_xlClauseNum + ' ' + _xlFwClause.titleEn) : item.reference;
      var _xlDesc = (_xlFwClause && _xlIsEn && _xlFwClause.descriptionEn) ? _xlFwClause.descriptionEn : item.description;
      docRows.push([
        _xlRef || "",
        _xlDesc || "",
        entry.justification || "",
        entry.tool || ""
      ]);
    });
    sections.push({
      key: "docReview",
      title: t('reportSections.docReview'),
      headers: [t('excelHeaders.docReference'), t('excelHeaders.docExigence'), t('excelHeaders.docJustification'), t('excelHeaders.docTool')],
      rows: docRows
    });
    const actionRows = (appData.actions || []).map((action) => {
      const linkLabels = getActionLinkLabels(action);
      const linkText = linkLabels.length ? linkLabels.join(", ") : t('excelHeaders.na');
      const assetLabels = getCriticalAssetLabels(action.criticalAssetIds);
      const assetText = assetLabels.length ? assetLabels.join(", ") : t('excelHeaders.noAsset');
      const progressRaw = parseInt(action.progress, 10);
      const progress = Number.isFinite(progressRaw) ? Math.max(0, Math.min(100, progressRaw)) : 0;
      return [
        action.id || "",
        action.title || "",
        linkText,
        assetText,
        getOptionLabel(ACTION_PRIORITY_OPTIONS, action.priority),
        `${progress}%`,
        action.dueDate ? formatDate(action.dueDate, "N/A") : "N/A",
        action.assignedTo ? action.assignedTo : "N/A",
        // M6 (audit 2026-07-10) : statut de l'action (planifie/en_cours/termine/annule) —
        // absent de l'export global alors qu'il est badge de la table, champ du formulaire et
        // filtre ; sans lui, une action annulée à échéance passée est indiscernable d'un retard.
        getOptionLabel(ACTION_STATUS_OPTIONS, action.status),
        // Unification 2026-07-10 : exporter le CONTENU des commentaires (avant : simple compte)
        formatCommentsReadonly(action.comments).split('\n').join(' | ')
      ];
    });
    sections.push({
      key: "actions",
      title: t('reportSections.actions'),
      headers: [t('excelHeaders.actionId'), t('excelHeaders.actionTitle'), t('excelHeaders.actionLink'), t('excelHeaders.actionAssets'), t('excelHeaders.actionPriority'), t('excelHeaders.actionProgress'), t('excelHeaders.actionDueDate'), t('excelHeaders.actionOwner'), t('excelHeaders.actionStatus'), t('excelHeaders.actionComments')],
      rows: actionRows
    });
    const riskRows = (appData.risks || []).map((risk) => {
      const impactLabel = IMPACT_LEVEL_LABELS[risk.impact] || risk.impact || "";
      const probLabel = PROBABILITY_LEVEL_LABELS[risk.probability] || risk.probability || "";
      const score = Number.isFinite(risk.score)
        ? risk.score
        : (parseInt(risk.impact, 10) || 0) * (parseInt(risk.probability, 10) || 0);
      const level = risk.level || calculateRiskLevel(score);
      const linkedActions = (appData.actions || []).filter((a) => actionLinksToRisk(a, risk.id));
      const actionsText = linkedActions.length
        ? linkedActions.map((a) => a.title || a.id).join(", ")
        : t('excelHeaders.none');
      const assetLabels = getCriticalAssetLabels(risk.criticalAssetIds);
      const assetText = assetLabels.length ? assetLabels.join(", ") : t('excelHeaders.noAsset');
      const codirDate = risk.codirDate ? formatDate(risk.codirDate, risk.codirDate) : "";
      return [
        risk.id || "",
        risk.title || "",
        risk.description || "",
        impactLabel,
        probLabel,
        score,
        level,
        actionsText,
        assetText,
        risk.treatment || "",
        risk.owner || "", // M17
        codirDate,
        formatCommentsReadonly(risk.comments).split('\n').join(' | ')
      ];
    });
    sections.push({
      key: "risks",
      title: t('reportSections.risks'),
      headers: [t('excelHeaders.riskId'), t('excelHeaders.riskTitle'), t('excelHeaders.riskDescription'), t('excelHeaders.riskImpact'), t('excelHeaders.riskProbability'), t('excelHeaders.riskScore'), t('excelHeaders.riskLevel'), t('excelHeaders.riskLinkedActions'), t('excelHeaders.riskAssets'), t('excelHeaders.riskTreatment'), t('excelHeaders.riskOwner'), t('excelHeaders.riskCodirDate'), t('excelHeaders.riskComments')],
      rows: riskRows
    });
    const threatRows = (appData.threats || []).map((threat) => {
      const impactLabel = IMPACT_LEVEL_LABELS[threat.impact] || threat.impact || "";
      const probLabel = PROBABILITY_LEVEL_LABELS[threat.likelihood] || threat.likelihood || "";
      const score = Number.isFinite(threat.score)
        ? threat.score
        : (parseInt(threat.impact, 10) || 0) * (parseInt(threat.likelihood, 10) || 0);
      const level = threat.level || calculateRiskLevel(score);
      const linkedActionIds = appData.actions
        .filter((a) => a.linkType === "threat" && a.linkId === threat.id)
        .map((a) => a.id)
        .join(", ");
      const assetLabels = getCriticalAssetLabels(threat.criticalAssetIds);
      const assetText = assetLabels.length ? assetLabels.join(", ") : t('excelHeaders.noAsset');
      return [
        threat.title || "",
        threat.description || "",
        impactLabel,
        probLabel,
        score,
        level,
        THREAT_STATUS_LABELS[threat.status] || threat.status || "",
        linkedActionIds || t('excelHeaders.none'),
        assetText
      ];
    });
    sections.push({
      key: "threats",
      title: t('reportSections.threats'),
      headers: [t('excelHeaders.threatTitle'), t('excelHeaders.threatDescription'), t('excelHeaders.threatImpact'), t('excelHeaders.threatProbability'), t('excelHeaders.threatScore'), t('excelHeaders.threatLevel'), t('excelHeaders.threatStatus'), t('excelHeaders.threatLinkedActions'), t('excelHeaders.threatAssets')],
      rows: threatRows
    });
    const auditMap = new Map((appData.audits || []).map((audit) => [audit.id, audit.title || audit.id]));
    const ncRows = (appData.nonconformities || []).map((nc) => {
      const relatedActions = (appData.actions || []).filter((a) => (a.ncIds || []).includes(nc.id) || (a.linkType === "nc" && a.linkId === nc.id));
      const actionsText = relatedActions.length ? relatedActions.map((a) => a.title || a.id).join(", ") : t('excelHeaders.none');
      const auditText = auditMap.get(nc.auditId) || t('excelHeaders.noAsset');
      const assetLabels = getCriticalAssetLabels(nc.criticalAssetIds);
      const assetText = assetLabels.length ? assetLabels.join(", ") : t('excelHeaders.noAsset');
      return [
        nc.title || "",
        nc.type || "",
        nc.status || "",
        nc.detectionDate ? formatDate(nc.detectionDate, "N/A") : "N/A",
        nc.assignedTo ? nc.assignedTo : "N/A",
        auditText,
        actionsText,
        assetText,
        nc.description || ""
      ];
    });
    sections.push({
      key: "nonconformities",
      title: t('reportSections.nonconformities'),
      headers: [t('excelHeaders.ncTitle'), t('excelHeaders.ncType'), t('excelHeaders.ncStatus'), t('excelHeaders.ncDetectionDate'), t('excelHeaders.ncOwner'), t('excelHeaders.ncLinkedAudit'), t('excelHeaders.ncLinkedActions'), t('excelHeaders.ncAssets'), t('excelHeaders.ncOperations')],
      rows: ncRows
    });
    const auditRows = (appData.audits || []).map((audit) => {
      const linkedActionIds = (appData.actions || []).filter((a) => a.linkType === "audit" && a.linkId === audit.id).map((a) => a.id).join(", ");
      return [
        audit.title || "",
        audit.type || "",
        audit.scope || "",
        audit.plannedDate ? formatDate(audit.plannedDate, "N/A") : "N/A",
        audit.executionDate ? formatDate(audit.executionDate, "N/A") : "N/A",
        audit.auditor ? audit.auditor : "N/A",
        audit.status || "",
        audit.findings || "",
        audit.recommendations || "",
        linkedActionIds || t('excelHeaders.none')
      ];
    });
    sections.push({
      key: "audits",
      title: t('reportSections.audits'),
      headers: [t('excelHeaders.auditTitle'), t('excelHeaders.auditType'), t('excelHeaders.auditScope'), t('excelHeaders.auditPlannedDate'), t('excelHeaders.auditExecutionDate', "Date d'exécution"), t('excelHeaders.auditAuditor'), t('excelHeaders.auditStatus'), t('excelHeaders.auditFindings', 'Constatations'), t('excelHeaders.auditRecommendations', 'Recommandations'), t('excelHeaders.auditLinkedActions')],
      rows: auditRows
    });
    const reviewRows = (appData.reviews || []).map((review) => [
      review.date ? formatDate(review.date, "N/A") : "N/A",
      review.participants || "",
      review.inputs || "",
      review.decisions || "",
      formatCommentsReadonly(review.comments).split('\n').join(' | ')
    ]);
    sections.push({
      key: "reviews",
      title: t('reportSections.managementReviews'),
      headers: [t('excelHeaders.reviewDate'), t('excelHeaders.reviewParticipants'), t('excelHeaders.reviewInputs'), t('excelHeaders.reviewDecisions'), t('excelHeaders.reviewComments')],
      rows: reviewRows
    });
    const stakeholderRows = (appData.stakeholders || []).map((st) => [
      st.name || "",
      st.role || "",
      st.contact || "",
      st.notes || ""
    ]);
    sections.push({
      key: "stakeholders",
      title: t('reportSections.stakeholders'),
      headers: [t('excelHeaders.stakeholderName'), t('excelHeaders.stakeholderRole'), t('excelHeaders.stakeholderContact'), t('excelHeaders.stakeholderComments')],
      rows: stakeholderRows
    });
    const documentRows = (appData.documents || []).map((doc) => [
      doc.category || "",
      doc.type || "",
      doc.tool || "",
      doc.link || ""
    ]);
    sections.push({
      key: "documents",
      title: t('reportSections.documents'),
      headers: [t('excelHeaders.documentLevel'), t('excelHeaders.documentType'), t('excelHeaders.documentTool'), t('excelHeaders.documentLink')],
      rows: documentRows
    });
    const kpiRows = (appData.kpis || []).map((kpi) => {
      const progressRaw = parseInt(kpi.progress, 10);
      const progress = Number.isFinite(progressRaw) ? Math.max(0, Math.min(100, progressRaw)) : 0;
      return [
        kpi.title || "",
        kpi.description || "",
        kpi.comments || "",
        `${progress}%`
      ];
    });
    sections.push({
      key: "kpis",
      title: t('reportSections.kpis'),
      headers: [t('excelHeaders.kpiTitle'), t('excelHeaders.kpiDescription'), t('excelHeaders.kpiComment'), t('excelHeaders.kpiProgress')],
      rows: kpiRows
    });
    const nis2ProgramData = getNis2ProgramExportData();
    sections.push({
      key: "nis2-program",
      title: t('reportSections.nis2Program'),
      headers: nis2ProgramData.headers,
      rows: nis2ProgramData.rows
    });
    const nis2SocleData = getNis2SocleExportData();
    sections.push({
      key: "nis2-socle",
      title: t('reportSections.nis2Socle'),
      headers: nis2SocleData.headers,
      rows: nis2SocleData.rows
    });
    const assetRows = (appData.criticalAssets || []).slice().sort((a, b) => SortUtils.numeric(a.priority, b.priority)).map((asset, index) => {
      const tags = Array.isArray(asset.productTags) ? asset.productTags.filter(Boolean) : [];
      const productParts = [];
      if (asset.productCode) productParts.push(asset.productCode);
      if (asset.productName) productParts.push(asset.productName);
      let productLabel = productParts.join(" - ");
      if (tags.length) {
        const tagLabel = tags.join(", ");
        productLabel = productLabel ? `${productLabel} (Tags: ${tagLabel})` : `Tags: ${tagLabel}`;
      }
      const availability = asset.availability ? resolveCriticalAssetLabel(asset.availability, CRITICAL_ASSET_AVAILABILITY_OPTIONS) : "—";
      const availabilityLabel = asset.availabilityNote ? `${availability} (${asset.availabilityNote})` : availability;
      const conf = resolveCriticalAssetLabel(asset.confidentiality, CRITICAL_ASSET_CONF_OPTIONS) || "—";
      const integrity = resolveCriticalAssetLabel(asset.integrity, CRITICAL_ASSET_INTEGRITY_OPTIONS) || "—";
      const assetType = resolveCriticalAssetLabel(asset.assetType, CRITICAL_ASSET_TYPE_OPTIONS) || "—";
      const importance = resolveCriticalAssetLabel(asset.importance, CRITICAL_ASSET_IMPORTANCE_OPTIONS) || "—";
      const impact = resolveCriticalAssetLabel(asset.impact, CRITICAL_ASSET_IMPACT_OPTIONS) || "—";
      const dacp = resolveCriticalAssetLabel(asset.dacpSensitive, CRITICAL_ASSET_DACP_OPTIONS) || "—";
      const mfaParts = [];
      if (asset.mfaStatus) mfaParts.push(resolveCriticalAssetLabel(asset.mfaStatus, CRITICAL_ASSET_MFA_STATUS_OPTIONS));
      if (asset.mfaDetail) mfaParts.push(asset.mfaDetail);
      const mfaLabel = mfaParts.length ? mfaParts.join(" - ") : "—";
      return [
        asset.priority || index + 1,
        asset.beneficiary || "",
        productLabel || "",
        assetType,
        importance,
        asset.rto || "—",
        asset.rpo || "—",
        availabilityLabel,
        conf,
        integrity,
        impact,
        dacp,
        mfaLabel
      ];
    });
    sections.push({
      key: "critical-assets",
      title: t('reportSections.criticalAssets'),
      headers: [t('excelHeaders.assetPriority'), t('excelHeaders.assetBeneficiary'), t('excelHeaders.assetProduct'), t('excelHeaders.assetType'), t('excelHeaders.assetImportance'), t('excelHeaders.assetRto'), t('excelHeaders.assetRpo'), t('excelHeaders.assetAvailability'), t('excelHeaders.assetConfidentiality'), t('excelHeaders.assetIntegrity'), t('excelHeaders.assetImpact'), t('excelHeaders.assetDacp'), t('excelHeaders.assetMfa')],
      rows: assetRows
    });
    const projectRows = (appData.projectRisks || []).map((risk) => {
      const statusMeta = getProjectRiskStatusMeta(risk.status);
      const revision = risk.revisionNumber || "";
      return [
        risk.projectName || "",
        risk.beneficiary || "",
        risk.reportNumber || "",
        revision,
        risk.description || "",
        risk.analysisLink || "—",
        formatProjectRiskDate(risk.completionDate),
        statusMeta.label,
        risk.priority ? t('projectRisks.priority' + risk.priority.charAt(0).toUpperCase() + risk.priority.slice(1)) : ''
      ];
    });
    sections.push({
      key: "project-risks",
      title: t('reportSections.projectRisks'),
      headers: [t('excelHeaders.projectName'), t('excelHeaders.projectBeneficiary'), t('excelHeaders.projectReportNum'), t('excelHeaders.projectRevision'), t('excelHeaders.projectDescription'), t('excelHeaders.projectAnalysisLink'), t('excelHeaders.projectCompletionDate'), t('excelHeaders.projectStatus'), t('excelHeaders.projectPriority')],
      rows: projectRows
    });
    return sections;
  }
  function openGlobalExcelExportModal() {
    const sections = getAllSectionExportData();
    if (!sections.length) {
      showToast2(t('toasts.noDataToExport'), "warning");
      return;
    }
    const fields = sections.map((section) => ({
      name: `export_${section.key}`,
      label: `${section.title} (${section.rows.length})`,
      type: "checkbox",
      value: true,
      allowInReadOnly: true
    }));
    openModal(t('modals.globalExcelExport'), fields, (data) => {
      const selected = sections.filter((section) => data[`export_${section.key}`]);
      if (!selected.length) {
        showToast2(t('exportLabels.selectAtLeastOne'), "warning");
        return false;
      }
      const label = appData.title ? `${appData.title}_export_global` : "export_global";
      downloadExcelWorkbook(label, selected);
      return true;
    }, null, {
      saveLabel: t('exportLabels.exportExcelBtn'),
      cancelLabel: t('exportLabels.cancelBtn'),
      allowReadOnly: true,
      message: t('exportLabels.selectSections')
    });
    const modalBody = document.getElementById("modalBody");
    if (modalBody) {
      const toolbar = document.createElement("div");
      toolbar.className = "settings-actions modal__toolbar";
      const selectAllBtn = document.createElement("button");
      selectAllBtn.type = "button";
      selectAllBtn.className = "btn btn--outline btn--sm";
      selectAllBtn.textContent = t('exportLabels.selectAll');
      const clearBtn = document.createElement("button");
      clearBtn.type = "button";
      clearBtn.className = "btn btn--outline btn--sm";
      clearBtn.textContent = t('exportLabels.deselectAll');
      toolbar.append(selectAllBtn, clearBtn);
      const messageEl = modalBody.querySelector(".modal__message");
      if (messageEl && messageEl.parentNode) {
        messageEl.parentNode.insertBefore(toolbar, messageEl.nextSibling);
      } else if (modalBody.firstChild) {
        modalBody.insertBefore(toolbar, modalBody.firstChild);
      } else {
        modalBody.appendChild(toolbar);
      }
      const setAll = (checked) => {
        fields.forEach((field) => {
          const input = document.getElementById(`modal-input-${field.name}`);
          if (input && input.type === "checkbox") input.checked = checked;
        });
      };
      selectAllBtn.addEventListener("click", () => setAll(true));
      clearBtn.addEventListener("click", () => setAll(false));
    }
  }
  function exportAllSectionsExcel() {
    try {
      openGlobalExcelExportModal();
    } catch (error) {
      void 0;
      showToast2(t('toasts.excelExportError'), "error");
    }
  }
  function exportData() {
    // Bloquer l'export en mode démo
    if (DEMO_MODE) {
      showDemoBlockedMessage('L\'export des données');
      return;
    }
    try {
      ensureNis2Plan();
      ensureCriticalAssets();
      ensureProjectRisks();
      const data = {
        controls: appData.controls,
        actions: appData.actions,
        criticalAssets: appData.criticalAssets,
        criticalAssetsMeta: appData.criticalAssetsMeta,
        projectRisks: appData.projectRisks,
        risks: appData.risks,
        threats: appData.threats,
        nonconformities: appData.nonconformities,
        audits: appData.audits,
        kpis: appData.kpis,
        reviews: appData.reviews,
        stakeholders: appData.stakeholders,
        documents: appData.documents,
        documentReview: appData.documentReview,
        nis2Controls: appData.nis2Controls,
        nis2Plan: normalizeNis2Plan(appData.nis2Plan),
        recyfControls: appData.recyfControls,
        soclePillars: appData.soclePillars,
        targetMaturity: appData.targetMaturity,
        soa: appData.soa,
        riskAnalyses: appData.riskAnalyses,
        impactMatrices: appData.impactMatrices,
        riskThresholds: appData.riskThresholds,
        frameworkMapper: appData.frameworkMapper,
        kpiHistory: appData.kpiHistory,
        settings: appData.settings,
        nextActionId: appData.nextActionId,
        title: appData.title,
        cyfunLevel: appData.cyfunLevel,
        cyfunLang: appData.cyfunLang,
        recyfLevel: appData.recyfLevel,
        nis2Referential: appData.nis2Referential,
        // FIX-CYFUN-ARCHIVE-EXPORT (audit 2026-07-02) : l'archive des évaluations CyFun par
        // niveau (hors niveau courant) est synchronisée au serveur mais était absente de
        // l'export → perdue à la migration locale→serveur. applyImportedData la restaure déjà.
        _cyfunEvalArchive: appData._cyfunEvalArchive,
        _cyfunLevelChosen: appData._cyfunLevelChosen,
        // FIX-CYFUN-LOCK-PORTABILITY: inclure le verrou de niveau dans l'export
        // pour qu'un re-import ailleurs (autre instance, restauration, etc.)
        // préserve le verrou. Sans ce champ, l'export-import perdait le verrou.
        _cyfunLevelLocked: appData._cyfunLevelLocked,
        // Verrou du référentiel NIS2 (CyFun/ReCyF) : posé au 1er choix confirmé
        // (wizard/modale). Une fois vrai, le sélecteur de référentiel des Réglages est
        // désactivé — seule une réinitialisation permet d'en changer. Persisté/synchronisé
        // pour rester cohérent en local ET en mode serveur multi-utilisateurs.
        _nis2ReferentialLocked: appData._nis2ReferentialLocked,
        actionsHistory: appData.actionsHistory,
        auditLog: appData.auditLog,
        exportDate: (/* @__PURE__ */ new Date()).toISOString(),
        version: "2.0"
      };
      // === CSI-ME === (audit import-export) : estampiller l'ORIGINE du fichier AVANT le hash
      // d'intégrité. Un export fait en périmètre sous-entité porte _csiEntity (id+nom, jamais
      // la matrice de droits) ; un export du périmètre principal en mode multi-entités porte
      // _csiExportScope:'group'. La garde serveur CROSS_IMPORT_MISMATCH peut ainsi détecter
      // un réimport vers un autre périmètre (deux fichiers d'entités différentes étaient
      // indiscernables). Un export hors mode multi-entités reste inchangé.
      var _csiScopeTag = '';
      if (appData._csiEntity && appData._csiEntity.id) {
        data._csiEntity = { id: String(appData._csiEntity.id), name: appData._csiEntity.name ? String(appData._csiEntity.name) : '' };
        _csiScopeTag = '_' + String(appData._csiEntity.id);
      } else if (window.CSI_EntityManager && window.CSI_EntityManager.isMultiEntity && window.CSI_EntityManager.isMultiEntity()) {
        data._csiExportScope = 'group';
        _csiScopeTag = '_' + (t('entities.exportGroupTag', 'entite-principale'));
      }
      // F-04: Hash d'intégrité pour détecter la corruption
      if (window.CSI_LegacyImport && typeof window.CSI_LegacyImport.computeIntegrityHash === 'function') {
        data._integrity = window.CSI_LegacyImport.computeIntegrityHash(data);
      }
      const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      const cryptoObj = (typeof crypto !== "undefined" && crypto) || (typeof window !== "undefined" ? window.crypto : void 0);
      const exportId = cryptoObj && typeof cryptoObj.randomUUID === "function" ? cryptoObj.randomUUID() : Math.random().toString(36).slice(2);
      a.download = `CyberAssistant_Export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}${_csiScopeTag}_${exportId}.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      addAuditEntry('export', 'system', a.download);
      showToast2(t('toasts.exportSuccess'), "success");
    } catch (error) {
      void 0;
      showToast2(t('toasts.exportError', "Impossible d'exporter les données."), "error");
    }
  }
  function formatImportDate(data) {
    const candidates = [
      data?.exportDate,
      data?.lastUpdated,
      data?.updatedAt,
      data?.lastModified
    ];
    for (const candidate of candidates) {
      if (!candidate) continue;
      const ts = Date.parse(candidate);
      if (Number.isFinite(ts)) return new Date(ts).toLocaleString();
    }
    return "Inconnue";
  }
  function buildImportSummary(data, sourceLabel) {
    const controlsCount = Array.isArray(data?.controls) ? data.controls.length : 0;
    const actionsCount = Array.isArray(data?.actions) ? data.actions.length : 0;
    const risksCount = Array.isArray(data?.risks) ? data.risks.length : 0;
    const projectRisksCount = Array.isArray(data?.projectRisks) ? data.projectRisks.length : 0;
    const nis2ControlsCount = Array.isArray(data?.nis2Controls) ? data.nis2Controls.length : 0;
    const hasNis2Plan = !!data?.nis2Plan;
    const dateLabel = formatImportDate(data);
    const lines = [
      `Source: ${sourceLabel}`,
      `Contrôles ISO: ${controlsCount}`,
      `Contrôles NIS2: ${nis2ControlsCount}`,
      `Actions: ${actionsCount}`,
      `Risques: ${risksCount}`,
      `Risques projets: ${projectRisksCount}`,
      `Analyses de risques: ${Array.isArray(data?.riskAnalyses) ? data.riskAnalyses.length : 0}`,
      `Matrices d'impacts: ${Array.isArray(data?.impactMatrices?.matrices) ? data.impactMatrices.matrices.length : 0}`,
      `Plan NIS2: ${hasNis2Plan ? 'Oui (avec budgets)' : 'Non (sera initialisé)'}`,
      `Dernière MAJ: ${dateLabel}`,
      ""
    ];
    if (!hasNis2Plan && nis2ControlsCount > 0) {
      lines.push("⚠️ " + t('import.oldFormatDetected', 'Ancien format détecté : le plan NIS2 et les budgets'));
      lines.push("   " + t('import.willBeInitialized', 'seront initialisés avec les valeurs par défaut.'));
      lines.push("");
    }
    lines.push(t('import.warningOverwrite', 'Attention : cette opération va écraser les données locales.'));
    return lines.join("\n");
  }
  function applyImportedData(data) {
    // === CSI-ME === (audit import-export) : origine du FICHIER, transmise telle quelle au
    // serveur (champ importOrigin du push post-import). Le push porte le _csiEntity du
    // périmètre COURANT (ré-ingéré), pas celui du fichier — sans ce fil explicite, la garde
    // anti-import-croisé ne voyait jamais l'origine réelle (origin==target à tous les coups).
    var _csiImportOrigin = null;
    try {
      if (data && data._csiEntity && data._csiEntity.id) _csiImportOrigin = String(data._csiEntity.id);
      else if (data && data._csiExportScope === 'group') _csiImportOrigin = 'group';
    } catch (e) { /* marqueur absent — fichier antérieur à la feature */ }
    try {
      addHistorySnapshot(t('snapshots.backupBeforeImport', 'Sauvegarde avant import'), { tag: "auto" });
    } catch (err) {
      void 0;
    }
    // Normaliser les contrôles (gestion de l'ancien format avec selected/justification)
    appData.controls = normalizeControls((data.controls || []).map(c => {
      const { selected, justification, ...rest } = c;
      if (typeof rest.justificationControl === "undefined") rest.justificationControl = "";
      return rest;
    }));

    // Normaliser SOA (compatibilité avec ancien format à 3 champs)
    if (Array.isArray(data.soa)) {
      appData.soa = normalizeSoa(data.soa);
    } else {
      appData.soa = normalizeSoa((data.controls || []).map(c => ({
        controlId: c.id,
        applicable: c.selected !== undefined ? c.selected : true,
        justification: c.justification || ""
      })));
    }

    // Normaliser toutes les autres sections avec compatibilité ascendante
    appData.actions = normalizeActions(data.actions || []);
    appData.criticalAssets = normalizeCriticalAssets(data.criticalAssets || []);
    appData.criticalAssetsMeta = normalizeCriticalAssetsMeta(data.criticalAssetsMeta);
    appData.projectRisks = normalizeProjectRisks(data.projectRisks || []);
    appData.risks = normalizeRisks(data.risks || []);
    ensureRiskIds();
    appData.threats = normalizeThreats(data.threats || []);
    // Recalculate risk scores and levels after import
    if (typeof calculateRiskLevel === 'function') {
      (appData.risks || []).forEach(function(risk) {
        if (risk.impact && risk.probability) {
          risk.score = (typeof risk.impact === 'number' ? risk.impact : parseInt(risk.impact, 10) || 0) * (typeof risk.probability === 'number' ? risk.probability : parseInt(risk.probability, 10) || 0);
          risk.level = calculateRiskLevel(risk.score);
        }
      });
      appData.threats.forEach(function(threat) {
        if (threat.impact && threat.likelihood) {
          threat.score = (typeof threat.impact === 'number' ? threat.impact : parseInt(threat.impact, 10) || 0) * (typeof threat.likelihood === 'number' ? threat.likelihood : parseInt(threat.likelihood, 10) || 0);
          threat.level = calculateRiskLevel(threat.score);
        }
      });
    }
    appData.nonconformities = normalizeNonconformities(data.nonconformities || []);
    appData.audits = normalizeAudits(data.audits || []);
    appData.kpis = normalizeKpis(data.kpis || []);
    appData.reviews = normalizeReviews(data.reviews || []);
    appData.stakeholders = normalizeStakeholders(data.stakeholders || []);
    appData.documents = normalizeDocuments(data.documents || []);
    appData.documentReview = createDefaultDocumentReview(data.documentReview || []);
    const tm = data.targetMaturity || {};
    appData.targetMaturity = {
      iso27002: typeof tm.iso27002 === "number" ? tm.iso27002 : parseInt(tm.iso27002, 10) || 3,
      nis2: buildNis2Maturity(tm.nis2 || {}),
      nis2Target: typeof tm.nis2Target === "number" ? tm.nis2Target : parseInt(tm.nis2Target, 10) || 3
    };
    const nis2PlanDefaults = getEmptyNis2Plan();
    let nis2PlanMigrated = false;
    if (data.nis2Plan) {
      const plan = data.nis2Plan;
      appData.nis2Plan = {
        phases: Array.isArray(plan.phases) && plan.phases.length ? plan.phases : nis2PlanDefaults.phases,
        maturity: {
          labels: Array.isArray(plan.maturity?.labels) && plan.maturity.labels.length ? plan.maturity.labels : nis2PlanDefaults.maturity.labels,
          current: Array.isArray(plan.maturity?.current) && plan.maturity.current.length ? plan.maturity.current : nis2PlanDefaults.maturity.current,
          target2027: typeof plan.maturity?.target2027 === "number" ? plan.maturity.target2027 : nis2PlanDefaults.maturity.target2027,
          target2028: typeof plan.maturity?.target2028 === "number" ? plan.maturity.target2028 : nis2PlanDefaults.maturity.target2028
        },
        statuses: Array.isArray(plan.statuses) ? plan.statuses : nis2PlanDefaults.statuses,
        budgetBlocks: Array.isArray(plan.budgetBlocks) ? plan.budgetBlocks : nis2PlanDefaults.budgetBlocks,
        customBlocks: Array.isArray(plan.customBlocks) ? plan.customBlocks : nis2PlanDefaults.customBlocks,
        domains: Array.isArray(plan.domains) ? plan.domains : nis2PlanDefaults.domains,
        // Budget Cybersécurité : préserver catégories, années et lignes à l'import.
        // (undefined => normalizeNis2Plan applique les défauts / migre depuis les actions)
        budgetCategories: Array.isArray(plan.budgetCategories) ? plan.budgetCategories : undefined,
        budgetYears: Array.isArray(plan.budgetYears) ? plan.budgetYears : undefined,
        budgetLines: Array.isArray(plan.budgetLines) ? plan.budgetLines : undefined
      };
    } else {
      // Migration automatique des anciens formats sans nis2Plan
      nis2PlanMigrated = true;
      appData.nis2Plan = cloneDefaultNis2Plan({ includeActions: true });

      // Tenter de récupérer la maturité depuis nis2Controls si disponible
      if (Array.isArray(data.nis2Controls) && data.nis2Controls.length > 0) {
        // Mapper les CMM des contrôles NIS2 vers des valeurs numériques
        const cmmToValue = { 'Initial': 1, 'Répétable': 2, 'Défini': 3, 'Géré': 4, 'Optimisé': 5 };
        const maturityValues = data.nis2Controls.map(ctrl => {
          const cmmVal = cmmToValue[ctrl.cmm] || 0;
          return cmmVal;
        });
        // Calculer une moyenne globale pour target2027/2028
        const avgMaturity = maturityValues.length > 0
          ? Math.round(maturityValues.reduce((a, b) => a + b, 0) / maturityValues.length)
          : 0;
        appData.nis2Plan.maturity.target2027 = Math.min(avgMaturity + 1, 5);
        appData.nis2Plan.maturity.target2028 = Math.min(avgMaturity + 2, 5);
      }

      void 0;
    }
    appData.soclePillars = normalizeSoclePillars(data.soclePillars);
    if (Array.isArray(data.riskAnalyses)) {
      appData.riskAnalyses = data.riskAnalyses.map(function(a) {
        return typeof window.CSI_RiskAnalysis !== 'undefined' && window.CSI_RiskAnalysis.normalizeAnalysis
          ? window.CSI_RiskAnalysis.normalizeAnalysis(a) : a;
      });
    } else {
      // AUDIT-H2: Reset à [] quand le fichier importé n'a pas de riskAnalyses.
      // Avant: le champ n'était pas remis à zéro, les analyses de la session
      // précédente survivaient silencieusement à l'import.
      appData.riskAnalyses = [];
    }
    if (data.impactMatrices && typeof normalizeImpactMatrices === 'function') {
      appData.impactMatrices = normalizeImpactMatrices(data.impactMatrices);
    } else {
      appData.impactMatrices = data.impactMatrices || appData.impactMatrices || null;
    }
    // AUDIT-M3: Normaliser riskThresholds et reset quand absent.
    // Avant: assignation brute sans validation, et pas de reset si absent
    // (les anciens seuils de la session survivaient silencieusement à l'import).
    if (data.riskThresholds && typeof data.riskThresholds === 'object') {
      appData.riskThresholds = data.riskThresholds;
    } else {
      appData.riskThresholds = null;
    }
    if (data.frameworkMapper) {
      appData.frameworkMapper = {
        enabledFrameworks: Array.isArray(data.frameworkMapper.enabledFrameworks) ? data.frameworkMapper.enabledFrameworks : _defaultEnabledFrameworks(),
        customMappings: Array.isArray(data.frameworkMapper.customMappings) ? data.frameworkMapper.customMappings : [],
        userNotes: (data.frameworkMapper.userNotes && typeof data.frameworkMapper.userNotes === 'object') ? data.frameworkMapper.userNotes : {}
      };
    }
    // Normalize imported enabledFrameworks for the current edition
    if (appData.frameworkMapper && Array.isArray(appData.frameworkMapper.enabledFrameworks)) {
      var _impFws = window._csiFrameworks || {};
      var _impEf = appData.frameworkMapper.enabledFrameworks;
      [['iso27002', 'open-security-controls'], ['iso27001', 'open-security-management']].forEach(function(pair) {
        var absent = !_impFws[pair[0]] ? pair[0] : (!_impFws[pair[1]] ? pair[1] : null);
        var present = absent === pair[0] ? pair[1] : pair[0];
        if (!absent) return;
        var idx = _impEf.indexOf(absent);
        if (idx >= 0) {
          if (_impEf.indexOf(present) < 0) _impEf[idx] = present;
          else _impEf.splice(idx, 1);
        }
      });
    }
    // Auto-detect: if imported data has NIS2/CyFun controls, enable framework
    if (Array.isArray(data.nis2Controls) && data.nis2Controls.length > 0) {
      if (!appData.frameworkMapper) appData.frameworkMapper = { enabledFrameworks: [_getActiveControlsFrameworkId() || 'iso27002'], customMappings: [], userNotes: {} };
      var _efws = appData.frameworkMapper.enabledFrameworks;
      if (_efws.indexOf('nis2') < 0) _efws.push('nis2');
      if (_efws.indexOf('cyfun2025') < 0) _efws.push('cyfun2025');
    }
    ensureNis2Plan();
    ensureCriticalAssets();
    normalizeCriticalAssetLinks(appData.actions);
    normalizeCriticalAssetLinks(appData.risks);
    normalizeCriticalAssetLinks(appData.threats);
    normalizeCriticalAssetLinks(appData.nonconformities);
    // Restore actionsHistory and auditLog if present
    if (data.actionsHistory && typeof data.actionsHistory === 'object') appData.actionsHistory = data.actionsHistory;
    if (Array.isArray(data.auditLog)) appData.auditLog = data.auditLog;
    // Restore CyFun settings from imported data BEFORE loading controls
    if (data.cyfunLevel !== undefined) appData.cyfunLevel = data.cyfunLevel;
    if (data._cyfunEvalArchive !== undefined) appData._cyfunEvalArchive = data._cyfunEvalArchive; // FIX-CYFUN-LEVEL-LOSS
    if (data.cyfunLang !== undefined) appData.cyfunLang = data.cyfunLang;
    if (data._cyfunLevelChosen !== undefined) appData._cyfunLevelChosen = data._cyfunLevelChosen;
    // FIX-CYFUN-LOCK-PORTABILITY: restaurer le verrou de niveau au re-import
    if (data._cyfunLevelLocked !== undefined) appData._cyfunLevelLocked = data._cyfunLevelLocked;
    if (data._nis2ReferentialLocked !== undefined) appData._nis2ReferentialLocked = data._nis2ReferentialLocked;
    const defaultNis2 = loadNis2Controls();
    if (Array.isArray(data.nis2Controls) && data.nis2Controls.length > 0) {
      // AUDIT-C3: Détecter le format legacy NIS2 (23 contrôles v1.0 avec fonction/categorie, sans ref).
      // Avant ce fix, les contrôles legacy n'avaient ni ref ni id compatible avec CyFun 2025,
      // donc found={} pour chaque contrôle → perte SILENCIEUSE de toutes les valeurs utilisateur.
      var _nis2Format = typeof detectLegacyNis2Format === 'function'
        ? detectLegacyNis2Format(data.nis2Controls) : 'unknown';
      var _legacyCategoryMaturity = null;
      if (_nis2Format === 'legacy_23') {
        _legacyCategoryMaturity = typeof migrateLegacyNis2Controls === 'function'
          ? migrateLegacyNis2Controls(data.nis2Controls) : {};
        void 0;
      }

      appData.nis2Controls = defaultNis2.map(d => {
        // Pour le format legacy: récupérer la maturité par catégorie NIST CSF
        if (_legacyCategoryMaturity) {
          var catMatch = (d.ref || '').match(/^([A-Z]{2}\.[A-Z]{2})/);
          var cat = catMatch ? catMatch[1] : '';
          var migratedCmm = _legacyCategoryMaturity[cat] || 0;
          return { ...d, docMaturity: migratedCmm, implMaturity: migratedCmm,
            justification: '', evidence: '',
            owner: '', reviewer: '', reviewFrequency: '', lastEvaluation: '', nextReviewDate: '',
            status: migratedCmm > 0 ? 'en_cours' : d.status, cmm: migratedCmm || d.cmm };
        }
        // Format moderne: match by ref (stable across versions) then fallback to id
        const found = data.nis2Controls.find(c => (c.ref && c.ref === d.ref) || c.id === d.id) || {};
        return { ...d, docMaturity: found.docMaturity || 0, implMaturity: found.implMaturity || 0,
          justification: found.justification || '', evidence: found.evidence || '',
          owner: found.owner || '', reviewer: found.reviewer || '',
          reviewFrequency: found.reviewFrequency || '',
          lastEvaluation: found.lastEvaluation || '',
          nextReviewDate: found.nextReviewDate || '',
          // FIX-NIS2-COMMENTS-IMPORT (audit 2026-07-02) : préserver les commentaires du
          // contrôle NIS2/CyFun (manageComments) — omis de la liste fermée, ils étaient
          // perdus à l'import (les chemins load/level-change les gardent via {...d,...found}).
          comments: window.normalizeComments(Array.isArray(found.comments) && found.comments.length ? found.comments : (found.comments || d.comments)),
          status: found.status || d.status, cmm: found.cmm || d.cmm };
      });
      if (_legacyCategoryMaturity) {
        showToast2(t('toasts.nis2LegacyMigrated', 'Contrôles NIS2 migrés depuis l\'ancien format (23 contrôles). Vérifiez les scores.'), 'info', 8000);
      }
    } else {
      appData.nis2Controls = defaultNis2;
    }
    // Restore ReCyF (France) settings + measures from imported data
    if (data.recyfLevel !== undefined) appData.recyfLevel = data.recyfLevel;
    if (data.nis2Referential !== undefined) appData.nis2Referential = data.nis2Referential;
    if (!appData.recyfLevel) appData.recyfLevel = 'importante';
    // Modèle NIS2 mutuellement exclusif : aligner enabledFrameworks sur le référentiel importé
    // (le bloc d'auto-détection plus haut a pu activer cyfun2025 alors que la France est active).
    if (typeof reconcileNis2Frameworks === 'function') reconcileNis2Frameworks();
    if (typeof window.rebuildRecyfThemes === 'function') window.rebuildRecyfThemes(appData.recyfLevel);
    if (Array.isArray(data.recyfControls) && data.recyfControls.length > 0 && typeof loadRecyfControls === 'function') {
      var _impRecyfByRef = {};
      data.recyfControls.forEach(function(c) { if (c && c.ref) _impRecyfByRef[c.ref] = c; });
      appData.recyfControls = loadRecyfControls().map(function(c) {
        var found = _impRecyfByRef[c.ref];
        if (found) {
          c.conformity = found.conformity || 'non_evalue';
          c.justification = found.justification || '';
          c.owner = found.owner || '';
          c.reviewer = found.reviewer || '';
          c.reviewFrequency = found.reviewFrequency || '';
          c.lastEvaluation = found.lastEvaluation || '';
          c.nextReviewDate = found.nextReviewDate || '';
          c.evidence = found.evidence || '';
          // Même classe de préservation que FIX-NIS2-COMMENTS-IMPORT côté CyFun :
          // sans cette ligne, les commentaires ReCyF seraient perdus à chaque import.
          c.comments = window.normalizeComments(found.comments);
        }
        return c;
      });
    } else {
      appData.recyfControls = [];
    }
    // AUDIT-H1: Sécuriser nextActionId contre les collisions d'ID.
    // Avant: `data.nextActionId || 0` mettait la valeur à 0 quand absent/null,
    // causant des doublons d'ID si des actions existaient déjà.
    var _importedNextId = (typeof data.nextActionId === 'number' && data.nextActionId > 0) ? data.nextActionId : 0;
    var _maxExistingActionId = 0;
    if (Array.isArray(appData.actions) && appData.actions.length > 0) {
      appData.actions.forEach(function(a) {
        if (a && a.id) {
          var n = parseInt(String(a.id).replace(/\D/g, ''), 10);
          if (Number.isFinite(n) && n > _maxExistingActionId) _maxExistingActionId = n;
        }
      });
    }
    appData.nextActionId = Math.max(_importedNextId, _maxExistingActionId + 1);
    if (_importedNextId < _maxExistingActionId + 1) {
      void 0;
    }
    if (data.kpiHistory) {
      appData.kpiHistory = data.kpiHistory;
    }
    if (data.settings && typeof data.settings === 'object') {
      appData.settings = data.settings;
    }
    appData.title = data.title || appData.title;
    updateAppTitle();
    warnMissingSubcategories();
    // F-03: Validation cross-references post-import
    if (typeof window.validateBeforeSave === 'function') {
      try {
        var _postValidation = window.validateBeforeSave(appData);
        if (_postValidation.warnings && _postValidation.warnings.length > 0) {
          void 0;
          showToast2(t('toasts.importIntegrityWarnings', 'Import terminé avec ' + _postValidation.warnings.length + ' avertissement(s) d\'intégrité'), "warning");
        }
      } catch (_ve) { void 0; }
    }
    // F-03: Post-import validation via legacy-import module
    if (window.CSI_LegacyImport && typeof window.CSI_LegacyImport.validatePostImport === 'function') {
      try {
        var _postReport = window.CSI_LegacyImport.validatePostImport(appData);
        if (_postReport.warnings && _postReport.warnings.length > 0) {
          void 0;
        }
      } catch (_le) { /* non bloquant */ }
    }
        addAuditEntry('import', 'system', t('audit.importFile', 'Import fichier'));
    updateDashboard();
    loadKpiTable();
    if (nis2PlanMigrated) {
      showToast2(t('toasts.importNis2Success', 'Import réussi - Plan NIS2 initialisé (ancien format)'), "info");
      setTimeout(() => {
        showToast2(t('toasts.importNis2Hint', "Retrouvez vos budgets dans l'onglet Budget Cybersécurité (Gouvernance)"), "info");
      }, 2500);
    } else {
      showToast2(t('toasts.importSuccess'), "success");
    }
    // PASS2-001: Check saveData return — don't reload if save failed (data would be lost)
    if (!saveData()) {
      showToast2(t('toasts.saveFailed', 'Erreur de sauvegarde — les données importées n\'ont pas été persistées. Réessayez.'), 'error', 10000);
      return;
    }
    // AUDIT-C1: Force sync serveur AVANT le reload (le debounce de 2s dans requestServerSync
    // était systématiquement tué par le reload à 100ms — les collaborateurs ne recevaient jamais
    // les données importées). On utilise forceServerSync() qui est synchrone/immédiat.
    var reloadDelay = nis2PlanMigrated ? 4500 : 100;
    if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode && window.CSI_SyncManager.isServerMode()
        && window.CSI_SyncManager.forceServerSync) {
      // FIX-IMPORT-RELOAD-GUARD (audit 2026-07-02, C2) : ne recharger QUE si le push serveur
      // a réellement réussi. En mode serveur, loadData() repart du serveur au reload ; recharger
      // après un échec (423 verrou perdu, 402 licence, 429, réseau, 'Sync in progress'...) jette
      // les données importées (jamais écrites en localStorage en mode serveur). forceServerSync
      // RÉSOUT {success:false} sur ces échecs (il ne rejette pas) → on doit tester success.
      var _importSyncFailMsg = function(m) {
        return t('toasts.importSyncFailed', "Import appliqué en mémoire mais NON enregistré sur le serveur : {{msg}}. Reprenez la main (mode édition) puis réimportez ; ne fermez pas l'onglet.").replace('{{msg}}', m || t('toasts.syncGenericError', 'échec de synchronisation'));
      };
      var _postImportSync = function(opts) {
        // === CSI-ME === (audit import-export) : transmettre l'origine du fichier importé —
        // c'est elle (et non le _csiEntity du périmètre courant) que la garde serveur compare.
        var _o = opts || {};
        if (_csiImportOrigin && !_o.importOrigin) _o.importOrigin = _csiImportOrigin;
        return window.CSI_SyncManager.forceServerSync(undefined, _o).then(function(res) {
          // === CSI-ME === (audit 2026-07-03) rattachement changé pendant l'import : recharger
          // (le /me ré-ingère le nouveau contexte) — l'import devra être refait au bon périmètre.
          if (res && res.entityContextChanged) {
            showToast2(t('toasts.entityContextChanged', "Votre rattachement d'entité a changé — l'application va se recharger pour appliquer votre nouveau périmètre. Une copie de vos modifications non enregistrées a été téléchargée."), 'warning', 8000);
            setTimeout(function() { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }, 3000);
            return;
          }
          // === CSI-ME === (audit C1) fichier d'un autre périmètre : confirmation explicite.
          if (res && res.crossImport) {
            var _ok = window.confirm(t('entities.crossImportConfirm', 'Ce fichier provient d\'un autre périmètre ({{origin}}).\n\nImporter ici REMPLACERA les données du périmètre courant ({{target}}). Confirmer ?').replace('{{origin}}', res.origin || '?').replace('{{target}}', res.target || '?'));
            if (_ok) { _postImportSync({ allowCrossImport: true }); }
            else { showToast2(t('toasts.crossImportCancelled', 'Import inter-périmètre annulé — données inchangées.'), 'info', 8000); }
            return;
          }
          if (res && res.success === false) {
            showToast2(_importSyncFailMsg(res.message), 'error', 15000);
            return; // pas de reload : on préserve l'import en mémoire (beforeunload protège)
          }
          // === CSI-ME === (audit 2026-07-03, K5) sections du fichier écartées par les
          // droits du périmètre : le dire — « Import réussi » seul était trompeur.
          if (res && res.ignoredSections && res.ignoredSections.length) {
            var _igList = res.ignoredSections.map(function(sid) {
              return (window.CSI_EntityUI && window.CSI_EntityUI.sectionLabel) ? window.CSI_EntityUI.sectionLabel(sid) : sid;
            }).join(', ');
            showToast2(t('toasts.importIgnoredSections', 'Sections en lecture seule pour votre périmètre, non modifiées par l\'import : {{list}}').replace('{{list}}', _igList), 'info', 12000);
            reloadDelay = Math.max(reloadDelay, 5000); // laisser lire le toast avant le reload
          }
          setTimeout(function() { location.reload(); }, reloadDelay);
        });
      };
      _postImportSync().catch(function(err) {
        void 0;
        showToast2(_importSyncFailMsg(err && err.message), 'error', 15000);
      });
    } else {
      setTimeout(function() { location.reload(); }, reloadDelay);
    }
  }
  function importData() {
    // Bloquer l'import en mode démo
    if (DEMO_MODE) {
      showDemoBlockedMessage('L\'import des données');
      return;
    }
    if (!ensureEditMode("import data")) return;
    const fileInput = document.getElementById("fileInput");
    if (!fileInput) { void 0; return; }
    fileInput.click();
    fileInput.onchange = function(event) {
      const file = event.target.files[0];
      if (!file) return;
      // Validate file extension (.json preferred, .smsi accepted for backward compat)
      var ext = (file.name || '').split('.').pop().toLowerCase();
      if (ext !== 'json' && ext !== 'smsi' && ext !== 'analyse') {
        showToast2(t('toasts.invalidFileType', 'Format de fichier invalide. Veuillez sélectionner un fichier .json ou .smsi.'), "error");
        fileInput.value = "";
        return;
      }
      // F-02: Guard taille fichier (max 10 MB)
      if (file.size > 10 * 1024 * 1024) {
        showToast2(t('toasts.fileTooLarge', 'Le fichier est trop volumineux (max 10 MB).'), "error");
        fileInput.value = "";
        return;
      }
      const reader = new FileReader();
      reader.onload = function(e) {
        try {
          const data = JSON.parse(e.target.result);
          // Route .analyse files to dedicated importer
          if (data.type === 'cyber-assistant-risk-analysis') {
            if (typeof window.raImportAnalyseFromData === 'function') {
              window.raImportAnalyseFromData(data, file.name);
            } else {
              showToast2(t('toasts.riskAnalysisModuleNotLoaded', "Module d'analyse de risques non chargé."), "error");
            }
            fileInput.value = "";
            return;
          }
          // === CSI-ME === (import/export entités, 2026-07-09) Import d'une SAUVEGARDE
          // COMPLÈTE (bundle csi-backup-all-v1) : choisir QUELLE entité charger dans cet
          // espace de travail (utile en Corporate Local mono-poste ; dossier §6.3/R9).
          // DOIT précéder la garde data.controls (un bundle n'a pas de controls top-level).
          // Noms d'entités rendus via <option>.textContent (openModal) — anti-XSS.
          if (data._format === 'csi-backup-all-v1') {
            var _bSrcs = [];
            if (data.group && Array.isArray(data.group.controls)) _bSrcs.push({ value: 'group', label: t('entities.mainEntity', 'Entité principale') });
            var _bEnts = data.entities || {};
            for (var _bk in _bEnts) { if (Object.prototype.hasOwnProperty.call(_bEnts, _bk) && _bEnts[_bk] && Array.isArray(_bEnts[_bk].controls)) _bSrcs.push({ value: _bk, label: _bk }); }
            if (!_bSrcs.length) {
              showToast2(t('toasts.bundleNoData', 'La sauvegarde ne contient aucune entité exploitable.'), 'error');
              fileInput.value = ''; return;
            }
            var _applyChosen = function(key) {
              var chosen = (key === 'group') ? data.group : (data.entities && data.entities[key]);
              if (!chosen || !Array.isArray(chosen.controls)) { showToast2(t('toasts.importError'), 'error'); return; }
              // Strip défensif des méta serveur / marqueurs de périmètre (hygiène : l'allowlist
              // d'applyImportedData ne les persiste pas, mais on nettoie le dataset choisi).
              var clean = {}; for (var ck in chosen) { if (Object.prototype.hasOwnProperty.call(chosen, ck)) clean[ck] = chosen[ck]; }
              delete clean._dataEpoch; delete clean._serverSyncDate; delete clean._restoredFrom; delete clean._rev;
              delete clean._csiEntity; delete clean._csiExportScope; delete clean._csiGroupView; delete clean._format; delete clean._createdAt;
              if (window.CSI_LegacyImport && typeof window.CSI_LegacyImport.analyzeImport === 'function') {
                var rep = window.CSI_LegacyImport.analyzeImport(clean);
                window.CSI_LegacyImport.showMigrationModal(rep, clean, function(migrated) {
                  try { applyImportedData(migrated); } catch (err) { void 0; showToast2(t('toasts.importError'), 'error'); }
                }, function() { /* annulé */ });
              } else {
                try { applyImportedData(clean); } catch (err) { void 0; showToast2(t('toasts.importError'), 'error'); }
              }
            };
            if (_bSrcs.length === 1) { _applyChosen(_bSrcs[0].value); }
            else {
              openModal(t('entities.pickEntityToLoadTitle', 'Charger une entité'), [
                { name: 'entityPick', label: t('entities.pickEntityToLoad', 'Quelle entité charger dans cet espace de travail ?'), type: 'select', options: _bSrcs, value: _bSrcs[0].value }
              ], function(vals) { _applyChosen(vals && vals.entityPick ? vals.entityPick : _bSrcs[0].value); },
                null, { saveLabel: t('import.importBtn', 'Importer'), cancelLabel: t('common.cancel', 'Annuler') });
            }
            fileInput.value = '';
            return;
          }
          if (!data.controls || !Array.isArray(data.controls)) throw new Error("Format de fichier invalide");
          // F-05: Délégation au module legacy-import si disponible
          if (window.CSI_LegacyImport && typeof window.CSI_LegacyImport.analyzeImport === 'function') {
            var _importReport = window.CSI_LegacyImport.analyzeImport(data);
            window.CSI_LegacyImport.showMigrationModal(_importReport, data, function(migratedData) {
              try {
                // === CSI-ME === (audit import-export) : préserver les marqueurs d'origine du
                // FICHIER à travers la migration legacy (la garde anti-import-croisé en dépend).
                if (migratedData && data && data._csiEntity && !migratedData._csiEntity) migratedData._csiEntity = data._csiEntity;
                if (migratedData && data && data._csiExportScope && !migratedData._csiExportScope) migratedData._csiExportScope = data._csiExportScope;
                applyImportedData(migratedData);
              } catch (error) {
                void 0;
                showToast2(t('toasts.importError'), "error");
              }
            }, function() { /* annulé */ });
          } else {
          // Fallback: flow standard si le module n'est pas chargé
          const summary = buildImportSummary(data, file.name ? `${t('import.file', 'Fichier')} ${file.name}` : t('import.importedFile', 'Fichier importé'));
          openModal(t('modals.confirmImport'), [
            { name: "summary", label: t('import.summaryBeforeImport', 'Résumé avant import'), type: "textarea", value: summary, readOnly: true }
          ], () => {
            try {
              applyImportedData(data);
            } catch (error) {
              void 0;
              showToast2(t('toasts.importError'), "error");
              return false;
            }
          }, null, { saveLabel: t('import.importBtn', 'Importer'), cancelLabel: t('common.cancel', 'Annuler') });
          }
        } catch (error) {
          void 0;
          showToast2(t('toasts.importError'), "error");
        } finally {
          fileInput.value = "";
        }
      };
      reader.readAsText(file);
    };
  }
  function runTestSuite() {
    showToast2(t('toasts.testsRunning', 'Exécution des tests en cours...'), "info");

    const tests = [];
    let passed = 0;
    let failed = 0;

    // Helper pour ajouter un test
    function addTest(name, fn) {
      tests.push({ name, fn });
    }

    // Test 1: Vérification de la structure appData
    addTest("Structure appData valide", () => {
      return appData && Array.isArray(appData.controls) && Array.isArray(appData.soa);
    });

    // Test 2: Fonction isControlNonApplicable existe
    addTest("Fonction isControlNonApplicable existe", () => {
      return typeof isControlNonApplicable === "function";
    });

    // Test 3: Fonction getSoaJustification existe
    addTest("Fonction getSoaJustification existe", () => {
      return typeof getSoaJustification === "function";
    });

    // Test 4: Fonction navigateToSoaControl existe
    addTest("Fonction navigateToSoaControl existe", () => {
      return typeof navigateToSoaControl === "function";
    });

    // Test 5: Test isControlNonApplicable avec contrôle applicable
    addTest("isControlNonApplicable retourne false pour contrôle applicable", () => {
      // Créer une entrée SoA temporaire pour le test
      const testControlId = "TEST_CONTROL_APPLICABLE";
      const originalSoa = [...appData.soa];
      appData.soa.push({ controlId: testControlId, applicable: true, justification: "" });
      const result = isControlNonApplicable(testControlId) === false;
      appData.soa = originalSoa;
      return result;
    });

    // Test 6: Test isControlNonApplicable avec contrôle non applicable
    addTest("isControlNonApplicable retourne true pour contrôle non applicable", () => {
      const testControlId = "TEST_CONTROL_NON_APPLICABLE";
      const originalSoa = [...appData.soa];
      appData.soa.push({ controlId: testControlId, applicable: false, justification: "Test" });
      const result = isControlNonApplicable(testControlId) === true;
      appData.soa = originalSoa;
      return result;
    });

    // Test 7: Test getSoaJustification
    addTest("getSoaJustification retourne la justification", () => {
      const testControlId = "TEST_CONTROL_JUSTIF";
      const originalSoa = [...appData.soa];
      appData.soa.push({ controlId: testControlId, applicable: false, justification: "Ma justification test" });
      const result = getSoaJustification(testControlId) === "Ma justification test";
      appData.soa = originalSoa;
      return result;
    });

    // Test 8: Contrôles ISO existent
    addTest("Contrôles ISO 27002 présents", () => {
      return (appData.controls || []).length > 0;
    });

    // Test 9: Chaque contrôle a un ID
    addTest("Chaque contrôle ISO a un ID", () => {
      return (appData.controls || []).every(c => c.id && typeof c.id === "string");
    });

    // Test 10: Entrées SoA correspondent aux contrôles
    addTest("Entrées SoA valides (controlId existe)", () => {
      const controlIds = (appData.controls || []).map(c => c.id);
      return appData.soa.every(s => controlIds.includes(s.controlId));
    });

    // Test 11: Navigation module dashboard fonctionne
    addTest("Module dashboard existe", () => {
      return document.getElementById("dashboard") !== null;
    });

    // Test 12: Module contrôles existe
    addTest("Module contrôles ISO existe", () => {
      return document.getElementById("controls") !== null;
    });

    // Test 13: Module SoA existe
    addTest("Module SoA existe", () => {
      return document.getElementById("soa") !== null;
    });

    // Test 14: Table contrôles body existe
    addTest("Table contrôles body existe", () => {
      return document.getElementById("controlsTableBody") !== null;
    });

    // Test 15: Table SoA body existe
    addTest("Table SoA body existe", () => {
      return document.getElementById("soaTableBody") !== null;
    });

    // Test 16: Fonction openModal existe
    addTest("Fonction openModal existe", () => {
      return typeof openModal === "function";
    });

    // Test 17: Fonction closeModal existe
    addTest("Fonction closeModal existe", () => {
      return typeof closeModal === "function";
    });

    // Test 18: Fonction loadControlsTable existe
    addTest("Fonction loadControlsTable existe", () => {
      return typeof loadControlsTable === "function";
    });

    // Test 19: Fonction loadSoaTable existe
    addTest("Fonction loadSoaTable existe", () => {
      return typeof loadSoaTable === "function";
    });

    // Test 20: Fonction escapeHtml existe
    addTest("Fonction escapeHtml existe", () => {
      return typeof escapeHtml === "function";
    });

    // Test 21: Fonction editSoaControl existe
    addTest("Fonction editSoaControl existe", () => {
      return typeof editSoaControl === "function";
    });

    // Test 22: Fonction renderSoaRiskBadges existe
    addTest("Fonction renderSoaRiskBadges existe", () => {
      return typeof renderSoaRiskBadges === "function";
    });

    // Test 23: Entrées SoA ont linkedRiskIds
    addTest("Entrées SoA supportent linkedRiskIds", () => {
      if (appData.soa.length === 0) return true;
      // Vérifier qu'au moins une entrée a linkedRiskIds défini
      return appData.soa.some(s => Array.isArray(s.linkedRiskIds));
    });

    // Test 24: renderSoaRiskBadges retourne HTML valide
    addTest("renderSoaRiskBadges retourne HTML pour risques vides", () => {
      const result = renderSoaRiskBadges([]);
      return result.includes("soa-risks__empty");
    });

    // Exécuter les tests
    setTimeout(() => {
      tests.forEach(test => {
        try {
          if (test.fn()) {
            passed++;
          } else {
            failed++;
            void 0;
          }
        } catch (e) {
          failed++;
          void 0;
        }
      });

      const total = tests.length;
      // FIX-I18N-AUTOSAVE (audit 2026-06-10, bundle.js:12866) : toasts traduits
      if (failed === 0) {
        showToast2(t('toasts.testsCompleted', 'Tests terminés : {passed}/{total} réussis')
          .replace('{passed}', passed).replace('{total}', total), "success");
      } else {
        showToast2(t('toasts.testsCompletedFailed', 'Tests terminés : {passed}/{total} réussis, {failed} échoué(s)')
          .replace('{passed}', passed).replace('{total}', total).replace('{failed}', failed), "warning");
      }
    }, 500);
  }
  function showToast2(message, type = "info", duration = 5000) {
    const container = document.getElementById("toastContainer");
    if (!container) return;
    // FIX-A: Deduplicate — skip if an identical toast is already visible
    var _existingMsgs = container.querySelectorAll('.toast .toast__message');
    for (var i = 0; i < _existingMsgs.length; i++) {
      if (_existingMsgs[i].textContent === message) return;
    }
    // O-004: Limit stacked toasts to 5
    var existingToasts = container.querySelectorAll('.toast');
    if (existingToasts.length >= 5) {
      var oldest = existingToasts[0];
      if (container.contains(oldest)) container.removeChild(oldest);
    }
    const toast = document.createElement("div");
    toast.className = `toast toast--${type}`;
    toast.setAttribute('role', (type === 'error' || type === 'warning') ? 'alert' : 'status');
    const icons = { success: "check-circle", error: "exclamation-triangle", warning: "exclamation-circle", info: "info-circle" };
    toast.innerHTML = `<div class="toast__icon"><i class="fas fa-${icons[type]}"></i></div><div class="toast__content"><div class="toast__message">${escapeHtml(message)}</div></div><button class="toast__close" aria-label="${t('common.close', 'Fermer')}"><i class="fas fa-times"></i></button>`;
    container.appendChild(toast);
    setTimeout(() => {
      toast.classList.add("toast--visible");
    }, 10);
    const closeBtn = toast.querySelector(".toast__close");
    closeBtn.addEventListener("click", () => {
      toast.classList.remove("toast--visible");
      setTimeout(() => {
        container.removeChild(toast);
      }, 300);
    });
    setTimeout(() => {
      if (container.contains(toast)) {
        toast.classList.remove("toast--visible");
        setTimeout(() => {
          if (container.contains(toast)) container.removeChild(toast);
        }, 300);
      }
    }, duration);
  }
  let missingSubcategoryWarned = false;
  function warnMissingSubcategories() {
    const missing = (appData.controls || []).filter(c =>
      !c.subcategory || (Array.isArray(c.subcategory) && c.subcategory.length === 0)
    );
    if (missing.length > 0 && !missingSubcategoryWarned) {
      missingSubcategoryWarned = true;
      // FIX-I18N-AUTOSAVE (audit 2026-06-10, bundle.js:12866) : la clé
      // warnings.missingSubcategory existait dans LES DEUX locales mais n'était pas branchée.
      showToast2(t('warnings.missingSubcategory', { count: missing.length }), "warning");
    }
  }
  function hideLoadingSpinner2() {
    const spinner = document.getElementById("loadingSpinner");
    if (spinner) spinner.classList.add("hidden");
  }

  function updateAppTitle() {
    const el = document.getElementById("appTitle");
    if (el) {
      // vMajor : chiffres uniquement (version.json contrôlé), injecté après escapeHtml du titre.
      const vRaw = (window.CSI && window.CSI.version) ? String(window.CSI.version).split('.')[0] : '3';
      const vMajor = /^\d+$/.test(vRaw) ? vRaw : '3';
      el.innerHTML = '<i class="fas fa-shield-alt"></i> ' + escapeHtml(appData.title) +
        ' <span class="header__version-badge" id="appVersionBadge" title="Version ' + vMajor + '" aria-label="Version ' + vMajor + '">V' + vMajor + '</span>';
    }
  }

  function openManual() {
    // Le manuel vit désormais sur le site (page dédiée avec recherche intégrée).
    // Nouvel onglet pour ne pas interrompre la session de travail.
    window.open("https://cyber-assistant.com/manuel", "_blank", "noopener");
  }

  // Store for settings section content generators
  let settingsSectionGenerators = null;
  let currentSettingsSection = "general";

  function renderSettingsPage() {
    const container = document.getElementById("settingsContent");
    if (!container) return;
    container.innerHTML = "";

    // Create main layout with sidebar
    const layout = document.createElement("div");
    layout.className = "settings-layout";

    // Create sidebar navigation
    const sidebar = document.createElement("nav");
    sidebar.className = "settings-sidebar";
    const nav = document.createElement("ul");
    nav.className = "settings-nav";

    // Define navigation items
    const navItems = [
      { id: "general", icon: "fa-sliders", label: t('settings.nav.general', 'Général') },
      { id: "security", icon: "fa-shield-halved", label: t('settings.nav.security', 'Sécurité') },
      { id: "data", icon: "fa-database", label: t('settings.nav.data', 'Données') },
      { id: "storage", icon: "fa-hard-drive", label: t('settings.nav.storage', 'Stockage') }
    ];
    // Add license if unlocked
    // FIX-LICENSE-NAV-CORPLOCAL (audit 2026-06-10) : isServerModeUnlocked() est toujours
    // false en file:// — afficher aussi la section Licence pour l'édition Corporate Local
    // (statut + renouvellement anticipé : renderLicenseSection s'appuie sur
    // CSI_TrialManager, qui délègue à CSI_LocalTrialManager pour cette édition).
    var _edLicNav = window._csiEdition;
    if (isServerModeUnlocked() || (_edLicNav && _edLicNav.id === 'corporate-local')) {
      navItems.push({ id: "license", icon: "fa-key", label: t('settings.nav.license', 'Licence'), badge: "PRO" });
    }
    // Add users management if admin + server mode
    var AM = window.CSI_AuthManager;
    if (isServerModeUnlocked() && AM && AM.isAdmin()) {
      navItems.push({ id: "users", icon: "fa-users", label: t('settings.users', 'Utilisateurs'), badge: "ADMIN" });
    }
    // === CSI-ME === carte Multi-entités (admin + mode serveur ; la carte gère
    // elle-même l'état licence/activation).
    if (isServerModeUnlocked() && AM && AM.isAdmin()) {
      navItems.push({ id: "entities", icon: "fa-sitemap", label: t('settings.entities', 'Multi-entités') });
    }
    navItems.push(
      { id: "frameworks", icon: "fa-arrows-left-right", label: t('mapping.settings.title', 'Référentiels de sécurité') },
      { id: "advanced", icon: "fa-flask-vial", label: t('settings.nav.advanced', 'Avancé') }
    );
    // Zone Danger (réinitialisation) : en mode serveur, réservée aux administrateurs.
    // En mode local/file:// il n'y a pas de comptes — elle reste accessible.
    if (!isServerModeUnlocked() || (AM && AM.isAdmin())) {
      navItems.push({ id: "reset", icon: "fa-triangle-exclamation", label: t('settings.nav.danger', 'Zone Danger'), danger: true });
    } else if (currentSettingsSection === 'reset') {
      currentSettingsSection = 'general';
    }

    navItems.forEach(item => {
      const li = document.createElement("li");
      li.className = "settings-nav__item";
      if (item.danger) li.classList.add("settings-nav__item--danger");
      if (item.id === currentSettingsSection) li.classList.add("settings-nav__item--active");
      li.dataset.section = item.id;

      const icon = document.createElement("span");
      icon.className = "settings-nav__icon";
      icon.innerHTML = `<i class="fas ${item.icon}"></i>`;

      const label = document.createElement("span");
      label.textContent = item.label;

      li.appendChild(icon);
      li.appendChild(label);

      if (item.badge) {
        const badge = document.createElement("span");
        badge.className = "settings-nav__badge";
        badge.textContent = item.badge;
        li.appendChild(badge);
      }

      li.addEventListener("click", () => {
        currentSettingsSection = item.id;
        document.querySelectorAll(".settings-nav__item").forEach(el => el.classList.remove("settings-nav__item--active"));
        li.classList.add("settings-nav__item--active");
        renderSettingsContent();
      });

      nav.appendChild(li);
    });

    sidebar.appendChild(nav);

    // Create content area
    const contentArea = document.createElement("div");
    contentArea.id = "settingsContentArea";
    contentArea.className = "settings-content-area";

    layout.appendChild(sidebar);
    layout.appendChild(contentArea);
    container.appendChild(layout);

    // Initialize section generators
    initSettingsSectionGenerators();

    // Render current section
    renderSettingsContent();
  }

  function renderSettingsContent() {
    const contentArea = document.getElementById("settingsContentArea");
    if (!contentArea || !settingsSectionGenerators) return;

    contentArea.classList.add("settings-content-area--transitioning");

    setTimeout(() => {
      contentArea.innerHTML = "";
      const generator = settingsSectionGenerators[currentSettingsSection];
      if (generator) {
        generator(contentArea);
      }
      contentArea.classList.remove("settings-content-area--transitioning");
      refreshReadOnlyLocks();
    }, 100);
  }

  function createSettingsCard({ title, description, icon, tone, status }) {
    const card = document.createElement("div");
    card.className = "settings-card";
    if (tone) card.classList.add(`settings-card--${tone}`);

    const header = document.createElement("div");
    header.className = "settings-card__header";

    const iconWrap = document.createElement("div");
    iconWrap.className = "settings-card__icon-wrap";
    iconWrap.innerHTML = `<i class="fas ${icon}"></i>`;

    const headerText = document.createElement("div");
    headerText.className = "settings-card__header-text";

    const titleEl = document.createElement("h3");
    titleEl.className = "settings-card__title";
    titleEl.textContent = title;

    const descEl = document.createElement("p");
    descEl.className = "settings-card__desc";
    descEl.textContent = description;

    headerText.appendChild(titleEl);
    headerText.appendChild(descEl);

    header.appendChild(iconWrap);
    header.appendChild(headerText);

    if (status !== undefined) {
      const statusEl = document.createElement("div");
      statusEl.className = `settings-card__status${status ? " settings-card__status--ok" : ""}`;
      header.appendChild(statusEl);
    }

    const body = document.createElement("div");
    body.className = "settings-card__body";

    card.appendChild(header);
    card.appendChild(body);

    return { card, body };
  }

  function initSettingsSectionGenerators() {
    settingsSectionGenerators = {
      general: renderGeneralSection,
      security: renderSecuritySection,
      data: renderDataSection,
      storage: renderStorageSection,
      license: renderLicenseSection,
      users: renderUsersSection,
      frameworks: renderFrameworksSection,
      advanced: renderAdvancedSection,
      reset: renderResetSection,
      // === CSI-ME === section Multi-entités (déléguée au module entity-ui.js).
      entities: function(container) { if (window.CSI_EntityUI) window.CSI_EntityUI.renderSettingsSection(container); }
    };
  }

  // Settings helper functions
  function createSettingsInputGroup({ id, label, value = "", type = "text", placeholder = "", help = "", readonly = true }) {
    const wrap = document.createElement("div");
    wrap.className = "settings-form-group";
    const lbl = document.createElement("label");
    lbl.className = "settings-form-label";
    lbl.setAttribute("for", id);
    lbl.textContent = label;
    const input = document.createElement("input");
    input.id = id;
    input.type = type;
    input.className = "form-control";
    input.value = value || "";
    if (placeholder) input.placeholder = placeholder;
    if (readonly) input.dataset.readonlyLock = "true";
    wrap.appendChild(lbl);
    wrap.appendChild(input);
    if (help) {
      const helpText = document.createElement("p");
      helpText.className = "settings-form-help";
      helpText.textContent = help;
      wrap.appendChild(helpText);
    }
    return { wrap, input };
  }

  function createSettingsPasswordGroup({ id, label, value = "", placeholder = "", help = "" }) {
    const wrap = document.createElement("div");
    wrap.className = "settings-form-group";
    const lbl = document.createElement("label");
    lbl.className = "settings-form-label";
    lbl.setAttribute("for", id);
    lbl.textContent = label;
    const row = document.createElement("div");
    row.className = "settings-input-row";
    const input = document.createElement("input");
    input.id = id;
    input.type = "password";
    input.className = "form-control";
    input.value = value || "";
    if (placeholder) input.placeholder = placeholder;
    input.dataset.readonlyLock = "true";
    const toggle = document.createElement("button");
    toggle.type = "button";
    toggle.className = "btn btn--outline btn--sm settings-toggle-btn";
    toggle.innerHTML = '<i class="fas fa-eye"></i>';
    toggle.setAttribute("aria-pressed", "false");
    toggle.addEventListener("click", () => {
      const isHidden = input.type === "password";
      input.type = isHidden ? "text" : "password";
      toggle.innerHTML = isHidden ? '<i class="fas fa-eye-slash"></i>' : '<i class="fas fa-eye"></i>';
      toggle.setAttribute("aria-pressed", String(isHidden));
    });
    row.append(input, toggle);
    wrap.appendChild(lbl);
    wrap.appendChild(row);
    if (help) {
      const helpText = document.createElement("p");
      helpText.className = "settings-form-help";
      helpText.textContent = help;
      wrap.appendChild(helpText);
    }
    return { wrap, input, toggle };
  }

  function createSettingsSelectGroup({ id, label, value = "", options = [], help = "" }) {
    const wrap = document.createElement("div");
    wrap.className = "settings-form-group";
    const lbl = document.createElement("label");
    lbl.className = "settings-form-label";
    lbl.setAttribute("for", id);
    lbl.textContent = label;
    const select = document.createElement("select");
    select.id = id;
    select.className = "form-control";
    select.dataset.readonlyLock = "true";
    options.forEach((opt) => {
      const option = document.createElement("option");
      option.value = opt.value;
      option.textContent = opt.label;
      select.appendChild(option);
    });
    select.value = value || "";
    wrap.appendChild(lbl);
    wrap.appendChild(select);
    if (help) {
      const helpText = document.createElement("p");
      helpText.className = "settings-form-help";
      helpText.textContent = help;
      wrap.appendChild(helpText);
    }
    return { wrap, select };
  }

  // === SECTION: GENERAL ===
  function renderGeneralSection(container) {
    const legacyTitle = "Système de Management de la Sécurité";
    const safeTitle = !appData.title || appData.title === legacyTitle ? "Cyber-Assistant" : appData.title;
    if (appData.title !== safeTitle) {
      appData.title = safeTitle;
      updateAppTitle();
    }

    const { card, body } = createSettingsCard({
      title: t('settings.generalTitle', 'Configuration générale'),
      description: t('settings.generalDesc', 'Personnalisez le titre, le thème et les cibles de maturité.'),
      icon: "fa-sliders"
    });

    const formGrid = document.createElement("div");
    formGrid.className = "settings-form-grid";

    const { wrap: titleWrap, input: titleInput } = createSettingsInputGroup({
      id: "settingsTitle",
      label: t('settings.appTitleLabel', "Titre de l'application"),
      value: safeTitle
    });
    titleWrap.classList.add("settings-form-group--full");

    const { wrap: themeWrap, select: themeSelect } = createSettingsSelectGroup({
      id: "settingsTheme",
      label: t('settings.themeLabel', 'Thème'),
      value: currentTheme,
      options: THEME_OPTIONS
    });

    const langOptions = window.CSI_i18n ? Object.entries(window.CSI_i18n.getAvailableLanguages()).map(([code, info]) => ({
      value: code,
      label: `${info.flag} ${info.name}`
    })) : [
      { value: "fr", label: "🇫🇷 Français" },
      { value: "en", label: "🇬🇧 English" }
    ];
    const currentLang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : "fr";
    const { wrap: langWrap, select: langSelect } = createSettingsSelectGroup({
      id: "settingsLanguage",
      label: "Langue / Language / Taal",
      value: currentLang,
      options: langOptions
    });

    const { wrap: isoWrap, input: isoInput } = createSettingsInputGroup({
      id: "settingsIsoTarget",
      label: t('settings.isoTarget', 'Cible ISO'),
      type: "number",
      value: appData.targetMaturity.iso27002,
      help: t('settings.isoTargetHelp', 'Niveau de maturité visé (1-5).')
    });
    isoInput.min = "1";
    isoInput.max = "5";

    const { wrap: nis2Wrap, input: nis2Input } = createSettingsInputGroup({
      id: "settingsNis2Target",
      label: t('settings.nis2Target', 'Cible NIS2'),
      type: "number",
      value: appData.targetMaturity.nis2Target,
      help: t('settings.nis2TargetHelp', 'Valeur cible (1-5) pour toutes les catégories.')
    });
    nis2Input.min = "1";
    nis2Input.max = "5";

    formGrid.append(titleWrap, themeWrap, langWrap);
    body.appendChild(formGrid);

    const targetGrid = document.createElement("div");
    targetGrid.className = "settings-form-grid settings-form-grid--targets";
    targetGrid.append(isoWrap, nis2Wrap);
    body.appendChild(targetGrid);

    const actions = document.createElement("div");
    actions.className = "settings-card__actions";
    const btnSave = document.createElement("button");
    btnSave.id = "settingsSave";
    btnSave.className = "btn btn--primary btn--sm";
    btnSave.textContent = t('settings.saveBtn', 'Enregistrer les paramètres');
    btnSave.dataset.readonlyLock = "true";
    actions.appendChild(btnSave);
    body.appendChild(actions);

    btnSave.addEventListener("click", () => {
      if (!ensureEditMode(t('settings.saveBtn', 'Enregistrer les paramètres'))) {
        refreshReadOnlyLocks();
        return;
      }
      const newTitle = titleInput.value.trim();
      if (newTitle) {
        appData.title = newTitle;
        updateAppTitle();
      }
      if (themeSelect.value) setTheme(themeSelect.value);
      const isoTarget = parseInt(isoInput.value, 10);
      if (!isNaN(isoTarget)) appData.targetMaturity.iso27002 = isoTarget;
      const nis2Target = parseInt(nis2Input.value, 10);
      if (!isNaN(nis2Target)) appData.targetMaturity.nis2Target = nis2Target;
      if (!appData.targetMaturity.nis2) {
        appData.targetMaturity.nis2 = Object.assign({}, DEFAULT_NIS2_MATURITY);
      }
      const newLang = langSelect.value;
      if (newLang && window.CSI_i18n && newLang !== window.CSI_i18n.getLanguage()) {
        window.CSI_i18n.setLanguage(newLang);
        window.CSI_i18n.translatePage();
        // FEAT-USER-LANGUAGE (2026-07-10) : en mode serveur, la langue suit le compte.
        // Push best-effort — en cas d'échec, localStorage reste la référence locale.
        if (window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn && window.CSI_AuthManager.isLoggedIn()) {
          window.CSI_AuthManager.setUserLanguage(newLang).catch(function(e) {
            void 0;
          });
        }
      }
            addAuditEntry('config', 'settings', appData.title || 'Settings');
      updateDashboard();
      showToast2(t('toasts.settingsSaved'), "success");
      btnSave.classList.add("btn--success-flash");
      setTimeout(() => btnSave.classList.remove("btn--success-flash"), 600);
      // FIX-GLOBAL-SYNC: Settings contain global properties (title, targets, theme)
      // that are not tab-specific. Push them to server so other users receive them.
      // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
      if (window.CSI_SyncManager && window.CSI_SyncManager.forceServerSync) {
        window.CSI_SyncManager.forceServerSync(['title', 'targetMaturity']).catch(function(e) {
          void 0;
        });
      }
    });

    container.appendChild(card);

    // === Logo card ===
    const { card: logoCard, body: logoBody } = createSettingsCard({
      title: t('settings.logoTitle'),
      description: t('settings.logoDescription'),
      icon: "fa-image"
    });

    const logoGrid = document.createElement("div");
    logoGrid.className = "settings-form-grid";

    const previewWrap = document.createElement("div");
    previewWrap.className = "settings-form-group settings-form-group--full settings-logo-preview-wrap";

    function refreshLogoPreview() {
      previewWrap.innerHTML = "";
      const currentLogo = (appData.settings && appData.settings.logo) || null;
      if (currentLogo) {
        const img = document.createElement("img");
        img.src = currentLogo;
        img.className = "settings-logo-preview";
        img.alt = "Logo";
        previewWrap.appendChild(img);
        btnRemoveLogo.style.display = "";
      } else {
        const placeholder = document.createElement("div");
        placeholder.className = "settings-logo-placeholder";
        placeholder.innerHTML = '<i class="fas fa-image"></i><span>' + t('settings.logoNone') + '</span>';
        previewWrap.appendChild(placeholder);
        btnRemoveLogo.style.display = "none";
      }
    }

    const fileWrap = document.createElement("div");
    fileWrap.className = "settings-form-group settings-form-group--full";
    const fileLabel = document.createElement("label");
    fileLabel.textContent = t('settings.logoUpload');
    fileLabel.className = "settings-label";
    const logoFileInput = document.createElement("input");
    logoFileInput.type = "file";
    logoFileInput.accept = "image/png,image/jpeg,image/svg+xml";
    logoFileInput.className = "form-control";
    logoFileInput.dataset.readonlyLock = "true";
    fileWrap.appendChild(fileLabel);
    fileWrap.appendChild(logoFileInput);

    const logoActions = document.createElement("div");
    logoActions.className = "settings-card__actions";
    const btnSaveLogo = document.createElement("button");
    btnSaveLogo.className = "btn btn--primary btn--sm";
    btnSaveLogo.textContent = t('settings.logoSave');
    btnSaveLogo.dataset.readonlyLock = "true";
    const btnRemoveLogo = document.createElement("button");
    btnRemoveLogo.className = "btn btn--outline btn--sm";
    btnRemoveLogo.textContent = t('settings.logoRemove');
    btnRemoveLogo.dataset.readonlyLock = "true";
    btnRemoveLogo.style.display = (appData.settings && appData.settings.logo) ? "" : "none";
    logoActions.append(btnSaveLogo, btnRemoveLogo);

    btnSaveLogo.addEventListener("click", () => {
      if (!ensureEditMode(t('settings.logoSave', 'Enregistrer le logo'))) { refreshReadOnlyLocks(); return; }
      const file = logoFileInput.files[0];
      if (!file) { showToast2(t('settings.logoNone'), "warning"); return; }
      if (file.size > 512000) { showToast2(t('settings.logoTooLarge'), "warning"); return; }
      const reader = new FileReader();
      reader.onload = function(ev) {
        if (!appData.settings) appData.settings = {};
        appData.settings.logo = ev.target.result;
                addAuditEntry('config', 'settings', t('audit.logoUploaded', 'Logo uploadé'));
        refreshLogoPreview();
        logoFileInput.value = "";
        showToast2(t('settings.logoSaved'), "success");
      };
      reader.readAsDataURL(file);
    });

    btnRemoveLogo.addEventListener("click", () => {
      if (!ensureEditMode(t('settings.logoRemove', 'Supprimer le logo'))) { refreshReadOnlyLocks(); return; }
      if (appData.settings) delete appData.settings.logo;
            addAuditEntry('config', 'settings', t('audit.logoRemoved', 'Logo supprimé'));
      refreshLogoPreview();
      logoFileInput.value = "";
      showToast2(t('settings.logoRemoved'), "info");
    });

    logoGrid.append(previewWrap, fileWrap);
    logoBody.append(logoGrid, logoActions);
    container.appendChild(logoCard);
    refreshLogoPreview();

    // === Second card: Display options ===
    const { card: displayCard, body: displayBody } = createSettingsCard({
      title: t('settings.displayTitle', "Options d'affichage"),
      description: t('settings.displayDesc', "Personnalisez l'apparence et la densité de l'interface."),
      icon: "fa-desktop"
    });

    const displayGrid = document.createElement("div");
    displayGrid.className = "settings-form-grid";

    // Compact mode toggle
    const compactWrap = document.createElement("div");
    compactWrap.className = "settings-form-group settings-form-group--full";
    const compactLabel = document.createElement("label");
    compactLabel.className = "settings-checkbox";
    const compactInput = document.createElement("input");
    compactInput.type = "checkbox";
    compactInput.id = "settingsCompactMode";
    compactInput.checked = document.body.classList.contains("compact-mode");
    compactLabel.appendChild(compactInput);
    compactLabel.appendChild(document.createTextNode(' ' + t('settings.compactMode', 'Mode compact')));
    const compactHelp = document.createElement("p");
    compactHelp.className = "settings-field-help";
    compactHelp.textContent = t('settings.compactModeHelp', "Réduit l'espacement et la taille des éléments pour afficher plus d'informations.");
    compactWrap.appendChild(compactLabel);
    compactWrap.appendChild(compactHelp);

    compactInput.addEventListener("change", () => {
      document.body.classList.toggle("compact-mode", compactInput.checked);
      try {
        localStorage.setItem("smsi_compact_mode", compactInput.checked ? "1" : "0");
      } catch (e) {
        void 0;
      }
      showToast2(compactInput.checked ? t('settings.compactEnabled', 'Mode compact activé') : t('settings.compactDisabled', 'Mode compact désactivé'), "info");
    });

    displayGrid.appendChild(compactWrap);
    displayBody.appendChild(displayGrid);
    container.appendChild(displayCard);
  }

  // === SECTION: SECURITY ===
  function renderSecuritySection(container) {
   // En mode serveur, le mot de passe d'édition locale (Phase 1) est inutile — l'auth JWT suffit
   if (!isServerModeUnlocked()) {
    const { card, body } = createSettingsCard({
      title: t('settings.editPasswordTitle', 'Mot de passe du mode édition'),
      description: t('settings.editPasswordDesc', `Minimum ${MIN_EDIT_PASSWORD_LENGTH} caractères, 1 majuscule, 1 minuscule, 1 caractère spécial.`).replace('{{min}}', MIN_EDIT_PASSWORD_LENGTH),
      icon: "fa-shield-halved",
      status: !!getStoredEditPasswordHash()
    });

    const formGrid = document.createElement("div");
    formGrid.className = "settings-form-grid";

    const { wrap: editPwdWrap, input: editPwdInput } = createSettingsPasswordGroup({
      id: "settingsEditPassword",
      label: t('settings.newPassword', 'Nouveau mot de passe'),
      placeholder: t('settings.minCharsPlaceholder', `Minimum ${MIN_EDIT_PASSWORD_LENGTH} caractères`).replace('{{min}}', MIN_EDIT_PASSWORD_LENGTH)
    });

    const { wrap: editPwdConfirmWrap, input: editPwdConfirmInput } = createSettingsPasswordGroup({
      id: "settingsEditPasswordConfirm",
      label: t('settings.confirmation', 'Confirmation'),
      placeholder: t('settings.retypePassword', 'Ressaisir le mot de passe')
    });

    formGrid.append(editPwdWrap, editPwdConfirmWrap);
    body.appendChild(formGrid);

    const { wrap: policyWrap, list: policyList } = createPasswordPolicyList();
    body.appendChild(policyWrap);

    const passwordStatus = document.createElement("p");
    passwordStatus.className = "settings-form-help";
    passwordStatus.textContent = getStoredEditPasswordHash()
      ? t('settings.customPasswordActive', 'Mot de passe personnalisé actif.')
      : t('settings.defaultPasswordActive', 'Mot de passe par défaut actif.');
    body.appendChild(passwordStatus);

    const updatePolicy = () => updatePasswordPolicyList(policyList, editPwdInput.value);
    editPwdInput.addEventListener("input", updatePolicy);
    updatePolicy();

    const actions = document.createElement("div");
    actions.className = "settings-card__actions";
    const btnUpdatePassword = document.createElement("button");
    btnUpdatePassword.className = "btn btn--primary btn--sm";
    btnUpdatePassword.textContent = t('settings.updatePassword', 'Mettre à jour le mot de passe');
    btnUpdatePassword.dataset.readonlyLock = "true";
    actions.appendChild(btnUpdatePassword);
    body.appendChild(actions);

    btnUpdatePassword.addEventListener("click", () => {
      if (!ensureEditMode(t('settings.updatePassword', 'Mettre à jour le mot de passe'))) {
        refreshReadOnlyLocks();
        return;
      }
      const password = editPwdInput.value || "";
      const confirm = editPwdConfirmInput.value || "";
      if (!password) {
        showToast2(t('toasts.enterPassword'), "warning");
        return;
      }
      const policy = getEditPasswordPolicyStatus(password);
      if (!policy.isValid) {
        const detail = policy.missing.length ? t('passwordPolicy.missingRequirements', 'Exigences manquantes') + ': ' + policy.missing.join(", ") + '.' : "";
        showToast2((t('settings.passwordNonCompliant', 'Mot de passe non conforme.') + ' ' + detail).trim(), "warning");
        return;
      }
      if (password !== confirm) {
        showToast2(t('toasts.passwordMismatch'), "error");
        return;
      }
      btnUpdatePassword.disabled = true;
      const initialLabel = btnUpdatePassword.textContent;
      btnUpdatePassword.textContent = t('common.saving', 'Enregistrement...');
      hashEditPassword(password)
        .then((hash) => {
          setStoredEditPasswordHash(hash);
          addAuditEntry('config', 'settings', t('audit.passwordChanged', 'Mot de passe modifié'));
          editPasswordUnlocked = true;
          editPwdInput.value = "";
          editPwdConfirmInput.value = "";
          passwordStatus.textContent = t('settings.customPasswordActive', 'Mot de passe personnalisé actif.');
          showToast2(t('toasts.passwordUpdated'), "success");
        })
        .catch((err) => {
          void 0;
          showToast2(t('toasts.passwordSaveError', "Impossible d'enregistrer le mot de passe"), "error");
        })
        .finally(() => {
          btnUpdatePassword.disabled = false;
          btnUpdatePassword.textContent = initialLabel;
        });
    });

    container.appendChild(card);
   } // fin if (!isServerModeUnlocked())

    // Server mode: "Change my password" card
    if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn()) {
      var { card: srvPwdCard, body: srvPwdBody } = createSettingsCard({
        title: t('settings.changeServerPassword', 'Changer mon mot de passe'),
        description: t('settings.changeServerPasswordDesc', 'Modifiez votre mot de passe de connexion.'),
        icon: 'fa-key'
      });
      var btnChangeSrvPwd = document.createElement('button');
      btnChangeSrvPwd.className = 'btn btn--primary btn--sm';
      btnChangeSrvPwd.textContent = t('settings.changePasswordBtn', 'Changer le mot de passe');
      btnChangeSrvPwd.addEventListener('click', function() {
        window.CSI_AuthManager.showChangePasswordModal();
      });
      srvPwdBody.appendChild(btnChangeSrvPwd);
      container.appendChild(srvPwdCard);
    }

    // ── Card A: Tableau de bord sécurité (both modes) ──
    (function renderSecurityDashboardCard() {
      var isServer = isServerModeUnlocked();
      var checks = [];
      var totalOk = 0;

      var hasCrypto = !!(window.crypto && window.crypto.subtle);
      checks.push({ label: t('adminUi.checkWebCrypto', 'Web Crypto API disponible'), ok: hasCrypto });
      if (hasCrypto) totalOk++;

      var hasEditPwd = !!getStoredEditPasswordHash();
      checks.push({ label: t('adminUi.checkEditPassword', "Mot de passe d'édition défini"), ok: hasEditPwd });
      if (hasEditPwd) totalOk++;

      checks.push({ label: t('adminUi.checkHashAlgo', 'Algorithme de hachage (PBKDF2 600k)'), ok: true });
      totalOk++;

      var hasRandomUUID = !!(window.crypto && typeof window.crypto.randomUUID === 'function');
      checks.push({ label: t('adminUi.checkRandomUuid', 'crypto.randomUUID() supporté'), ok: hasRandomUUID });
      if (hasRandomUUID) totalOk++;

      var proto = window.location.protocol;
      var secureProto = (proto === 'file:' || proto === 'https:');
      checks.push({ label: t('adminUi.checkSecureProtocol', 'Protocole sécurisé (file:// ou https://)'), ok: secureProto });
      if (secureProto) totalOk++;

      var totalChecks = checks.length;

      var _buildDashboardCard = function(serverChecks, serverStats) {
        var allChecks = checks.slice();
        if (serverChecks && typeof serverChecks === 'object') {
          var keys = Object.keys(serverChecks);
          for (var si = 0; si < keys.length; si++) {
            var sk = keys[si];
            var sOk = !!serverChecks[sk];
            allChecks.push({ label: sk.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); }), ok: sOk });
            totalChecks++;
            if (sOk) totalOk++;
          }
        }

        var scoreText = totalOk + '/' + totalChecks + ' ' + t('adminUi.checksOkSuffix', 'contrôles OK');
        var allOk = (totalOk === totalChecks);

        var dashRes = createSettingsCard({
          title: t('settings.securityDashboard', 'Tableau de bord sécurité'),
          description: scoreText,
          icon: 'fa-shield-halved',
          status: allOk
        });
        var dashCard = dashRes.card;
        var dashBody = dashRes.body;

        var dashGrid = document.createElement('div');
        dashGrid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:8px;';

        for (var ci = 0; ci < allChecks.length; ci++) {
          var chk = allChecks[ci];
          var item = document.createElement('div');
          item.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;background:var(--bg-secondary,#f5f5f5);';

          var ico = document.createElement('i');
          if (chk.ok) {
            ico.className = 'fas fa-check-circle';
            ico.style.color = '#16a34a';
          } else {
            ico.className = 'fas fa-exclamation-triangle';
            ico.style.color = '#d97706';
          }
          var lbl = document.createElement('span');
          lbl.style.cssText = 'font-size:0.85rem;';
          lbl.textContent = chk.label;

          item.appendChild(ico);
          item.appendChild(lbl);
          dashGrid.appendChild(item);
        }

        dashBody.appendChild(dashGrid);

        if (serverStats && typeof serverStats === 'object') {
          var statsWrap = document.createElement('div');
          statsWrap.style.cssText = 'margin-top:12px;padding:10px;border-radius:6px;background:var(--bg-secondary,#f5f5f5);';
          var statsTitle = document.createElement('strong');
          statsTitle.textContent = t('adminUi.serverStats', 'Statistiques serveur');
          statsTitle.style.cssText = 'display:block;margin-bottom:6px;font-size:0.85rem;';
          statsWrap.appendChild(statsTitle);

          var statKeys = Object.keys(serverStats);
          for (var sti = 0; sti < statKeys.length; sti++) {
            var statLine = document.createElement('div');
            statLine.style.cssText = 'display:flex;justify-content:space-between;font-size:0.8rem;padding:2px 0;';
            var statLabel = document.createElement('span');
            statLabel.textContent = statKeys[sti].replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
            var statVal = document.createElement('span');
            statVal.style.fontWeight = '600';
            statVal.textContent = String(serverStats[statKeys[sti]]);
            statLine.appendChild(statLabel);
            statLine.appendChild(statVal);
            statsWrap.appendChild(statLine);
          }
          dashBody.appendChild(statsWrap);
        }

        container.appendChild(dashCard);
      };

      if (isServer && window.CSI_AuthManager && typeof window.CSI_AuthManager.getSecurityStatus === 'function') {
        window.CSI_AuthManager.getSecurityStatus()
          .then(function(result) {
            _buildDashboardCard(result && result.checks, result && result.stats);
          })
          .catch(function() {
            _buildDashboardCard(null, null);
          });
      } else {
        _buildDashboardCard(null, null);
      }
    })();

    // ── Card B: Recommandations de sécurité (both modes) ──
    (function renderSecurityRecommendationsCard() {
      var isServer = isServerModeUnlocked();
      var recoRes = createSettingsCard({
        title: t('settings.securityRecommendations', 'Recommandations de sécurité'),
        description: t('settings.securityRecommendationsDesc', 'Conseils contextuels basés sur votre posture de sécurité.'),
        icon: 'fa-lightbulb'
      });
      var recoCard = recoRes.card;
      var recoBody = recoRes.body;

      var recoList = document.createElement('ul');
      recoList.style.cssText = 'list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px;';

      var _addReco = function(text, level) {
        var li = document.createElement('li');
        li.style.cssText = 'display:flex;align-items:flex-start;gap:8px;padding:8px 10px;border-radius:6px;background:var(--bg-secondary,#f5f5f5);font-size:0.85rem;';
        var rIco = document.createElement('i');
        if (level === 'warning') {
          rIco.className = 'fas fa-exclamation-triangle';
          rIco.style.color = '#d97706';
        } else {
          rIco.className = 'fas fa-info-circle';
          rIco.style.color = '#2563eb';
        }
        var rTxt = document.createElement('span');
        rTxt.textContent = text;
        li.appendChild(rIco);
        li.appendChild(rTxt);
        recoList.appendChild(li);
      };

      if (!isServer) {
        if (!getStoredEditPasswordHash()) {
          _addReco(t('adminUi.recoEditPassword', "Définissez un mot de passe d'édition robuste pour protéger vos données."), 'warning');
        }
        _addReco(t('adminUi.recoExportBackup', 'Exportez régulièrement vos données en tant que sauvegarde.'), 'info');
        _addReco(t('adminUi.recoModernBrowser', 'Utilisez un navigateur moderne avec support de Web Crypto API.'), 'info');
      } else {
        if (window.location.protocol === 'http:' && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
          _addReco(t('adminUi.recoHttps', 'Activez HTTPS pour des communications chiffrées.'), 'warning');
        }
        _addReco(t('adminUi.recoCors', 'Configurez une origine CORS spécifique plutôt que * (wildcard).'), 'warning');
        _addReco(t('adminUi.recoUpdates', 'Mettez à jour régulièrement le serveur et ses dépendances.'), 'info');
        _addReco(t('adminUi.recoBackups', 'Activez les sauvegardes automatiques sur le serveur.'), 'info');
        _addReco(t('adminUi.recoAuditLogs', "Vérifiez les journaux d'audit régulièrement."), 'info');
      }

      recoBody.appendChild(recoList);
      container.appendChild(recoCard);
    })();

    // ── Card C: Journal d'audit (server, admin only) ──
    if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isAdmin()) {
      (function renderAuditLogCard() {
        var eventColors = {
          login_success: '#16a34a',
          login_failed: '#dc2626',
          logout: '#6b7280',
          user_created: '#2563eb',
          user_deleted: '#dc2626',
          user_suspended: '#d97706',
          user_unsuspended: '#16a34a',
          role_changed: '#7c3aed',
          password_changed: '#2563eb',
          password_reset: '#d97706',
          data_import: '#0891b2',
          data_push: '#0891b2',
          lock_acquired: '#6b7280',
          license_activated: '#16a34a',
          setup_completed: '#16a34a',
          rate_limit_hit: '#dc2626',
          account_locked: '#dc2626',
          brute_force_detected: '#dc2626'
        };

        var auditRes = createSettingsCard({
          title: t('settings.auditLog', "Journal d'audit"),
          description: t('settings.auditLogDesc', 'Historique des événements de sécurité du serveur.'),
          icon: 'fa-clipboard-list'
        });
        var auditCard = auditRes.card;
        var auditBody = auditRes.body;

        var filterRow = document.createElement('div');
        filterRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;';

        var filterLabel = document.createElement('label');
        filterLabel.textContent = t('adminUi.filterByType', 'Filtrer par type :');
        filterLabel.style.cssText = 'font-size:0.85rem;font-weight:500;';

        var filterSelect = document.createElement('select');
        filterSelect.style.cssText = 'padding:4px 8px;border:1px solid var(--border-color,#ccc);border-radius:4px;font-size:0.8rem;';
        var optAll = document.createElement('option');
        optAll.value = '';
        optAll.textContent = t('adminUi.allEvents', 'Tous les événements');
        filterSelect.appendChild(optAll);

        var eventTypes = Object.keys(eventColors);
        for (var ei = 0; ei < eventTypes.length; ei++) {
          var opt = document.createElement('option');
          opt.value = eventTypes[ei];
          opt.textContent = eventTypes[ei].replace(/_/g, ' ');
          filterSelect.appendChild(opt);
        }

        filterRow.appendChild(filterLabel);
        filterRow.appendChild(filterSelect);
        auditBody.appendChild(filterRow);

        var tableWrap = document.createElement('div');
        tableWrap.style.cssText = 'max-height:350px;overflow-y:auto;border:1px solid var(--border-color,#ddd);border-radius:6px;';

        var table = document.createElement('table');
        table.style.cssText = 'width:100%;border-collapse:collapse;font-size:0.8rem;';
        var thead = document.createElement('thead');
        var headRow = document.createElement('tr');
        headRow.style.cssText = 'background:var(--bg-secondary,#f5f5f5);position:sticky;top:0;';
        var headCols = [t('adminUi.colDate', 'Date'), t('adminUi.colEvent', 'Événement'), t('adminUi.colUser', 'Utilisateur'), t('adminUi.colIp', 'IP'), t('adminUi.colDetails', 'Détails')];
        for (var hi = 0; hi < headCols.length; hi++) {
          var th = document.createElement('th');
          th.textContent = headCols[hi];
          th.style.cssText = 'padding:8px 10px;text-align:left;font-weight:600;border-bottom:2px solid var(--border-color,#ddd);white-space:nowrap;';
          headRow.appendChild(th);
        }
        thead.appendChild(headRow);
        table.appendChild(thead);

        var tbody = document.createElement('tbody');
        table.appendChild(tbody);
        tableWrap.appendChild(table);
        auditBody.appendChild(tableWrap);

        var currentOffset = 0;
        var currentFilter = '';

        var _renderRows = function(entries) {
          for (var ri = 0; ri < entries.length; ri++) {
            var entry = entries[ri];
            var tr = document.createElement('tr');
            tr.style.cssText = 'border-bottom:1px solid var(--border-color,#eee);';

            var tdDate = document.createElement('td');
            tdDate.style.cssText = 'padding:6px 10px;white-space:nowrap;';
            var dateStr = entry.timestamp || entry.date || '';
            if (dateStr) {
              try { dateStr = new Date(dateStr).toLocaleString('fr-FR'); } catch(e) {}
            }
            tdDate.textContent = dateStr;
            tr.appendChild(tdDate);

            var tdEvent = document.createElement('td');
            tdEvent.style.cssText = 'padding:6px 10px;';
            var badge = document.createElement('span');
            var evtType = entry.eventType || entry.event || entry.type || '';
            badge.textContent = evtType.replace(/_/g, ' ');
            var badgeColor = eventColors[evtType] || '#6b7280';
            badge.style.cssText = 'display:inline-block;padding:2px 8px;border-radius:10px;font-size:0.75rem;font-weight:500;color:#fff;background:' + badgeColor + ';';
            tdEvent.appendChild(badge);
            tr.appendChild(tdEvent);

            var tdUser = document.createElement('td');
            tdUser.style.cssText = 'padding:6px 10px;';
            tdUser.textContent = entry.userName || entry.username || entry.user || '-';
            tr.appendChild(tdUser);

            var tdIP = document.createElement('td');
            tdIP.style.cssText = 'padding:6px 10px;font-family:monospace;font-size:0.75rem;';
            tdIP.textContent = entry.ip || '-';
            tr.appendChild(tdIP);

            var tdDetails = document.createElement('td');
            tdDetails.style.cssText = 'padding:6px 10px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
            tdDetails.textContent = entry.details || entry.message || '-';
            tdDetails.title = entry.details || entry.message || '';
            tr.appendChild(tdDetails);

            tbody.appendChild(tr);
          }
        };

        var _loadEntries = function(offset, eventType) {
          if (typeof window.CSI_AuthManager.getAuditLog !== 'function') return;
          window.CSI_AuthManager.getAuditLog(50, offset, eventType || undefined)
            .then(function(result) {
              var entries = Array.isArray(result) ? result : (result && result.entries ? result.entries : []);
              _renderRows(entries);
              currentOffset = offset + entries.length;
              if (entries.length < 50) {
                btnLoadMore.style.display = 'none';
              } else {
                btnLoadMore.style.display = '';
              }
            })
            .catch(function(err) {
              void 0;
              var trErr = document.createElement('tr');
              var tdErr = document.createElement('td');
              tdErr.setAttribute('colspan', '5');
              tdErr.style.cssText = 'padding:12px;text-align:center;color:#dc2626;';
              tdErr.textContent = t('adminUi.auditLoadError', 'Erreur lors du chargement du journal.');
              trErr.appendChild(tdErr);
              tbody.appendChild(trErr);
            });
        };

        var loadMoreWrap = document.createElement('div');
        loadMoreWrap.style.cssText = 'margin-top:8px;text-align:center;';
        var btnLoadMore = document.createElement('button');
        btnLoadMore.className = 'btn btn--secondary btn--sm';
        btnLoadMore.textContent = t('adminUi.loadMore', 'Charger plus');
        btnLoadMore.addEventListener('click', function() {
          _loadEntries(currentOffset, currentFilter);
        });
        loadMoreWrap.appendChild(btnLoadMore);
        auditBody.appendChild(loadMoreWrap);

        filterSelect.addEventListener('change', function() {
          currentFilter = filterSelect.value;
          currentOffset = 0;
          while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
          _loadEntries(0, currentFilter);
        });

        _loadEntries(0, '');

        container.appendChild(auditCard);
      })();
    }

    // ── Card D: Filtrage IP (server, admin only) ──
    if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isAdmin()) {
      (function renderIPFilteringCard() {
        var ipRes = createSettingsCard({
          title: t('settings.ipFiltering', 'Filtrage IP'),
          description: t('settings.ipFilteringDesc', "Restreindre l'accès par adresse IP (supporte la notation CIDR)."),
          icon: 'fa-network-wired'
        });
        var ipCard = ipRes.card;
        var ipBody = ipRes.body;

        var modeGroup = document.createElement('div');
        modeGroup.style.cssText = 'display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap;';

        // FIX-SECCONFIG-CONTRACT (audit 2026-06-10) : valeurs alignées sur le serveur
        // (users.php n'accepte que disabled / allowlist-only / denylist-only).
        var modes = [
          { value: 'disabled', label: t('adminUi.ipModeDisabled', 'Désactivé') },
          { value: 'allowlist-only', label: t('adminUi.ipModeAllowlist', 'Liste blanche (Allowlist)') },
          { value: 'denylist-only', label: t('adminUi.ipModeDenylist', 'Liste noire (Denylist)') }
        ];

        var radioName = 'csi_ip_filter_mode_' + Date.now();

        for (var mi = 0; mi < modes.length; mi++) {
          var modeWrap = document.createElement('label');
          modeWrap.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:0.85rem;cursor:pointer;';

          var radio = document.createElement('input');
          radio.type = 'radio';
          radio.name = radioName;
          radio.value = modes[mi].value;
          if (mi === 0) radio.checked = true;

          var modeLbl = document.createElement('span');
          modeLbl.textContent = modes[mi].label;

          modeWrap.appendChild(radio);
          modeWrap.appendChild(modeLbl);
          modeGroup.appendChild(modeWrap);
        }

        ipBody.appendChild(modeGroup);

        var ipLabel = document.createElement('label');
        ipLabel.textContent = t('adminUi.ipAddressesLabel', 'Adresses IP (une par ligne, notation CIDR supportée) :');
        ipLabel.style.cssText = 'display:block;font-size:0.85rem;font-weight:500;margin-bottom:4px;';
        ipBody.appendChild(ipLabel);

        var ipTextarea = document.createElement('textarea');
        ipTextarea.style.cssText = 'width:100%;min-height:100px;padding:8px;border:1px solid var(--border-color,#ccc);border-radius:6px;font-family:monospace;font-size:0.8rem;resize:vertical;box-sizing:border-box;';
        ipTextarea.placeholder = '192.168.1.0/24\n10.0.0.1\n2001:db8::/32';
        ipBody.appendChild(ipTextarea);

        var ipActions = document.createElement('div');
        ipActions.className = 'settings-card__actions';
        ipActions.style.marginTop = '10px';

        var btnSaveIP = document.createElement('button');
        btnSaveIP.className = 'btn btn--primary btn--sm';
        btnSaveIP.textContent = t('adminUi.save', 'Enregistrer');
        btnSaveIP.addEventListener('click', function() {
          var selectedRadio = modeGroup.querySelector('input[type="radio"]:checked');
          var mode = selectedRadio ? selectedRadio.value : 'disabled';
          var ips = ipTextarea.value.split('\n').map(function(line) { return line.trim(); }).filter(function(line) { return line.length > 0; });

          btnSaveIP.disabled = true;
          btnSaveIP.textContent = t('adminUi.saving', 'Enregistrement...');

          // FIX-SECCONFIG-CONTRACT (audit 2026-06-10) : l'ancien payload
          // {ipFiltering:{mode,addresses}} n'existait pas côté serveur (users.php attend
          // ipFilterMode/ipAllowlist/ipDenylist à plat) → PHP retombait sur les défauts
          // « désactivé » en répondant ok:true : le filtrage n'était JAMAIS appliqué.
          var payload = { ipFilterMode: mode };
          if (mode === 'allowlist-only') payload.ipAllowlist = ips;
          if (mode === 'denylist-only') payload.ipDenylist = ips;
          window.CSI_AuthManager.updateSecurityConfig(payload)
          .then(function(res) {
            if (!res || res.ok === false) {
              var srvMsg = (res && res.data && res.data.error) ? res.data.error : t('adminUi.serverRefusedConfig', "Le serveur a refusé la configuration.");
              showToast2(t('adminUi.ipFilterErrorPrefix', 'Erreur filtrage IP : ') + srvMsg, 'error', 8000);
              return;
            }
            showToast2(t('adminUi.ipFilterSaved', 'Configuration de filtrage IP enregistrée.'), 'success');
          })
          .catch(function(err) {
            void 0;
            showToast2(t('adminUi.ipFilterSaveError', "Erreur lors de l'enregistrement du filtrage IP."), 'error');
          })
          .finally(function() {
            btnSaveIP.disabled = false;
            btnSaveIP.textContent = t('adminUi.save', 'Enregistrer');
          });
        });

        ipActions.appendChild(btnSaveIP);
        ipBody.appendChild(ipActions);

        // FIX-SECCONFIG-CONTRACT : précharger la configuration existante (la carte
        // s'affichait toujours sur « Désactivé » avec un textarea vide, et une simple
        // re-sauvegarde écrasait la configuration réelle).
        if (window.CSI_AuthManager.getSecurityStatus) {
          window.CSI_AuthManager.getSecurityStatus().then(function(st) {
            var checks = st && st.checks;
            if (!checks) return;
            var currentMode = checks.ipFilterMode || 'disabled';
            var radios = modeGroup.querySelectorAll('input[type="radio"]');
            for (var ri = 0; ri < radios.length; ri++) {
              radios[ri].checked = (radios[ri].value === currentMode);
            }
            var list = currentMode === 'denylist-only' ? checks.ipDenylist : checks.ipAllowlist;
            if (Array.isArray(list) && list.length) ipTextarea.value = list.join('\n');
          }).catch(function() {});
        }

        container.appendChild(ipCard);
      })();
    }

    // ── Card E: Politique de mots de passe (server, admin only) ──
    if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isAdmin()) {
      (function renderPasswordPolicyCard() {
        var polRes = createSettingsCard({
          title: t('settings.passwordPolicy', 'Politique de mots de passe'),
          description: t('settings.passwordPolicyDesc', 'Configurez les règles de sécurité des mots de passe.'),
          icon: 'fa-lock'
        });
        var polCard = polRes.card;
        var polBody = polRes.body;

        var policyReminder = document.createElement('div');
        policyReminder.style.cssText = 'padding:10px;border-radius:6px;background:var(--bg-secondary,#f5f5f5);margin-bottom:12px;font-size:0.85rem;';
        var policyReminderTitle = document.createElement('strong');
        policyReminderTitle.textContent = t('adminUi.currentPolicy', 'Politique actuelle :');
        policyReminderTitle.style.display = 'block';
        var policyReminderText = document.createElement('span');
        policyReminderText.textContent = t('adminUi.policyRules', '18 caractères minimum, 1 majuscule, 1 minuscule, 1 caractère spécial obligatoire.');
        policyReminder.appendChild(policyReminderTitle);
        policyReminder.appendChild(policyReminderText);
        polBody.appendChild(policyReminder);

        var expRow = document.createElement('div');
        expRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap;';

        var expLabel = document.createElement('label');
        expLabel.textContent = t('adminUi.passwordExpiration', 'Expiration des mots de passe :');
        expLabel.style.cssText = 'font-size:0.85rem;font-weight:500;';

        var expSelect = document.createElement('select');
        expSelect.style.cssText = 'padding:4px 8px;border:1px solid var(--border-color,#ccc);border-radius:4px;font-size:0.85rem;';

        var expOptions = [
          { value: '0', label: t('adminUi.expirationDisabled', "Désactivée (pas d'expiration)") },
          { value: '30', label: t('adminUi.days30', '30 jours') },
          { value: '60', label: t('adminUi.days60', '60 jours') },
          { value: '90', label: t('adminUi.days90', '90 jours') }
        ];
        for (var oi = 0; oi < expOptions.length; oi++) {
          var expOpt = document.createElement('option');
          expOpt.value = expOptions[oi].value;
          expOpt.textContent = expOptions[oi].label;
          expSelect.appendChild(expOpt);
        }

        expRow.appendChild(expLabel);
        expRow.appendChild(expSelect);
        polBody.appendChild(expRow);

        var polActions = document.createElement('div');
        polActions.className = 'settings-card__actions';

        var btnSavePol = document.createElement('button');
        btnSavePol.className = 'btn btn--primary btn--sm';
        btnSavePol.textContent = t('adminUi.save', 'Enregistrer');
        btnSavePol.addEventListener('click', function() {
          var expDays = parseInt(expSelect.value, 10);

          btnSavePol.disabled = true;
          btnSavePol.textContent = t('adminUi.saving', 'Enregistrement...');

          // FIX-SECCONFIG-CONTRACT (audit 2026-06-10) : l'ancien payload
          // {passwordPolicy:{expirationDays,...}} n'existait pas côté serveur (users.php
          // attend passwordMaxAgeDays à plat) → l'expiration n'était JAMAIS appliquée.
          window.CSI_AuthManager.updateSecurityConfig({
            passwordMaxAgeDays: expDays
          })
          .then(function(res) {
            if (!res || res.ok === false) {
              var srvMsg = (res && res.data && res.data.error) ? res.data.error : t('adminUi.serverRefusedConfig', "Le serveur a refusé la configuration.");
              showToast2(t('adminUi.policyErrorPrefix', 'Erreur politique de mots de passe : ') + srvMsg, 'error', 8000);
              return;
            }
            showToast2(t('adminUi.policySaved', 'Politique de mots de passe enregistrée.'), 'success');
          })
          .catch(function(err) {
            void 0;
            showToast2(t('adminUi.policySaveError', "Erreur lors de l'enregistrement de la politique."), 'error');
          })
          .finally(function() {
            btnSavePol.disabled = false;
            btnSavePol.textContent = t('adminUi.save', 'Enregistrer');
          });
        });

        polActions.appendChild(btnSavePol);
        polBody.appendChild(polActions);

        // FIX-SECCONFIG-CONTRACT : précharger l'expiration configurée (le select
        // s'affichait toujours sur « Désactivée »).
        if (window.CSI_AuthManager.getSecurityStatus) {
          window.CSI_AuthManager.getSecurityStatus().then(function(st) {
            var checks = st && st.checks;
            if (checks && typeof checks.passwordMaxAgeDays !== 'undefined') {
              expSelect.value = String(checks.passwordMaxAgeDays);
            }
          }).catch(function() {});
        }

        container.appendChild(polCard);
      })();
    }

    // ── Card F: Configuration SSO (OIDC) (server, admin only) ──
    if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isAdmin()) {
      (function renderSsoConfigCard() {
        var AM = window.CSI_AuthManager;
        var ssoRes = createSettingsCard({
          title: t('users.ssoCardTitle', 'Authentification SSO (OIDC)'),
          description: t('adminUi.ssoCardDesc', 'Connectez un fournisseur d’identité (Azure AD, Google Workspace, Okta…) pour l’authentification unique.'),
          icon: 'fa-building'
        });
        var ssoCard = ssoRes.card;
        var ssoBody = ssoRes.body;

        var ssoLoading = document.createElement('div');
        ssoLoading.textContent = t('users.ssoLoading', 'Chargement de la configuration SSO…');
        ssoLoading.style.cssText = 'padding:12px 0;color:#6b7280;font-size:0.9rem;';
        ssoBody.appendChild(ssoLoading);

        AM.getSsoConfig().then(function(res) {
          var cfg = (res && res.config) ? res.config : {};
          // (audit 2026-07-11) Config vierge (jamais configurée) : les options de sécurité
          // recommandées (vérif signature, email vérifié) sont proposées ACTIVÉES par défaut.
          // Une config existante conserve strictement ses valeurs (clé absente = OFF = comportement
          // historique → zéro régression).
          var isFresh = !(res && res.config);
          ssoBody.removeChild(ssoLoading);

          var form = document.createElement('div');
          form.className = 'settings-form-grid settings-form-grid--single-col';

          // --- Toggle: SSO enabled ---
          var toggleRow = document.createElement('label');
          toggleRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:12px;cursor:pointer;font-weight:600;';
          var chkEnabled = document.createElement('input');
          chkEnabled.type = 'checkbox';
          chkEnabled.checked = !!cfg.enabled;
          var toggleLabel = document.createElement('span');
          toggleLabel.textContent = t('users.ssoEnable', 'Activer l\'authentification SSO');
          toggleRow.appendChild(chkEnabled);
          toggleRow.appendChild(toggleLabel);
          form.appendChild(toggleRow);

          // --- Helper: create labelled input ---
          function makeSsoField(labelText, type, placeholder, value, mono) {
            var wrap = document.createElement('div');
            wrap.style.cssText = 'margin-bottom:10px;';
            var lbl = document.createElement('label');
            lbl.style.cssText = 'display:block;font-size:0.85rem;font-weight:600;margin-bottom:4px;color:#374151;';
            lbl.textContent = labelText;
            var inp = document.createElement('input');
            inp.type = type;
            inp.placeholder = placeholder || '';
            inp.value = value || '';
            inp.className = 'settings-input';
            inp.style.cssText = 'width:100%;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:0.9rem;' + (mono ? 'font-family:monospace;' : '');
            wrap.appendChild(lbl);
            wrap.appendChild(inp);
            return { wrap: wrap, input: inp };
          }

          // --- Helper: create labelled select ---
          function makeSsoSelect(labelText, options, selectedValue) {
            var wrap = document.createElement('div');
            wrap.style.cssText = 'margin-bottom:10px;';
            var lbl = document.createElement('label');
            lbl.style.cssText = 'display:block;font-size:0.85rem;font-weight:600;margin-bottom:4px;color:#374151;';
            lbl.textContent = labelText;
            var sel = document.createElement('select');
            sel.className = 'settings-input';
            sel.style.cssText = 'width:100%;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:0.9rem;';
            for (var i = 0; i < options.length; i++) {
              var opt = document.createElement('option');
              opt.value = options[i].value;
              opt.textContent = options[i].label;
              if (options[i].value === selectedValue) opt.selected = true;
              sel.appendChild(opt);
            }
            wrap.appendChild(lbl);
            wrap.appendChild(sel);
            return { wrap: wrap, select: sel };
          }

          // --- Helper: répéteur de mapping (clé texte → valeur select) (audit 2026-07-11) ---
          function makeMapRepeater(labelText, rightOptions, existingMap, keyPlaceholder) {
            var wrap = document.createElement('div');
            wrap.style.cssText = 'margin-bottom:10px;';
            var lbl = document.createElement('label');
            lbl.style.cssText = 'display:block;font-size:0.85rem;font-weight:600;margin-bottom:4px;color:#374151;';
            lbl.textContent = labelText;
            wrap.appendChild(lbl);
            var rowsBox = document.createElement('div');
            wrap.appendChild(rowsBox);
            function addRow(k, v) {
              var row = document.createElement('div');
              row.style.cssText = 'display:flex;gap:6px;margin-bottom:6px;align-items:center;';
              var keyInp = document.createElement('input');
              keyInp.type = 'text';
              keyInp.placeholder = keyPlaceholder || 'Groupe IdP';
              keyInp.value = k || '';
              keyInp.className = 'settings-input sso-map-key';
              keyInp.style.cssText = 'flex:1;padding:6px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:0.85rem;';
              var valSel = document.createElement('select');
              valSel.className = 'settings-input sso-map-val';
              valSel.style.cssText = 'flex:1;padding:6px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:0.85rem;';
              rightOptions.forEach(function(o) {
                var op = document.createElement('option');
                op.value = o.value; op.textContent = o.label;
                if (o.value === v) op.selected = true;
                valSel.appendChild(op);
              });
              var del = document.createElement('button');
              del.type = 'button'; del.className = 'btn btn--outline'; del.textContent = '✕';
              del.style.cssText = 'padding:4px 8px;font-size:0.8rem;';
              del.addEventListener('click', function() { rowsBox.removeChild(row); });
              row.appendChild(keyInp); row.appendChild(valSel); row.appendChild(del);
              rowsBox.appendChild(row);
            }
            if (existingMap && typeof existingMap === 'object') {
              Object.keys(existingMap).forEach(function(k) { addRow(k, existingMap[k]); });
            }
            var addBtn = document.createElement('button');
            addBtn.type = 'button'; addBtn.className = 'btn btn--outline';
            addBtn.style.cssText = 'font-size:0.8rem;padding:4px 12px;margin-top:2px;';
            addBtn.textContent = t('users.ssoMapAddRow', '+ Ajouter une règle');
            addBtn.addEventListener('click', function() { addRow('', rightOptions[0] ? rightOptions[0].value : ''); });
            wrap.appendChild(addBtn);
            function collect() {
              var map = {};
              for (var i = 0; i < rowsBox.children.length; i++) {
                var r = rowsBox.children[i];
                var kEl = r.querySelector('.sso-map-key');
                var vEl = r.querySelector('.sso-map-val');
                if (!kEl || !vEl) continue;
                var k = kEl.value.trim();
                if (k) map[k] = vEl.value;
              }
              return map;
            }
            return { wrap: wrap, collect: collect };
          }

          var issuerField = makeSsoField(t('users.ssoIssuer', 'Issuer URL'), 'url', 'https://login.microsoftonline.com/{tenant}/v2.0', cfg.issuer, true);
          form.appendChild(issuerField.wrap);

          var clientIdField = makeSsoField(t('users.ssoClientId', 'Client ID'), 'text', '', cfg.clientId, true);
          form.appendChild(clientIdField.wrap);

          var clientSecretField = makeSsoField(t('users.ssoClientSecret', 'Client Secret'), 'password', '', cfg.clientSecret, true);
          form.appendChild(clientSecretField.wrap);

          var modeSelect = makeSsoSelect(t('users.ssoMode', 'Mode d\'authentification'), [
            { value: 'sso_only', label: t('users.ssoModeOnly', 'SSO uniquement (recommandé)') },
            { value: 'sso_and_password', label: t('users.ssoModeMixed', 'SSO + mot de passe') }
          ], cfg.mode || 'sso_only');
          form.appendChild(modeSelect.wrap);

          var defaultRoleSelect = makeSsoSelect(t('users.ssoDefaultRole', 'Rôle par défaut des nouveaux utilisateurs SSO'), [
            { value: 'editor', label: t('users.ssoRoleEditor', 'Éditeur') },
            { value: 'viewer', label: t('users.ssoRoleViewer', 'Lecteur') }
          ], cfg.defaultRole || 'editor');
          form.appendChild(defaultRoleSelect.wrap);

          // === CSI-ME === (Lot 3a) Sous-entité par défaut des nouveaux comptes SSO.
          // Visible seulement en mode multi-entités actif ; appliquée à la création
          // uniquement (l'appartenance d'un compte existant n'est jamais modifiée par le SSO).
          var defaultEntitySelect = null;
          if (window.CSI_EntityManager && window.CSI_EntityManager.isMultiEntity()) {
            var _deOpts = [{ value: '', label: t('users.ssoDefaultEntityNone', 'Entité principale (aucune)') }];
            window.CSI_EntityManager.entities().forEach(function(e) {
              if (e && !e.archived) _deOpts.push({ value: e.id, label: e.name || e.id });
            });
            defaultEntitySelect = makeSsoSelect(t('users.ssoDefaultEntity', 'Sous-entité par défaut des nouveaux utilisateurs SSO'), _deOpts, cfg.defaultEntityId || '');
            form.appendChild(defaultEntitySelect.wrap);
          }

          // M31 (audit 2026-07-10) : défaut = plafond produit (100), pas 50 — enregistrer la
          // carte SSO sans toucher ce champ n'abaisse plus silencieusement le plafond global.
          // Max borné au plafond produit côté serveur (getSsoMaxUsers/validation).
          var maxUsersField = makeSsoField(t('users.ssoMaxUsers', 'Nombre max. d\'utilisateurs (tous comptes)'), 'number', '100', cfg.maxUsers != null ? String(cfg.maxUsers) : '100', false);
          maxUsersField.input.min = '5';
          maxUsersField.input.max = '100';
          form.appendChild(maxUsersField.wrap);

          // M32 (audit 2026-07-10) : allowlist de domaines email pour le provisioning SSO.
          // Vide = aucune restriction. Sépare par virgules ou retours ligne.
          var _allowedDomVal = (cfg.allowedDomains && Array.isArray(cfg.allowedDomains)) ? cfg.allowedDomains.join(', ') : '';
          var allowedDomainsField = makeSsoField(t('users.ssoAllowedDomains', 'Domaines email autorisés (vide = tous)'), 'text', 'exemple.be, exemple.fr', _allowedDomVal, false);
          form.appendChild(allowedDomainsField.wrap);

          // (audit 2026-07-11, finding 2.1) Avertissement : allowlist de domaines vide + SSO activé.
          var domainsWarn = document.createElement('div');
          domainsWarn.style.cssText = 'background:#fffbeb;border:1px solid #fde68a;border-radius:4px;padding:6px 10px;font-size:0.8rem;color:#92400e;margin:-4px 0 10px;display:none;';
          domainsWarn.textContent = t('users.ssoAllowedDomainsWarn', 'Aucun domaine autorisé : tout compte renvoyé par l\'IdP peut obtenir un accès. Renseignez vos domaines, surtout avec un IdP multi-tenant (Google, Azure « common »).');
          form.appendChild(domainsWarn);
          function refreshDomainsWarn() {
            domainsWarn.style.display = (chkEnabled.checked && allowedDomainsField.input.value.trim() === '') ? 'block' : 'none';
          }
          allowedDomainsField.input.addEventListener('input', refreshDomainsWarn);
          chkEnabled.addEventListener('change', refreshDomainsWarn);
          refreshDomainsWarn();

          // ============================================================
          // (audit 2026-07-11, finding 1.1) Mapping des groupes IdP → rôle / entité (RBAC)
          // ============================================================
          var mapToggle = document.createElement('button');
          mapToggle.type = 'button'; mapToggle.className = 'btn btn--outline';
          mapToggle.style.cssText = 'margin:12px 0 6px;font-size:0.82rem;padding:4px 12px;';
          var _mapLabel = t('users.ssoMapSection', 'Mapping des groupes (rôles & entités)');
          mapToggle.textContent = _mapLabel + ' ▼';
          var mapPanel = document.createElement('div');
          mapPanel.style.cssText = 'display:none;border:1px solid #e5e7eb;border-radius:6px;padding:12px;margin-bottom:10px;background:#fafafa;';
          mapToggle.addEventListener('click', function() {
            var vis = mapPanel.style.display !== 'none';
            mapPanel.style.display = vis ? 'none' : 'block';
            mapToggle.textContent = _mapLabel + ' ' + (vis ? '▼' : '▲');
          });
          form.appendChild(mapToggle);

          var mapInfo = document.createElement('div');
          mapInfo.style.cssText = 'background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;padding:8px 10px;margin-bottom:10px;font-size:0.82rem;color:#1e40af;';
          mapInfo.textContent = t('users.ssoMapInfo', 'Attribuez automatiquement un rôle et/ou une entité selon les groupes envoyés par l\'IdP. Sans correspondance, le rôle/entité par défaut s\'applique.');
          mapPanel.appendChild(mapInfo);

          var groupClaimField = makeSsoField(t('users.ssoGroupClaim', 'Nom du claim de groupe (ex. groups, roles)'), 'text', 'groups', cfg.groupClaim || '', true);
          mapPanel.appendChild(groupClaimField.wrap);

          var scopesField = makeSsoField(t('users.ssoScopes', 'Scopes OIDC (vide = openid email profile)'), 'text', 'openid email profile groups', cfg.scopes || '', true);
          mapPanel.appendChild(scopesField.wrap);

          var roleMapRepeater = makeMapRepeater(t('users.ssoGroupToRole', 'Groupe → rôle'), [
            { value: 'editor', label: t('users.ssoRoleEditor', 'Éditeur') },
            { value: 'viewer', label: t('users.ssoRoleViewer', 'Lecteur') }
          ], cfg.groupToRole || {}, t('users.ssoGroupPlaceholder', 'Groupe IdP'));
          mapPanel.appendChild(roleMapRepeater.wrap);

          var entityMapRepeater = null;
          if (window.CSI_EntityManager && window.CSI_EntityManager.isMultiEntity()) {
            var _emOpts = [];
            window.CSI_EntityManager.entities().forEach(function(e) {
              if (e && !e.archived) _emOpts.push({ value: e.id, label: e.name || e.id });
            });
            if (_emOpts.length) {
              entityMapRepeater = makeMapRepeater(t('users.ssoGroupToEntity', 'Groupe → entité'), _emOpts, cfg.groupToEntity || {}, t('users.ssoGroupPlaceholder', 'Groupe IdP'));
              mapPanel.appendChild(entityMapRepeater.wrap);
            }
          }

          var chkStrict = document.createElement('input');
          (function() {
            var strictWrap = document.createElement('div');
            strictWrap.style.cssText = 'margin:8px 0;display:flex;align-items:center;gap:8px;';
            chkStrict.type = 'checkbox'; chkStrict.checked = !!cfg.strictMapping; chkStrict.id = 'sso-strict';
            var l = document.createElement('label'); l.htmlFor = 'sso-strict';
            l.style.cssText = 'font-size:0.85rem;color:#374151;cursor:pointer;';
            l.textContent = t('users.ssoStrictMapping', 'Refuser les comptes sans groupe mappé (strict)');
            strictWrap.appendChild(chkStrict); strictWrap.appendChild(l); mapPanel.appendChild(strictWrap);
          })();

          var chkReconcile = document.createElement('input');
          (function() {
            var recWrap = document.createElement('div');
            recWrap.style.cssText = 'margin:8px 0;display:flex;align-items:center;gap:8px;';
            chkReconcile.type = 'checkbox'; chkReconcile.checked = !!cfg.reconcileOnLogin; chkReconcile.id = 'sso-reconcile';
            var l = document.createElement('label'); l.htmlFor = 'sso-reconcile';
            l.style.cssText = 'font-size:0.85rem;color:#374151;cursor:pointer;';
            l.textContent = t('users.ssoReconcile', 'Ré-appliquer le mapping à chaque connexion');
            recWrap.appendChild(chkReconcile); recWrap.appendChild(l); mapPanel.appendChild(recWrap);
          })();

          form.appendChild(mapPanel);
          if (cfg.groupClaim || (cfg.groupToRole && Object.keys(cfg.groupToRole).length)
              || (cfg.groupToEntity && Object.keys(cfg.groupToEntity).length) || cfg.strictMapping || cfg.reconcileOnLogin) {
            mapPanel.style.display = 'block';
            mapToggle.textContent = _mapLabel + ' ▲';
          }

          // ============================================================
          // (audit 2026-07-11, findings 2.1/2.2/2.4) Sécurité renforcée
          // ============================================================
          var secToggle = document.createElement('button');
          secToggle.type = 'button'; secToggle.className = 'btn btn--outline';
          secToggle.style.cssText = 'margin:6px 0;font-size:0.82rem;padding:4px 12px;';
          var _secLabel = t('users.ssoSecSection', 'Sécurité renforcée (recommandé)');
          secToggle.textContent = _secLabel + ' ▼';
          var secPanel = document.createElement('div');
          secPanel.style.cssText = 'display:none;border:1px solid #e5e7eb;border-radius:6px;padding:12px;margin-bottom:10px;background:#fafafa;';
          secToggle.addEventListener('click', function() {
            var vis = secPanel.style.display !== 'none';
            secPanel.style.display = vis ? 'none' : 'block';
            secToggle.textContent = _secLabel + ' ' + (vis ? '▼' : '▲');
          });
          form.appendChild(secToggle);
          function makeSecCheck(id, labelText, checked) {
            var w = document.createElement('div'); w.style.cssText = 'margin:6px 0;display:flex;align-items:center;gap:8px;';
            var c = document.createElement('input'); c.type = 'checkbox'; c.id = id; c.checked = checked;
            var l = document.createElement('label'); l.htmlFor = id; l.style.cssText = 'font-size:0.85rem;color:#374151;cursor:pointer;'; l.textContent = labelText;
            w.appendChild(c); w.appendChild(l); secPanel.appendChild(w); return c;
          }
          var _defVerifySig = (cfg.verifyIdTokenSignature != null ? !!cfg.verifyIdTokenSignature : isFresh);
          var _defReqEmail = (cfg.requireEmailVerified != null ? !!cfg.requireEmailVerified : isFresh);
          var chkVerifySig = makeSecCheck('sso-verifysig', t('users.ssoVerifySig', 'Vérifier la signature des jetons (JWKS) — recommandé'), _defVerifySig);
          var chkReqEmailVer = makeSecCheck('sso-reqemail', t('users.ssoRequireEmailVerified', 'Exiger un email vérifié pour créer un compte — recommandé'), _defReqEmail);
          var chkIdpLogout = makeSecCheck('sso-idplogout', t('users.ssoIdpLogout', 'Déconnexion aussi côté fournisseur (SSO logout)'), !!cfg.idpLogout);
          form.appendChild(secPanel);
          if (_defVerifySig || _defReqEmail || cfg.idpLogout) {
            secPanel.style.display = 'block';
            secToggle.textContent = _secLabel + ' ▲';
          }

          // --- Advanced: Enterprise IAM options (collapsible) ---
          var advancedToggle = document.createElement('button');
          advancedToggle.type = 'button';
          advancedToggle.className = 'btn btn--outline';
          advancedToggle.style.cssText = 'margin:12px 0 6px;font-size:0.82rem;padding:4px 12px;';
          advancedToggle.textContent = t('users.ssoAdvanced', 'Options avancées (IAM local / on-premise)') + ' \u25BC';
          var advancedPanel = document.createElement('div');
          advancedPanel.style.cssText = 'display:none;border:1px solid #e5e7eb;border-radius:6px;padding:12px;margin-bottom:10px;background:#fafafa;';
          advancedToggle.addEventListener('click', function() {
            var visible = advancedPanel.style.display !== 'none';
            advancedPanel.style.display = visible ? 'none' : 'block';
            advancedToggle.textContent = t('users.ssoAdvanced', 'Options avancées (IAM local / on-premise)') + ' ' + (visible ? '\u25BC' : '\u25B2');
          });
          form.appendChild(advancedToggle);

          // Allow HTTP checkbox
          var httpWrap = document.createElement('div');
          httpWrap.style.cssText = 'margin-bottom:8px;display:flex;align-items:center;gap:8px;';
          var chkHttp = document.createElement('input');
          chkHttp.type = 'checkbox';
          chkHttp.checked = !!cfg.allowHttp;
          chkHttp.id = 'sso-allow-http';
          var lblHttp = document.createElement('label');
          lblHttp.htmlFor = 'sso-allow-http';
          lblHttp.style.cssText = 'font-size:0.85rem;color:#374151;cursor:pointer;';
          lblHttp.textContent = t('users.ssoAllowHttp', 'Autoriser HTTP (IAM local sans certificat TLS)');
          httpWrap.appendChild(chkHttp);
          httpWrap.appendChild(lblHttp);
          advancedPanel.appendChild(httpWrap);

          // CA Certificate path
          var caField = makeSsoField(t('users.ssoCaPath', 'Chemin du certificat CA (PKI interne)'), 'text', '/etc/ssl/certs/enterprise-ca.crt', cfg.caCertPath || '', false);
          advancedPanel.appendChild(caField.wrap);

          // Insecure skip verify checkbox
          var insecureWrap = document.createElement('div');
          insecureWrap.style.cssText = 'margin-bottom:4px;display:flex;align-items:center;gap:8px;';
          var chkInsecure = document.createElement('input');
          chkInsecure.type = 'checkbox';
          chkInsecure.checked = !!cfg.insecureSkipVerify;
          chkInsecure.id = 'sso-insecure';
          var lblInsecure = document.createElement('label');
          lblInsecure.htmlFor = 'sso-insecure';
          lblInsecure.style.cssText = 'font-size:0.85rem;color:#991b1b;cursor:pointer;';
          lblInsecure.textContent = t('users.ssoSkipVerify', 'Désactiver la vérification TLS (dev/test uniquement !)');
          insecureWrap.appendChild(chkInsecure);
          insecureWrap.appendChild(lblInsecure);
          advancedPanel.appendChild(insecureWrap);

          // Warning for insecure mode
          var insecureWarn = document.createElement('div');
          insecureWarn.style.cssText = 'background:#fef2f2;border:1px solid #fca5a5;border-radius:4px;padding:6px 10px;font-size:0.8rem;color:#991b1b;margin-top:4px;display:' + (chkInsecure.checked ? 'block' : 'none') + ';';
          insecureWarn.textContent = t('users.ssoSkipVerifyWarn', 'Attention : désactiver la vérification TLS expose les échanges SSO à des attaques man-in-the-middle. À utiliser uniquement en environnement de test.');
          chkInsecure.addEventListener('change', function() {
            insecureWarn.style.display = chkInsecure.checked ? 'block' : 'none';
          });
          advancedPanel.appendChild(insecureWarn);

          form.appendChild(advancedPanel);

          // Show advanced panel if any option is non-default
          if (cfg.allowHttp || cfg.caCertPath || cfg.insecureSkipVerify) {
            advancedPanel.style.display = 'block';
            advancedToggle.textContent = t('users.ssoAdvanced', 'Options avancées (IAM local / on-premise)') + ' \u25B2';
          }

          // --- Info note ---
          var infoNote = document.createElement('div');
          infoNote.style.cssText = 'background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;padding:10px 12px;margin:10px 0;font-size:0.85rem;color:#1e40af;';
          infoNote.textContent = t('users.ssoPasswordNote', 'Le login par mot de passe reste toujours disponible pour le compte administrateur, même en mode SSO uniquement.');
          form.appendChild(infoNote);

          // --- Callback URL display ---
          var callbackUrl = window.location.origin + window.location.pathname.replace(/\/[^\/]*$/, '/data/users.php?action=sso-callback');
          var cbWrap = document.createElement('div');
          cbWrap.style.cssText = 'margin-bottom:10px;';
          var cbLabel = document.createElement('label');
          cbLabel.style.cssText = 'display:block;font-size:0.85rem;font-weight:600;margin-bottom:4px;color:#374151;';
          cbLabel.textContent = t('users.ssoCallbackUrl', 'Callback URL (à configurer dans votre IdP)');
          var cbDisplay = document.createElement('div');
          cbDisplay.style.cssText = 'background:#f3f4f6;border:1px solid #d1d5db;border-radius:6px;padding:8px 10px;font-family:monospace;font-size:0.85rem;word-break:break-all;color:#374151;user-select:all;';
          cbDisplay.textContent = callbackUrl;
          cbWrap.appendChild(cbLabel);
          cbWrap.appendChild(cbDisplay);
          form.appendChild(cbWrap);

          ssoBody.appendChild(form);

          // --- Buttons ---
          var btnRow = document.createElement('div');
          btnRow.style.cssText = 'display:flex;gap:8px;margin-top:16px;';

          var btnTest = document.createElement('button');
          btnTest.className = 'btn btn--outline';
          btnTest.textContent = t('users.ssoTestBtn', 'Tester la connexion');
          btnTest.addEventListener('click', function() {
            var issuer = issuerField.input.value.trim();
            if (!issuer) {
              showToast2(t('users.ssoIssuerRequired', 'Veuillez renseigner l\'Issuer URL.'), 'error');
              return;
            }
            btnTest.disabled = true;
            btnTest.textContent = t('users.ssoTesting', 'Test en cours…');
            AM.testSsoConnection(issuer).then(function(res) {
              // FIX-SSO-FALSE-OK (audit 2026-06-10) : _fetch resout {ok:false} sur erreur
              // HTTP (4xx/5xx) sans rejeter — sans ce test, un echec serveur affichait
              // quand meme « Connexion SSO OK » et l'admin activait un SSO casse.
              if (!res || res.ok === false) {
                var srvMsg = (res && res.data && res.data.error) ? res.data.error : t('adminUi.ssoTestRefused', 'Le serveur a refusé le test SSO.');
                showToast2(t('users.ssoTestFailedPrefix', 'Échec du test SSO : ') + srvMsg, 'error');
                return;
              }
              var msg = t('adminUi.ssoTestOk', 'Connexion SSO OK : le fournisseur a répondu.');
              if (res && res.data && res.data.certAutoFetched) {
                msg += ' ' + t('adminUi.ssoCertAutoFetched', 'Certificat TLS récupéré automatiquement') + ' (' + (res.data.certAutoFetched.subject || '?') + ').';
              }
              showToast2(msg, 'success');
            }).catch(function(err) {
              // Map server error messages (from JSON response) vs network errors (from JS exceptions)
              var errMsg = (err && err.data && err.data.error) ? err.data.error : t('adminUi.ssoUnreachable', 'Impossible de contacter le fournisseur SSO. Vérifiez l\'URL et la connectivité réseau.');
              showToast2(t('users.ssoTestFailedPrefix', 'Échec du test SSO : ') + errMsg, 'error');
            }).finally(function() {
              btnTest.disabled = false;
              btnTest.textContent = t('users.ssoTestBtn', 'Tester la connexion');
            });
          });
          btnRow.appendChild(btnTest);

          var btnSave = document.createElement('button');
          btnSave.className = 'btn btn--primary';
          btnSave.textContent = t('users.ssoSaveBtn', 'Enregistrer');
          btnSave.addEventListener('click', function() {
            var enabled = chkEnabled.checked;
            var issuer = issuerField.input.value.trim();
            var clientId = clientIdField.input.value.trim();
            var clientSecret = clientSecretField.input.value.trim();

            if (enabled && (!issuer || !clientId || !clientSecret)) {
              showToast2(t('users.ssoRequiredFields', 'Issuer URL, Client ID et Client Secret sont requis pour activer le SSO.'), 'error');
              return;
            }

            // Validate caCertPath format
            var caPath = caField.input.value.trim();
            if (caPath && (caPath.indexOf('..') !== -1 || caPath.length > 512 || caPath.charAt(0) !== '/')) {
              showToast2(t('users.ssoCaInvalid', 'Chemin CA invalide. Utilisez un chemin absolu sans ".." (ex: /etc/ssl/certs/ca.pem)'), 'error');
              return;
            }

            btnSave.disabled = true;
            btnSave.textContent = t('users.ssoSaving', 'Enregistrement…');
            AM.saveSsoConfig({
              enabled: enabled,
              issuer: issuer,
              clientId: clientId,
              clientSecret: clientSecret,
              mode: modeSelect.select.value,
              defaultRole: defaultRoleSelect.select.value,
              // === CSI-ME === (audit SSO 2026-07-03) : si le sélecteur n'est pas rendu
              // (feature off/indisponible au rendu), PRÉSERVER la valeur stockée — envoyer
              // '' effaçait silencieusement defaultEntityId à chaque enregistrement.
              defaultEntityId: defaultEntitySelect ? defaultEntitySelect.select.value : ((cfg && cfg.defaultEntityId) || ''),
              maxUsers: parseInt(maxUsersField.input.value, 10) || 100,
              allowedDomains: allowedDomainsField.input.value, // M32 : le serveur parse/normalise
              allowHttp: chkHttp.checked,
              caCertPath: caPath,
              insecureSkipVerify: chkInsecure.checked,
              // (audit 2026-07-11) Mapping RBAC + sécurité renforcée.
              scopes: scopesField.input.value.trim(),
              groupClaim: groupClaimField.input.value.trim(),
              groupToRole: roleMapRepeater.collect(),
              // Préserver la map stockée si le répéteur d'entité n'est pas rendu (multi-entités off).
              groupToEntity: entityMapRepeater ? entityMapRepeater.collect() : ((cfg && cfg.groupToEntity) || {}),
              strictMapping: chkStrict.checked,
              reconcileOnLogin: chkReconcile.checked,
              requireEmailVerified: chkReqEmailVer.checked,
              verifyIdTokenSignature: chkVerifySig.checked,
              idpLogout: chkIdpLogout.checked
            }).then(function(res) {
              // FIX-SSO-FALSE-OK (audit 2026-06-10) : voir bouton « Tester la connexion » —
              // un refus serveur (maxUsers hors bornes, licence expiree, erreur disque...)
              // affichait quand meme « Configuration SSO enregistree. ».
              if (!res || res.ok === false) {
                var srvMsg = (res && res.data && res.data.error) ? res.data.error : t('adminUi.serverRefusedConfig', 'Le serveur a refusé la configuration.');
                showToast2(t('users.ssoErrorPrefix', 'Erreur : ') + srvMsg, 'error');
                return;
              }
              showToast2(t('users.ssoSaved', 'Configuration SSO enregistrée.'), 'success');
            }).catch(function(err) {
              showToast2(t('users.ssoErrorPrefix', 'Erreur : ') + (err.message || err), 'error');
            }).finally(function() {
              btnSave.disabled = false;
              btnSave.textContent = t('users.ssoSaveBtn', 'Enregistrer');
            });
          });
          btnRow.appendChild(btnSave);

          ssoBody.appendChild(btnRow);
        }).catch(function(err) {
          ssoLoading.textContent = t('users.ssoLoadFailed', 'Impossible de charger la configuration SSO.');
          void 0;
        });

        container.appendChild(ssoCard);
      })();
    }
  }

  // === SECTION: DATA (Backup, Export, Snapshots) ===
  function renderDataSection(container) {
    // Backup Card
    const { card: backupCard, body: backupBody } = createSettingsCard({
      title: t('settings.backupTitle', 'Sauvegarde'),
      description: t('settings.backupDesc', 'Exportez ou importez vos données SMSI.'),
      icon: "fa-download"
    });

    const quickActions = document.createElement("div");
    quickActions.className = "quick-actions";

    const exportAction = document.createElement("div");
    exportAction.className = "quick-action";
    exportAction.id = "settingsExport";
    exportAction.innerHTML = `
      <div class="quick-action__icon"><i class="fas fa-download"></i></div>
      <span class="quick-action__label">${t('settings.export')}</span>
      <span class="quick-action__hint">.json</span>
    `;
    exportAction.addEventListener("click", exportData);

    const importAction = document.createElement("div");
    importAction.className = "quick-action";
    importAction.id = "settingsImport";
    importAction.dataset.readonlyLock = "true";
    importAction.innerHTML = `
      <div class="quick-action__icon"><i class="fas fa-upload"></i></div>
      <span class="quick-action__label">${t('settings.import')}</span>
      <span class="quick-action__hint">.json</span>
    `;
    importAction.addEventListener("click", importData);

    const pdfAction = document.createElement("div");
    pdfAction.className = "quick-action";
    pdfAction.id = "settingsPdfReport";
    pdfAction.innerHTML = `
      <div class="quick-action__icon"><i class="fas fa-file-pdf"></i></div>
      <span class="quick-action__label">${t('settings.pdfReport')}</span>
      <span class="quick-action__hint">${t('settings.complete')}</span>
    `;
    pdfAction.addEventListener("click", () => {
      if (typeof generatePdfReportWithFeedback === "function") {
        generatePdfReportWithFeedback();
      } else {
        showToast2(t('exportLabels.pdfUnavailable'), "error");
      }
    });

    const excelAction = document.createElement("div");
    excelAction.className = "quick-action";
    excelAction.id = "settingsExcelAll";
    excelAction.innerHTML = `
      <div class="quick-action__icon"><i class="fas fa-file-excel"></i></div>
      <span class="quick-action__label">${t('settings.excelExport')}</span>
      <span class="quick-action__hint">${t('settings.global')}</span>
    `;
    excelAction.addEventListener("click", exportAllSectionsExcel);

    const analyseImportAction = document.createElement("div");
    analyseImportAction.className = "quick-action";
    analyseImportAction.dataset.readonlyLock = "true";
    analyseImportAction.innerHTML = `
      <div class="quick-action__icon"><i class="fas fa-file-import"></i></div>
      <span class="quick-action__label">${t('settings.importAnalyse') || 'Import Analyse'}</span>
      <span class="quick-action__hint">.analyse</span>
    `;
    analyseImportAction.addEventListener("click", () => {
      if (typeof window.raImportAnalyseFile === 'function') window.raImportAnalyseFile();
      else showToast2(t('toasts.riskAnalysisModuleNotLoaded', 'Module d\'analyse de risques non chargé.'), "error");
    });

    quickActions.append(exportAction, importAction, analyseImportAction, pdfAction, excelAction);
    backupBody.appendChild(quickActions);
    container.appendChild(backupCard);

    // Snapshots Card
    const { card: snapshotCard, body: snapshotBody } = createSettingsCard({
      title: t('settings.snapshotTitle', 'Historique & Snapshots'),
      description: t('settings.snapshotDesc', 'Créez des points de sauvegarde locaux.'),
      icon: "fa-history"
    });

    const snapshotGrid = document.createElement("div");
    snapshotGrid.className = "settings-form-grid";

    const { wrap: snapshotLabelWrap, input: snapshotLabelInput } = createSettingsInputGroup({
      id: "snapshotLabel",
      label: t('settings.snapshotLabel', 'Nom du snapshot'),
      placeholder: t('settings.snapshotPlaceholder', 'Ex. : comité sécurité T2'),
      readonly: false
    });

    snapshotGrid.appendChild(snapshotLabelWrap);
    snapshotBody.appendChild(snapshotGrid);

    const snapshotActions = document.createElement("div");
    snapshotActions.className = "settings-card__actions";

    const btnValidate = document.createElement("button");
    btnValidate.id = "settingsValidate";
    btnValidate.className = "btn btn--outline btn--sm";
    btnValidate.innerHTML = '<i class="fas fa-search"></i> ' + escapeHtml(t('settings.analyze', 'Analyser'));
    btnValidate.addEventListener("click", validateDataQuality);

    const btnSnapshot = document.createElement("button");
    btnSnapshot.id = "settingsSnapshot";
    btnSnapshot.className = "btn btn--primary btn--sm";
    btnSnapshot.innerHTML = '<i class="fas fa-save"></i> ' + escapeHtml(t('settings.createSnapshot', 'Créer'));
    btnSnapshot.dataset.readonlyLock = "true";
    btnSnapshot.addEventListener("click", () => {
      const label = snapshotLabelInput.value.trim() || t('settings.manualSnapshot', 'Snapshot manuel');
      addHistorySnapshot(label);
      snapshotLabelInput.value = "";
    });

    const btnSnapshotClear = document.createElement("button");
    btnSnapshotClear.className = "btn btn--outline btn--sm";
    btnSnapshotClear.textContent = t('settings.cleanSnapshots', 'Nettoyer');
    btnSnapshotClear.dataset.readonlyLock = "true";
    btnSnapshotClear.addEventListener("click", clearHistorySnapshots);

    snapshotActions.append(btnValidate, btnSnapshot, btnSnapshotClear);
    snapshotBody.appendChild(snapshotActions);

    // Snapshots Timeline
    const historyList = document.createElement("div");
    historyList.id = "historyList";
    historyList.className = "snapshots-timeline";
    snapshotBody.appendChild(historyList);

    container.appendChild(snapshotCard);
    renderHistoryList();
  }

  // === SECTION: STORAGE ===
  function renderStorageSection(container) {
    var inServerMode = isServerModeUnlocked();
    // Server mode: show server storage card first
    if (inServerMode) {
      var { card: srvStorageCard, body: srvStorageBody } = createSettingsCard({
        title: t('settings.storageServerTitle', 'Stockage serveur'),
        description: t('settings.storageServerDesc', 'Vos données sont stockées et synchronisées sur le serveur.'),
        icon: 'fa-server',
        status: true
      });
      var srvInfo = document.createElement('div');
      srvInfo.className = 'settings-info-banner';
      var srvInfoIcon = document.createElement('i');
      srvInfoIcon.className = 'fas fa-check-circle';
      var srvInfoText = document.createElement('span');
      srvInfoText.textContent = ' ' + t('settings.storageServerActive', 'Le serveur gère le stockage principal. Toutes les modifications sont synchronisées automatiquement.');
      srvInfo.appendChild(srvInfoIcon);
      srvInfo.appendChild(srvInfoText);
      srvStorageBody.appendChild(srvInfo);

      // Show last sync timestamps
      var srvSyncStats = document.createElement('div');
      srvSyncStats.className = 'storage-stats';
      var formatSyncDate = function(kind) {
        // FIX-SYNCMETA-DISPLAY (audit 2026-06-10, bundle.js:9645) : en mode serveur les
        // timestamps de sync ne sont conservés QU'EN MÉMOIRE (CSI_Storage.getSyncTimestamps) —
        // la lecture directe de localStorage affichait toujours « Jamais ».
        try {
          var meta = (window.CSI_Storage && window.CSI_Storage.getSyncTimestamps) ? window.CSI_Storage.getSyncTimestamps() : {};
          var val = kind === 'pull' ? meta.lastPull : meta.lastPush;
          var ts = Date.parse(val || '');
          return Number.isFinite(ts) ? new Date(ts).toLocaleString() : t('common.never', 'Jamais');
        } catch(e) { return t('common.never', 'Jamais'); }
      };
      var createSyncStat = function(label, value) {
        var stat = document.createElement('div');
        stat.className = 'storage-stat';
        var valEl = document.createElement('div');
        valEl.className = 'storage-stat__value';
        valEl.textContent = value;
        var lblEl = document.createElement('div');
        lblEl.className = 'storage-stat__label';
        lblEl.textContent = label;
        stat.append(valEl, lblEl);
        return stat;
      };
      srvSyncStats.append(
        createSyncStat(t('settings.lastServerPull', 'Dernière réception'), formatSyncDate('pull')),
        createSyncStat(t('settings.lastServerPush', 'Dernier envoi'), formatSyncDate('push'))
      );
      srvStorageBody.appendChild(srvSyncStats);
      container.appendChild(srvStorageCard);
    }

    // Note : en mode serveur, la carte « Stockage serveur » ci-dessus couvre déjà l'information
    // (une seconde carte redondante du même titre a été retirée — audit 2026-07-10). La carte
    // « Stockage & Santé » (localStorage) qui suit reste masquée en mode serveur.
    const { card, body } = createSettingsCard({
      title: t('settings.storageTitle', 'Stockage & Santé'),
      description: t('settings.storageDesc', "Surveillance de l'occupation localStorage."),
      icon: "fa-hard-drive"
    });
    if (inServerMode) {
      card.style.display = 'none';
    }

    const gaugeWrap = document.createElement("div");
    gaugeWrap.style.marginBottom = "var(--spacing-md)";

    const gaugeLabel = document.createElement("div");
    gaugeLabel.className = "settings-form-label";
    gaugeLabel.textContent = inServerMode ? t('settings.storageUsageServer', 'Occupation du cache local') : t('settings.storageUsage', 'Occupation localStorage');
    gaugeLabel.style.marginBottom = "var(--spacing-xs)";

    const gauge = document.createElement("div");
    gauge.className = "storage-gauge";
    const gaugeBar = document.createElement("div");
    gaugeBar.id = "storageUsageBar";
    gaugeBar.className = "storage-gauge__fill";
    gauge.appendChild(gaugeBar);

    const gaugeText = document.createElement("div");
    gaugeText.id = "storageUsageText";
    gaugeText.className = "settings-form-help";
    gaugeText.style.marginTop = "var(--spacing-xs)";

    gaugeWrap.append(gaugeLabel, gauge, gaugeText);
    body.appendChild(gaugeWrap);

    const stats = document.createElement("div");
    stats.className = "storage-stats";

    const createStat = (label, id) => {
      const stat = document.createElement("div");
      stat.className = "storage-stat";
      const value = document.createElement("div");
      value.id = id;
      value.className = "storage-stat__value";
      value.textContent = "—";
      const lbl = document.createElement("div");
      lbl.className = "storage-stat__label";
      lbl.textContent = label;
      stat.append(value, lbl);
      return stat;
    };

    stats.append(
      createStat(t('settings.dataSize', 'Taille données'), "storageDatasetSize"),
      createStat(t('settings.lastAutosave', 'Dernier autosave'), "storageLastSave")
    );
    body.appendChild(stats);

    const warning = document.createElement("div");
    warning.id = "storageHealthWarning";
    warning.className = "danger-warning";
    warning.hidden = true;
    warning.innerHTML = `
      <i class="fas fa-exclamation-triangle danger-warning__icon"></i>
      <span class="danger-warning__text">${escapeHtml(t('settings.storageLow', 'Espace de stockage faible. Exportez vos données.'))}</span>
    `;
    body.appendChild(warning);

    container.appendChild(card);
    refreshStorageHealth();
  }

  // === SECTION: LICENSE ===
  function renderLicenseSection(container) {
    var TM = window.CSI_TrialManager;

    // --- Card 1: License Status ---
    var { card: statusCard, body: statusBody } = createSettingsCard({
      title: t('settings.license.statusTitle', 'Statut de la licence'),
      description: t('settings.license.statusDesc', 'Informations sur votre licence ou période d\'essai.'),
      icon: 'fa-id-badge'
    });

    var statusDiv = document.createElement('div');
    statusDiv.className = 'license-status-display';
    statusDiv.style.cssText = 'display:flex;align-items:center;gap:var(--spacing-md);padding:var(--spacing-md);border-radius:var(--radius-md,8px);';

    var statusIcon = document.createElement('div');
    statusIcon.style.cssText = 'font-size:2rem;flex-shrink:0;';

    var statusInfo = document.createElement('div');
    statusInfo.style.cssText = 'flex:1;';

    var statusTitle = document.createElement('div');
    statusTitle.style.cssText = 'font-weight:600;font-size:1.05rem;margin-bottom:4px;';

    var statusDetail = document.createElement('div');
    statusDetail.style.cssText = 'font-size:0.9rem;color:var(--color-text-secondary,#666);';

    if (TM && TM.isLicensed() && !TM.isLicenseExpired()) {
      // Licensed
      statusDiv.style.background = 'var(--color-success-bg,#f0fdf4)';
      statusDiv.style.border = '1px solid var(--color-success-light,#86efac)';
      statusIcon.textContent = '\u2705';
      statusTitle.textContent = t('license.active', 'Licence active');
      statusTitle.style.color = 'var(--color-success,#22c55e)';
      var org = TM.getOrganization();
      var state = TM.getState();
      var details = [];
      if (org) details.push(t('license.organization', 'Organisation') + ' : ' + org);
      if (state && state.expiresAt) details.push(t('license.expiresAt', 'Expire le {date}').replace('{date}', new Date(state.expiresAt).toLocaleDateString()));
      statusDetail.textContent = details.join(' — ') || '';
    } else if (TM && TM.isLicenseExpired()) {
      // License expired
      statusDiv.style.background = 'var(--color-danger-bg,#fef2f2)';
      statusDiv.style.border = '1px solid var(--color-danger-light,#fca5a5)';
      statusIcon.textContent = '\u26A0\uFE0F';
      statusTitle.textContent = t('license.expired', 'Licence expirée');
      statusTitle.style.color = 'var(--color-danger,#dc3545)';
      statusDetail.textContent = t('license.renewNeeded', 'Licence expirée — entrez une nouvelle clé pour réactiver');
    } else if (TM && TM.isTrialActive()) {
      // Trial active
      var daysLeft = TM.getDaysLeft();
      statusDiv.style.background = 'var(--color-warning-bg,#fffbeb)';
      statusDiv.style.border = '1px solid var(--color-warning-light,#fde68a)';
      statusIcon.textContent = '\u23F3';
      statusTitle.textContent = t('license.trialActive', 'Période d\'essai active');
      statusTitle.style.color = 'var(--color-warning-dark,#92400e)';
      statusDetail.textContent = t('license.trialDetail', 'Essai gratuit — {days} jours restants sur 14. Toutes les fonctionnalités sont disponibles.').replace('{days}', daysLeft);
    } else if (TM && TM.isTrialExpired()) {
      // Trial expired
      statusDiv.style.background = 'var(--color-danger-bg,#fef2f2)';
      statusDiv.style.border = '1px solid var(--color-danger-light,#fca5a5)';
      statusIcon.textContent = '\u26A0\uFE0F';
      statusTitle.textContent = t('license.trialExpired', 'Période d\'essai expirée');
      statusTitle.style.color = 'var(--color-danger,#dc3545)';
      statusDetail.textContent = t('license.renewNeeded', 'Licence expirée — entrez une nouvelle clé pour réactiver');
    } else if (TM && TM.isDetected()) {
      // Detected but no active trial/license
      statusDiv.style.background = 'var(--color-surface-alt,#f9fafb)';
      statusDiv.style.border = '1px solid var(--color-border,#e5e7eb)';
      statusIcon.textContent = '\u2139\uFE0F';
      statusTitle.textContent = t('license.noLicense', 'Aucune licence active');
      statusDetail.textContent = t('license.noLicenseDetail', 'Activez une clé de licence ci-dessous.');
    } else {
      // TM null or not yet detected (should not happen after await fix)
      statusDiv.style.background = 'var(--color-surface-alt,#f9fafb)';
      statusDiv.style.border = '1px solid var(--color-border,#e5e7eb)';
      statusIcon.textContent = '\u2139\uFE0F';
      statusTitle.textContent = t('license.detecting', 'Détection de la licence...');
      statusDetail.textContent = '';
    }

    statusInfo.appendChild(statusTitle);
    statusInfo.appendChild(statusDetail);
    statusDiv.appendChild(statusIcon);
    statusDiv.appendChild(statusInfo);
    statusBody.appendChild(statusDiv);
    container.appendChild(statusCard);

    // --- Card 2: License Activation ---
    var { card: activateCard, body: activateBody } = createSettingsCard({
      title: t('settings.license.activateTitle', 'Activer une licence'),
      description: t('settings.license.activateDesc', 'Collez votre clé CSI2-... pour activer ou renouveler votre licence.'),
      icon: 'fa-key'
    });

    var keyTextarea = document.createElement('textarea');
    keyTextarea.className = 'csi-license-textarea';
    keyTextarea.placeholder = 'CSI2-...';
    keyTextarea.style.cssText = 'width:100%;min-height:80px;padding:12px;border:1px solid var(--color-border,#ddd);border-radius:var(--radius-md,6px);margin-bottom:var(--spacing-sm);font-family:monospace;font-size:0.85rem;resize:vertical;box-sizing:border-box;background:var(--color-surface,#fff);color:var(--color-text,#333);';

    var msgDiv = document.createElement('div');
    msgDiv.style.cssText = 'margin-bottom:var(--spacing-sm);font-size:0.9rem;display:none;padding:10px;border-radius:var(--radius-md,6px);';

    var btnRow = document.createElement('div');
    btnRow.className = 'settings-card__actions';

    var activateBtn = document.createElement('button');
    activateBtn.className = 'btn btn--primary btn--sm';
    activateBtn.textContent = t('license.activate', 'Activer la licence');

    function showActivateMsg(text, type) {
      msgDiv.style.display = 'block';
      msgDiv.textContent = text;
      if (type === 'error') {
        msgDiv.style.background = 'var(--color-danger-bg,#fef2f2)';
        msgDiv.style.color = 'var(--color-danger,#dc3545)';
      } else {
        msgDiv.style.background = 'var(--color-success-bg,#f0fdf4)';
        msgDiv.style.color = 'var(--color-success,#22c55e)';
      }
    }

    activateBtn.addEventListener('click', function() {
      var key = (keyTextarea.value || '').trim();
      if (!key || key.indexOf('CSI2-') !== 0) {
        showActivateMsg(t('license.error', 'Clé de licence invalide'), 'error');
        return;
      }
      activateBtn.disabled = true;
      activateBtn.textContent = '...';
      msgDiv.style.display = 'none';

      if (TM) {
        TM.activate(key).then(function(result) {
          showActivateMsg(t('license.success', 'Licence activée avec succès') + (result.organization ? ' — ' + result.organization : ''), 'success');
          setTimeout(function() {
            // Refresh the section to show the new status
            container.textContent = '';
            renderLicenseSection(container);
          }, 1500);
        }).catch(function(err) {
          activateBtn.disabled = false;
          activateBtn.textContent = t('license.activate', 'Activer la licence');
          showActivateMsg(err.message || t('license.error', 'Clé de licence invalide'), 'error');
        });
      } else {
        activateBtn.disabled = false;
        activateBtn.textContent = t('license.activate', 'Activer la licence');
        showActivateMsg('TrialManager non disponible', 'error');
      }
    });

    btnRow.appendChild(activateBtn);
    activateBody.appendChild(keyTextarea);
    activateBody.appendChild(msgDiv);
    activateBody.appendChild(btnRow);
    container.appendChild(activateCard);
  }

  // === SECTION: USERS ===
  function renderUsersSection(container) {
    var AM = window.CSI_AuthManager;
    if (!AM || !AM.isAdmin()) {
      container.textContent = t('users.noAccess', 'Accès réservé aux administrateurs.');
      return;
    }

    // --- Card 1: User list ---
    var { card: listCard, body: listBody } = createSettingsCard({
      title: t('users.title', 'Gestion des utilisateurs'),
      description: t('users.description', 'Créer, suspendre ou supprimer des comptes utilisateurs.'),
      icon: 'fa-users'
    });

    var listStatus = document.createElement('div');
    listStatus.className = 'users-status';
    listStatus.textContent = t('common.loading', 'Chargement...');
    listBody.appendChild(listStatus);

    // Recherche utilisateurs (utile dès quelques dizaines de comptes, jusqu'au plafond 100/200).
    // Filtrage 100 % client sur les lignes déjà rendues (nom, email, rôle, statut) — pas de
    // requête réseau par frappe, pas de restructuration du rendu de ligne existant.
    var _lastFullCountText = '';
    var usersSearchWrap = document.createElement('div');
    usersSearchWrap.className = 'users-search-wrap';
    usersSearchWrap.style.margin = 'var(--spacing-sm, 8px) 0';
    var usersSearchInput = document.createElement('input');
    usersSearchInput.type = 'search';
    usersSearchInput.className = 'form-control users-search';
    usersSearchInput.placeholder = t('users.searchPlaceholder', 'Rechercher (nom, email, rôle)…');
    usersSearchInput.setAttribute('aria-label', t('users.searchPlaceholder', 'Rechercher (nom, email, rôle)…'));
    usersSearchWrap.appendChild(usersSearchInput);
    listBody.appendChild(usersSearchWrap);

    var tableWrap = document.createElement('div');
    tableWrap.className = 'users-table-wrap';
    listBody.appendChild(tableWrap);

    function filterUserRows() {
      var q = (usersSearchInput.value || '').trim().toLowerCase();
      var rows = tableWrap.querySelectorAll('tbody tr');
      var shown = 0;
      rows.forEach(function(tr) {
        var match = !q || tr.textContent.toLowerCase().indexOf(q) >= 0;
        tr.style.display = match ? '' : 'none';
        if (match) shown++;
      });
      if (q) {
        listStatus.textContent = t('users.countFiltered', '{shown} / {count} utilisateurs (filtrés)')
          .replace('{shown}', shown).replace('{count}', rows.length);
      } else if (_lastFullCountText) {
        listStatus.textContent = _lastFullCountText;
      }
    }
    usersSearchInput.addEventListener('input', filterUserRows);

    function refreshUserList() {
      listStatus.textContent = t('common.loading', 'Chargement...');
      tableWrap.innerHTML = '';
      // FIX-MAXUSERS-STATUS (audit 2026-06-10) : la limite d'utilisateurs est annoncée par
      // le serveur (status.maxUsers, configurable 5-200 via SSO) — ne plus coder 5 en dur
      // pour l'affichage « N / max » (repli 5 si le statut est indisponible).
      Promise.all([
        AM.listUsers(),
        AM.getStatus().catch(function() { return null; })
      ]).then(function(_userListRes) {
        var users = _userListRes[0];
        var _srvStatus = _userListRes[1];
        if (!users || !users.length) {
          listStatus.textContent = t('users.noUsers', 'Aucun utilisateur trouvé.');
          return;
        }
        var maxUsers = (_srvStatus && typeof _srvStatus.maxUsers === 'number' && _srvStatus.maxUsers > 0) ? _srvStatus.maxUsers : 5;
        listStatus.textContent = t('users.count', '{count} / {max} utilisateurs')
          .replace('{count}', users.length).replace('{max}', maxUsers);
        _lastFullCountText = listStatus.textContent;

        var table = document.createElement('table');
        table.className = 'users-table';

        // Header
        var thead = document.createElement('thead');
        var hRow = document.createElement('tr');
        [t('users.name', 'Nom'), t('users.role', 'Rôle'), t('users.status', 'Statut'), t('users.actions', 'Actions')].forEach(function(h) {
          var th = document.createElement('th');
          th.textContent = h;
          hRow.appendChild(th);
        });
        thead.appendChild(hRow);
        table.appendChild(thead);

        var tbody = document.createElement('tbody');
        var currentUser = AM.getCurrentUser();

        users.forEach(function(user) {
          var tr = document.createElement('tr');
          tr.className = 'users-table__row';
          if (user.suspended) tr.classList.add('users-table__row--suspended');

          // Name
          var tdName = document.createElement('td');
          tdName.textContent = user.name;
          // FIX-SSO-EMAIL-LINK (audit 2026-06-10, complément client) : affichage de
          // l'email du compte (liaison SSO par email) s'il existe.
          if (user.email) {
            var emailLine = document.createElement('div');
            emailLine.style.cssText = 'font-size:0.78em;color:var(--color-text-secondary,#888);';
            emailLine.textContent = user.email;
            tdName.appendChild(emailLine);
          }
          tr.appendChild(tdName);

          // Role
          var tdRole = document.createElement('td');
          var roleBadge = document.createElement('span');
          roleBadge.className = 'users-badge users-badge--' + user.role;
          var roleLabels = {
            admin: t('users.roleAdmin', 'Admin'),
            editor: t('users.roleEditor', 'Éditeur'),
            viewer: t('users.roleViewer', 'Lecteur')
          };
          roleBadge.textContent = roleLabels[user.role] || user.role;
          tdRole.appendChild(roleBadge);
          tr.appendChild(tdRole);

          // Status
          var tdStatus = document.createElement('td');
          var statusBadge = document.createElement('span');
          statusBadge.className = user.suspended ? 'users-badge users-badge--suspended' : 'users-badge users-badge--active';
          statusBadge.textContent = user.suspended ? t('users.suspended', 'Suspendu') : t('users.active', 'Actif');
          tdStatus.appendChild(statusBadge);
          if (user.mustChangePassword) {
            var tmpBadge = document.createElement('span');
            tmpBadge.className = 'users-badge users-badge--temp';
            tmpBadge.textContent = t('users.tempPassword', 'MdP temp.');
            tdStatus.appendChild(tmpBadge);
          }
          if (user.sso) {
            var ssoBadge = document.createElement('span');
            ssoBadge.className = 'users-badge users-badge--sso';
            ssoBadge.textContent = 'SSO';
            tdStatus.appendChild(ssoBadge);
          }
          tr.appendChild(tdStatus);

          // Actions
          var tdActions = document.createElement('td');
          tdActions.className = 'users-actions';

          var isSelf = currentUser && currentUser.id === user.id;

          // FIX-SSO-EMAIL-LINK (audit 2026-06-10, complément client) : modale de
          // modification de l'email (update-user accepte 'email', '' = suppression).
          // Masqué pour les comptes SSO (email géré par le fournisseur d'identité).
          if (!user.sso) {
            var btnEmail = document.createElement('button');
            btnEmail.className = 'btn btn--xs btn--outline';
            btnEmail.textContent = t('users.editEmail', 'Email');
            btnEmail.title = t('users.editEmailTitle', 'Modifier l\'email (liaison SSO)');
            btnEmail.addEventListener('click', function() {
              showEditEmailModal(user);
            });
            tdActions.appendChild(btnEmail);
          }

          // === CSI-ME === (audit M8) réassignation d'entité(s) d'un compte EXISTANT.
          // Mode multi-entités actif, cible non-admin (V12 : un admin n'a jamais d'entityIds).
          var _emUsers = window.CSI_EntityManager;
          if (_emUsers && _emUsers.isMultiEntity && _emUsers.isMultiEntity() && user.role !== 'admin') {
            var btnEnt = document.createElement('button');
            btnEnt.className = 'btn btn--xs btn--outline';
            btnEnt.textContent = t('users.editEntities', 'Entité(s)');
            btnEnt.title = t('users.editEntitiesTitle', 'Modifier le rattachement aux sous-entités');
            btnEnt.addEventListener('click', function() { showEditEntitiesModal(user, refreshUserList); });
            tdActions.appendChild(btnEnt);
          }

          if (!isSelf) {
            // Suspend/Reactivate
            var btnSuspend = document.createElement('button');
            btnSuspend.className = 'btn btn--xs btn--outline';
            btnSuspend.textContent = user.suspended ? t('users.reactivate', 'Réactiver') : t('users.suspend', 'Suspendre');
            btnSuspend.addEventListener('click', function() {
              var msg = user.suspended
                ? t('users.confirmReactivate', 'Réactiver cet utilisateur ?')
                : t('users.confirmSuspend', 'Suspendre cet utilisateur ?');
              showConfirmModal(
                user.suspended ? t('users.reactivateTitle', 'Réactiver') : t('users.suspendTitle', 'Suspendre'),
                msg,
                function() {
                  AM.suspendUser(user.id).then(function(res) {
                    if (!res.ok) throw new Error(res.data && res.data.error || t('common.error', 'Erreur'));
                    showToast(user.suspended ? t('users.reactivateSuccess', 'Utilisateur réactivé') : t('users.suspendSuccess', 'Utilisateur suspendu'), 'success');
                    refreshUserList();
                  }).catch(function(e) { showToast(e.message || t('common.error', 'Erreur'), 'error'); });
                },
                { type: 'warning' }
              );
            });
            tdActions.appendChild(btnSuspend);

            // Reset password (hidden for SSO users)
            if (!user.sso) {
              var btnReset = document.createElement('button');
              btnReset.className = 'btn btn--xs btn--outline';
              btnReset.textContent = t('users.resetPassword', 'Réinit. MdP');
              btnReset.addEventListener('click', function() {
                showResetPasswordModal(user);
              });
              tdActions.appendChild(btnReset);
            }

            // Role toggle button (editor ↔ viewer, not for admins)
            if (user.role !== 'admin') {
              var btnRole = document.createElement('button');
              btnRole.className = 'btn btn--xs btn--outline';
              var newRole = user.role === 'editor' ? 'viewer' : 'editor';
              var newRoleLabel = newRole === 'editor'
                ? t('users.roleEditor', 'Éditeur')
                : t('users.roleViewer', 'Lecteur');
              btnRole.textContent = '→ ' + newRoleLabel;
              btnRole.title = t('users.changeRoleTitle', 'Changer le rôle');
              btnRole.addEventListener('click', function() {
                showConfirmModal(
                  t('users.changeRoleTitle', 'Changer le rôle'),
                  t('users.changeRoleConfirm', 'Changer le rôle de {name} en {role} ?')
                    .replace('{name}', user.name).replace('{role}', newRoleLabel),
                  function() {
                    AM.updateUser(user.id, user.name, newRole).then(function(res) {
                      if (!res.ok) throw new Error(res.data && res.data.error || 'Erreur');
                      showToast(t('users.roleChangeSuccess', 'Rôle modifié'), 'success');
                      refreshUserList();
                    }).catch(function(e) { showToast(e.message, 'error'); });
                  },
                  { type: 'warning' }
                );
              });
              tdActions.appendChild(btnRole);
            }

            // Delete button removed — admin can only suspend accounts
          } else {
            var selfLabel = document.createElement('span');
            selfLabel.className = 'users-self-label';
            selfLabel.textContent = t('users.you', '(vous)');
            tdActions.appendChild(selfLabel);
          }

          tr.appendChild(tdActions);
          tbody.appendChild(tr);
        });

        table.appendChild(tbody);
        tableWrap.appendChild(table);
        // Réappliquer un filtre de recherche actif après reconstruction du tableau.
        if (usersSearchInput.value) filterUserRows();
      }).catch(function(e) {
        listStatus.textContent = t('users.loadError', 'Impossible de charger les utilisateurs.');
        void 0;
      });
    }

    refreshUserList();
    container.appendChild(listCard);

    // --- Card 2: Create user ---
    var { card: createCard, body: createBody } = createSettingsCard({
      title: t('users.create', 'Créer un utilisateur'),
      description: t('users.createDesc', 'Ajoutez un nouveau compte utilisateur.'),
      icon: 'fa-user-plus'
    });

    var createForm = document.createElement('div');
    createForm.className = 'settings-form-grid settings-form-grid--single-col';

    var { wrap: nameWrap, input: nameInput } = createSettingsInputGroup({
      id: 'createUserName',
      label: t('users.name', 'Nom'),
      value: '',
      placeholder: t('adminUi.userPlaceholder', 'utilisateur'),
      readonly: false
    });

    // FIX-SSO-EMAIL-LINK (audit 2026-06-10, complément client) : email optionnel à la
    // création — permet la liaison « match by email » du SSO entreprise (users.php
    // valide filter_var + unicité côté serveur).
    var { wrap: emailWrap, input: emailInput } = createSettingsInputGroup({
      id: 'createUserEmail',
      label: t('users.emailOptional', 'Email (optionnel)'),
      value: '',
      type: 'email',
      placeholder: t('users.emailPlaceholder', 'utilisateur@exemple.com'),
      help: t('users.emailHelp', 'Permet la liaison du compte avec le SSO entreprise.'),
      readonly: false
    });

    var { wrap: roleWrap, select: roleSelect } = createSettingsSelectGroup({
      id: 'createUserRole',
      label: t('users.roleLabel', 'Rôle'),
      value: 'editor',
      options: [
        { value: 'editor', label: t('users.roleEditor', 'Éditeur') },
        { value: 'viewer', label: t('users.roleViewer', 'Lecteur') }
      ],
      help: t('users.roleHelp', 'Éditeur = peut modifier les données. Lecteur = lecture seule.')
    });
    // Remove data-readonly-lock since this is an admin-only form
    delete roleSelect.dataset.readonlyLock;

    // === CSI-ME === sélecteur d'entité (mode multi-entités actif) : « Entité
    // principale » ou une/plusieurs sous-entités (Lot 3c : multi-appartenance —
    // le compte bascule ensuite parmi SES entités). Passé à createUser en entityIds.
    var entitySelect = null;
    var EMcu = window.CSI_EntityManager;
    if (EMcu && EMcu.isMultiEntity()) {
      var _entOpts = [{ value: '', label: t('users.entityMain', 'Entité principale') }];
      EMcu.entities().forEach(function(e) { if (e && !e.archived && !e.disabled) _entOpts.push({ value: e.id, label: e.name || e.id }); });
      var entGroup = createSettingsSelectGroup({ id: 'createUserEntity', label: t('users.entityLabel', 'Entité(s)'), value: '', options: _entOpts, help: t('users.entityHelpMulti', 'Une sous-entité ne voit que ses propres encodages. Sélection multiple possible (Ctrl/Cmd) — « Entité principale » ne se combine pas.') });
      var entityWrap = entGroup.wrap; entitySelect = entGroup.select;
      delete entitySelect.dataset.readonlyLock;
      entitySelect.multiple = true;
      entitySelect.size = Math.min(4, _entOpts.length);
      // Exclusivité : « Entité principale » (valeur vide) ne se combine pas avec des
      // sous-entités — on désélectionne l'autre camp au changement.
      entitySelect.addEventListener('change', function() {
        var opts = entitySelect.options, mainSel = false, subSel = false, k;
        for (k = 0; k < opts.length; k++) { if (opts[k].selected) { if (opts[k].value === '') mainSel = true; else subSel = true; } }
        if (mainSel && subSel) {
          // Le dernier geste gagne : si la principale vient d'être cliquée, on garde elle seule.
          var keepMain = (document.activeElement === entitySelect && entitySelect.value === '');
          for (k = 0; k < opts.length; k++) {
            if (opts[k].value === '') opts[k].selected = keepMain;
            else if (keepMain) opts[k].selected = false;
          }
        }
      });
    }

    var { wrap: pwdWrap, input: pwdInput } = createSettingsPasswordGroup({
      id: 'createUserPwd',
      label: t('users.password', 'Mot de passe'),
      value: '',
      placeholder: t('users.pwdMinPlaceholder', '18 caractères min.'),
      help: t('users.passwordHelp', 'Minimum 18 caractères, 1 majuscule, 1 minuscule, 1 caractère spécial.')
    });

    var { wrap: pwd2Wrap, input: pwd2Input } = createSettingsPasswordGroup({
      id: 'createUserPwd2',
      label: t('users.confirmPassword', 'Confirmer'),
      value: '',
      placeholder: t('users.confirmPwdPlaceholder', 'Confirmer le mot de passe')
    });
    // FIX-USERS-PWD-LOCK (audit 2026-06-10) : la gestion des utilisateurs est une action
    // d'ADMINISTRATION, pas une édition de données — retirer le verrou lecture seule posé
    // par createSettingsPasswordGroup (même traitement que le select de rôle ci-dessus),
    // sinon les champs mot de passe restent grisés hors mode édition.
    delete pwdInput.dataset.readonlyLock;
    delete pwd2Input.dataset.readonlyLock;

    createForm.append(nameWrap, emailWrap, roleWrap, pwdWrap, pwd2Wrap);
    if (entitySelect && typeof entityWrap !== 'undefined') createForm.appendChild(entityWrap); // === CSI-ME ===
    createBody.appendChild(createForm);

    var createMsgDiv = document.createElement('div');
    createMsgDiv.className = 'users-create-msg';
    createBody.appendChild(createMsgDiv);

    var createActions = document.createElement('div');
    createActions.className = 'settings-card__actions';
    var btnCreate = document.createElement('button');
    btnCreate.className = 'btn btn--primary btn--sm';
    btnCreate.textContent = t('users.createBtn', 'Créer le compte');
    btnCreate.addEventListener('click', function() {
      var name = (nameInput.value || '').trim();
      var email = (emailInput.value || '').trim();
      var pwd = pwdInput.value || '';
      var pwd2 = pwd2Input.value || '';

      createMsgDiv.textContent = '';
      createMsgDiv.className = 'users-create-msg';

      if (!name || name.length < 2) {
        createMsgDiv.textContent = t('users.errorNameShort', 'Le nom doit contenir au moins 2 caractères.');
        createMsgDiv.classList.add('users-create-msg--error');
        return;
      }
      if (pwd.length < 18) {
        createMsgDiv.textContent = t('users.errorPwdShort', 'Le mot de passe doit contenir au moins 18 caractères.');
        createMsgDiv.classList.add('users-create-msg--error');
        return;
      }
      if (!/[A-Z]/.test(pwd) || !/[a-z]/.test(pwd) || !/[^a-zA-Z0-9]/.test(pwd)) {
        createMsgDiv.textContent = t('users.errorPwdWeak', 'Le mot de passe doit contenir majuscule, minuscule et caractère spécial.');
        createMsgDiv.classList.add('users-create-msg--error');
        return;
      }
      if (pwd !== pwd2) {
        createMsgDiv.textContent = t('users.errorPwdMismatch', 'Les mots de passe ne correspondent pas.');
        createMsgDiv.classList.add('users-create-msg--error');
        return;
      }

      btnCreate.disabled = true;
      btnCreate.textContent = '...';
      // FIX-SSO-EMAIL-LINK : transmission de l'email optionnel ; l'erreur serveur
      // (email invalide / déjà utilisé) est affichée par le catch ci-dessous.
      // === CSI-ME === assignation d'entité si le sélecteur est présent.
      // === CSI-ME === (Lot 3c) collecter TOUTES les sous-entités sélectionnées
      // (multi-appartenance) ; aucune sélection ou « Entité principale » = compte principal.
      var _entIdsCu = [];
      if (entitySelect) {
        for (var _ei = 0; _ei < entitySelect.options.length; _ei++) {
          var _eo = entitySelect.options[_ei];
          if (_eo.selected && _eo.value) _entIdsCu.push(_eo.value);
        }
      }
      var _entOptsCu = _entIdsCu.length ? { entityIds: _entIdsCu } : undefined;
      AM.createUser(name, pwd, roleSelect.value, email, _entOptsCu).then(function(res) {
        if (!res.ok) throw new Error(res.data && res.data.error || t('common.error', 'Erreur'));
        showToast(t('users.createSuccess', 'Utilisateur créé'), 'success');
        nameInput.value = '';
        emailInput.value = '';
        pwdInput.value = '';
        pwd2Input.value = '';
        createMsgDiv.textContent = '';
        refreshUserList();
      }).catch(function(e) {
        createMsgDiv.textContent = e.message || t('common.error', 'Erreur');
        createMsgDiv.classList.add('users-create-msg--error');
      }).finally(function() {
        btnCreate.disabled = false;
        btnCreate.textContent = t('users.createBtn', 'Créer le compte');
      });
    });
    createActions.appendChild(btnCreate);
    createBody.appendChild(createActions);

    container.appendChild(createCard);

    // --- Reset password modal helper ---
    function showResetPasswordModal(user) {
      var overlay = document.createElement('div');
      overlay.className = 'csi-modal-overlay';
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:99999;';

      var modal = document.createElement('div');
      modal.style.cssText = 'background:var(--color-surface,#fff);border-radius:12px;padding:32px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.3);';

      var title = document.createElement('h3');
      title.style.cssText = 'margin:0 0 16px 0;color:var(--color-text,#333);';
      title.textContent = t('users.resetPasswordFor', 'Réinitialiser le mot de passe de') + ' ' + user.name;
      modal.appendChild(title);

      var pwdField = document.createElement('input');
      pwdField.type = 'password';
      pwdField.className = 'form-control';
      pwdField.placeholder = t('users.newPassword', 'Nouveau mot de passe (18+ car.)');
      pwdField.style.cssText = 'width:100%;margin-bottom:12px;box-sizing:border-box;';
      modal.appendChild(pwdField);

      var pwd2Field = document.createElement('input');
      pwd2Field.type = 'password';
      pwd2Field.className = 'form-control';
      pwd2Field.placeholder = t('users.confirmPassword', 'Confirmer');
      pwd2Field.style.cssText = 'width:100%;margin-bottom:16px;box-sizing:border-box;';
      modal.appendChild(pwd2Field);

      var msgDiv = document.createElement('div');
      msgDiv.className = 'users-create-msg';
      modal.appendChild(msgDiv);

      var btnRow = document.createElement('div');
      btnRow.style.cssText = 'display:flex;gap:12px;justify-content:flex-end;margin-top:12px;';

      var btnCancel = document.createElement('button');
      btnCancel.className = 'btn btn--outline btn--sm';
      btnCancel.textContent = t('common.cancel', 'Annuler');
      btnCancel.addEventListener('click', function() { overlay.remove(); });

      var btnConfirm = document.createElement('button');
      btnConfirm.className = 'btn btn--primary btn--sm';
      btnConfirm.textContent = t('users.resetPassword', 'Réinit. MdP');
      btnConfirm.addEventListener('click', function() {
        var pwd = pwdField.value || '';
        var pwd2 = pwd2Field.value || '';
        msgDiv.textContent = '';
        msgDiv.className = 'users-create-msg';

        if (pwd.length < 18) {
          msgDiv.textContent = t('users.errorPwdShort', 'Le mot de passe doit contenir au moins 18 caractères.');
          msgDiv.classList.add('users-create-msg--error');
          return;
        }
        if (pwd !== pwd2) {
          msgDiv.textContent = t('users.errorPwdMismatch', 'Les mots de passe ne correspondent pas.');
          msgDiv.classList.add('users-create-msg--error');
          return;
        }
        btnConfirm.disabled = true;
        btnConfirm.textContent = '...';
        AM.resetPassword(user.id, pwd).then(function(res) {
          if (!res.ok) throw new Error(res.data && res.data.error || t('common.error', 'Erreur'));
          showToast(t('users.resetSuccess', 'Mot de passe réinitialisé'), 'success');
          overlay.remove();
        }).catch(function(e) {
          msgDiv.textContent = e.message || t('common.error', 'Erreur');
          msgDiv.classList.add('users-create-msg--error');
          btnConfirm.disabled = false;
          btnConfirm.textContent = t('users.resetPassword', 'Réinit. MdP');
        });
      });

      btnRow.append(btnCancel, btnConfirm);
      modal.appendChild(btnRow);
      overlay.appendChild(modal);
      document.body.appendChild(overlay);
      setTimeout(function() { pwdField.focus(); }, 100);
    }

    // --- Edit email modal helper ---
    // FIX-SSO-EMAIL-LINK (audit 2026-06-10, complément client) : modification de l'email
    // d'un compte (update-user) — chaîne vide = suppression. La validation (filter_var)
    // et l'unicité sont faites côté serveur ; l'erreur serveur est affichée telle quelle.
    function showEditEmailModal(user) {
      var overlay = document.createElement('div');
      overlay.className = 'csi-modal-overlay';
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:99999;';

      var modal = document.createElement('div');
      modal.style.cssText = 'background:var(--color-surface,#fff);border-radius:12px;padding:32px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.3);';

      var title = document.createElement('h3');
      title.style.cssText = 'margin:0 0 16px 0;color:var(--color-text,#333);';
      title.textContent = t('users.editEmailTitle', 'Modifier l\'email (liaison SSO)') + ' — ' + user.name;
      modal.appendChild(title);

      var emailField = document.createElement('input');
      emailField.type = 'email';
      emailField.className = 'form-control';
      emailField.value = user.email || '';
      emailField.placeholder = t('users.emailPlaceholder', 'utilisateur@exemple.com');
      emailField.style.cssText = 'width:100%;margin-bottom:8px;box-sizing:border-box;';
      modal.appendChild(emailField);

      var helpP = document.createElement('p');
      helpP.style.cssText = 'margin:0 0 12px;font-size:0.8rem;color:var(--color-text-secondary,#888);';
      helpP.textContent = t('users.emailClearHint', 'Laissez vide puis enregistrez pour supprimer l\'email.');
      modal.appendChild(helpP);

      var msgDiv = document.createElement('div');
      msgDiv.className = 'users-create-msg';
      modal.appendChild(msgDiv);

      var btnRow = document.createElement('div');
      btnRow.style.cssText = 'display:flex;gap:12px;justify-content:flex-end;margin-top:12px;';

      var btnCancel = document.createElement('button');
      btnCancel.className = 'btn btn--outline btn--sm';
      btnCancel.textContent = t('common.cancel', 'Annuler');
      btnCancel.addEventListener('click', function() { overlay.remove(); });

      var btnConfirm = document.createElement('button');
      btnConfirm.className = 'btn btn--primary btn--sm';
      btnConfirm.textContent = t('common.save', 'Enregistrer');
      btnConfirm.addEventListener('click', function() {
        var email = (emailField.value || '').trim();
        msgDiv.textContent = '';
        msgDiv.className = 'users-create-msg';
        btnConfirm.disabled = true;
        btnConfirm.textContent = '...';
        // null pour name/role : mise à jour de l'email uniquement
        AM.updateUser(user.id, null, null, email).then(function(res) {
          if (!res.ok) throw new Error(res.data && res.data.error || t('common.error', 'Erreur'));
          showToast(t('users.emailUpdateSuccess', 'Email mis à jour'), 'success');
          overlay.remove();
          refreshUserList();
        }).catch(function(e) {
          msgDiv.textContent = e.message || t('common.error', 'Erreur');
          msgDiv.classList.add('users-create-msg--error');
          btnConfirm.disabled = false;
          btnConfirm.textContent = t('common.save', 'Enregistrer');
        });
      });

      btnRow.append(btnCancel, btnConfirm);
      modal.appendChild(btnRow);
      overlay.appendChild(modal);
      document.body.appendChild(overlay);
      setTimeout(function() { emailField.focus(); }, 100);
    }

    // === CSI-ME === (audit M8) Modale de réassignation des sous-entités d'un compte.
    function showEditEntitiesModal(user, onDone) {
      var EM = window.CSI_EntityManager;
      var overlay = document.createElement('div');
      overlay.className = 'csi-modal-overlay';
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:99999;';
      var modal = document.createElement('div');
      modal.style.cssText = 'background:var(--color-surface,#fff);border-radius:12px;padding:32px;max-width:460px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.3);';
      var title = document.createElement('h3');
      title.style.cssText = 'margin:0 0 16px 0;color:var(--color-text);';
      title.textContent = t('users.editEntitiesTitle', 'Modifier le rattachement aux sous-entités') + ' — ' + user.name;
      modal.appendChild(title);

      var current = (user.entityIds && user.entityIds.length) ? user.entityIds : [];
      var sel = document.createElement('select');
      sel.multiple = true;
      sel.className = 'form-control';
      sel.size = 5;
      var optMain = document.createElement('option');
      optMain.value = '';
      optMain.textContent = t('users.entityMain', 'Entité principale');
      optMain.selected = (current.length === 0);
      sel.appendChild(optMain);
      (EM && EM.entities ? EM.entities() : []).forEach(function(e) {
        if (!e || e.archived) return;
        var o = document.createElement('option');
        o.value = e.id; o.textContent = e.name || e.id;
        o.selected = current.indexOf(e.id) >= 0;
        sel.appendChild(o);
      });
      // Exclusivité principal ↔ sous-entités.
      sel.addEventListener('change', function() {
        var opts = sel.options, mainSel = false, subSel = false, k;
        for (k = 0; k < opts.length; k++) { if (opts[k].selected) { if (opts[k].value === '') mainSel = true; else subSel = true; } }
        if (mainSel && subSel) {
          var keepMain = (document.activeElement === sel && sel.value === '');
          for (k = 0; k < opts.length; k++) {
            if (opts[k].value === '') opts[k].selected = keepMain;
            else if (keepMain) opts[k].selected = false;
          }
        }
      });
      modal.appendChild(sel);

      var help = document.createElement('p');
      help.style.cssText = 'margin:8px 0 12px;font-size:0.8rem;color:var(--color-text-secondary);';
      help.textContent = t('users.editEntitiesHint', 'Sélection multiple possible (Ctrl/Cmd). « Entité principale » = aucun rattachement. Retirer la dernière sous-entité suspend le compte (fail-closed).');
      modal.appendChild(help);

      var msgDiv = document.createElement('div');
      msgDiv.className = 'users-create-msg';
      modal.appendChild(msgDiv);

      var btnRow = document.createElement('div');
      btnRow.style.cssText = 'display:flex;gap:12px;justify-content:flex-end;margin-top:12px;';
      var btnCancel = document.createElement('button');
      btnCancel.className = 'btn btn--outline btn--sm';
      btnCancel.textContent = t('common.cancel', 'Annuler');
      btnCancel.addEventListener('click', function() { overlay.remove(); });
      var btnSave = document.createElement('button');
      btnSave.className = 'btn btn--primary btn--sm';
      btnSave.textContent = t('common.save', 'Enregistrer');
      btnSave.addEventListener('click', function() {
        var ids = [];
        for (var i = 0; i < sel.options.length; i++) { if (sel.options[i].selected && sel.options[i].value) ids.push(sel.options[i].value); }
        // Détachement de la dernière sous-entité (→ périmètre principal) : le serveur
        // exige detachConfirm. Le client sait le détecter (ids vide + compte assigné)
        // et confirme en amont — pas de dépendance à un code d'erreur serveur.
        var isDetach = (ids.length === 0 && current.length > 0);
        if (isDetach && !window.confirm(t('users.lastEntityDetach', 'Ce compte n\'aura plus aucune sous-entité et obtiendrait un accès au périmètre principal. Confirmer ?'))) {
          return;
        }
        msgDiv.textContent = ''; msgDiv.className = 'users-create-msg';
        btnSave.disabled = true; btnSave.textContent = '...';
        // email=undefined → inchangé ; entOpts porte entityIds (+ detachConfirm au besoin).
        AM.updateUser(user.id, null, null, undefined, { entityIds: ids, detachConfirm: isDetach }).then(function(res) {
          if (!res.ok) throw new Error(res.data && res.data.error || t('common.error', 'Erreur'));
          showToast(t('users.entitiesUpdateSuccess', 'Rattachement mis à jour'), 'success');
          overlay.remove();
          if (typeof onDone === 'function') onDone();
        }).catch(function(e) {
          msgDiv.textContent = e.message || t('common.error', 'Erreur');
          msgDiv.classList.add('users-create-msg--error');
          btnSave.disabled = false; btnSave.textContent = t('common.save', 'Enregistrer');
        });
      });
      btnRow.append(btnCancel, btnSave);
      modal.appendChild(btnRow);
      overlay.appendChild(modal);
      document.body.appendChild(overlay);
    }
  }

  // === SECTION: ADVANCED ===
  // === SECTION: FRAMEWORKS ===
  function renderFrameworksSection(container) {
    var mapper = window.CSI_FrameworkMapper;
    if (!mapper) {
      var msg = document.createElement('p');
      msg.textContent = t('mapping.notAvailable', 'Module de mapping non disponible.');
      container.appendChild(msg);
      return;
    }

    var frameworks = mapper.listFrameworks();

    frameworks.forEach(function(fw) {
      var fwData = mapper.getFramework(fw.id);
      var isCore = !!(fwData && fwData.meta && fwData.meta.mandatory);
      // En Community, listFrameworks renvoie le canonique (iso27002/iso27001) car
      // open-security-controls/management en sont des alias (meta.aliasOf). Pour l'AFFICHAGE,
      // préférer le framework réellement utilisé par l'édition courante (« Open Security… »).
      // fw.id reste le canonique pour la logique (mappings, toggles, stats).
      var _dispId = fw.id;
      if (!_isFrameworkAllowed(fw.id)) {
        var _fwsAll = window._csiFrameworks || {};
        for (var _aid in _fwsAll) {
          if (_fwsAll.hasOwnProperty(_aid) && _fwsAll[_aid].meta &&
              _fwsAll[_aid].meta.aliasOf === fw.id && _isFrameworkAllowed(_aid)) { _dispId = _aid; break; }
        }
      }
      var meta = mapper.getFramework(_dispId) || fwData;
      var _dispName = (meta && meta.meta && meta.meta.name) ? meta.meta.name : fw.name;
      var controlCount = (meta && meta.meta ? (meta.meta.controlCount || meta.meta.clauseCount) : (fw.controlCount || fw.clauseCount)) || 0;
      var iconClass = (meta && meta.meta ? meta.meta.icon : 'fa-shield-halved') || 'fa-shield-halved';
      var cardOpts = {
        title: _dispName,
        description: controlCount + ' ' + t('mapping.settings.controls', 'contrôles'),
        icon: iconClass
      };
      var result = createSettingsCard(cardOpts);
      var body = result.body;

      // Toggle or "always active" label
      var toggleWrap = document.createElement('div');
      toggleWrap.style.cssText = 'display:flex;align-items:center;gap:12px;margin-top:8px;';

      if (isCore) {
        var badge = document.createElement('span');
        badge.className = 'badge badge--success';
        var lockIcon = document.createElement('i');
        lockIcon.className = 'fas fa-lock';
        badge.appendChild(lockIcon);
        badge.appendChild(document.createTextNode(' ' + t('mapping.settings.alwaysActive', 'Toujours actif')));
        toggleWrap.appendChild(badge);
      } else if (fw.id === 'cyfun2025' || fw.id === 'recyf2025') {
        // Référentiels NIS2 (Belgique/CyFun & France/ReCyF) : mutuellement exclusifs. Pas de
        // bascule activé/désactivé indépendante (c'était la source de l'incohérence). L'unique
        // commande est le sélecteur « Référentiel NIS2 actif » ci-dessous. On affiche ici le
        // statut actif/inactif, dérivé de appData.nis2Referential.
        var _isActiveRef = (typeof appData !== 'undefined' && (appData.nis2Referential || 'cyfun2025') === fw.id);
        var refBadge = document.createElement('span');
        var rbIcon = document.createElement('i');
        if (_isActiveRef) {
          refBadge.className = 'badge badge--success';
          rbIcon.className = 'fas fa-circle-check';
          refBadge.appendChild(rbIcon);
          refBadge.appendChild(document.createTextNode(' ' + t('settings.nis2RefActive', 'Référentiel NIS2 actif')));
        } else {
          refBadge.className = 'badge';
          refBadge.style.cssText = 'background:var(--color-surface-alt,#eceff3);color:var(--text-secondary,#6b7280);';
          rbIcon.className = 'far fa-circle';
          refBadge.appendChild(rbIcon);
          refBadge.appendChild(document.createTextNode(' ' + t('settings.nis2RefInactive', 'Référentiel NIS2 — non sélectionné')));
        }
        toggleWrap.appendChild(refBadge);
      } else {
        var toggleLabel = document.createElement('label');
        toggleLabel.className = 'toggle';
        toggleLabel.style.marginBottom = '0';
        var input = document.createElement('input');
        input.type = 'checkbox';
        input.checked = fw.enabled;
        input.addEventListener('change', (function(fwId) {
          return function(e) {
            mapper.setEnabled(fwId, e.target.checked);
            if (e.target.checked && typeof initFrameworkData === 'function') initFrameworkData(fwId);
            mapper.applyFrameworkVisibility();
            if (typeof updateDashboard === 'function') updateDashboard();
            if (typeof saveData === 'function') saveData();
            showToast2(t('settings.saved', 'Paramètres sauvegardés'), 'success');
          };
        })(fw.id));
        var slider = document.createElement('span');
        slider.className = 'toggle__slider';
        toggleLabel.appendChild(input);
        toggleLabel.appendChild(slider);
        toggleWrap.appendChild(toggleLabel);

        var toggleText = document.createElement('span');
        toggleText.textContent = fw.enabled ? t('common.enabled', 'Activé') : t('common.disabled', 'Désactivé');
        toggleWrap.appendChild(toggleText);
      }

      body.appendChild(toggleWrap);

      // Mapping stats
      var enabledFws = mapper.listEnabledFrameworks();
      var statsDiv = document.createElement('div');
      statsDiv.style.cssText = 'margin-top:12px;font-size:0.85rem;color:var(--text-secondary);';
      var mappingCount = 0;
      enabledFws.forEach(function(other) {
        if (other.id !== fw.id) {
          var stats = mapper.getCoverageStats(fw.id, other.id);
          if (stats) mappingCount += stats.totalMappings;
        }
      });
      if (mappingCount > 0) {
        var linkIcon = document.createElement('i');
        linkIcon.className = 'fas fa-link';
        linkIcon.style.marginRight = '4px';
        statsDiv.appendChild(linkIcon);
        statsDiv.appendChild(document.createTextNode(mappingCount + ' ' + t('mapping.settings.mappingsLoaded', 'correspondances chargées')));
      }
      body.appendChild(statsDiv);

      // NIS2/CyFun assurance level selector — carte cyfun2025, et seulement quand la Belgique
      // est le référentiel NIS2 actif (sinon ce réglage de niveau n'a pas lieu d'être affiché).
      if (fw.id === 'cyfun2025' && (appData.nis2Referential || 'cyfun2025') === 'cyfun2025') {
        var levelWrap = document.createElement('div');
        levelWrap.className = 'form-group';
        levelWrap.id = 'cyfunLevelSetting';
        levelWrap.style.cssText = 'margin-top:16px;';

        var levelLabel = document.createElement('label');
        levelLabel.setAttribute('for', 'cyfunLevelSelect');
        levelLabel.textContent = t('settings.cyfunLevel', 'Niveau d\'assurance NIS2');
        levelWrap.appendChild(levelLabel);

        var levelSelect = document.createElement('select');
        levelSelect.id = 'cyfunLevelSelect';
        levelSelect.className = 'form-control';
        [
          { value: 'basic', text: 'BASIC (34 ' + t('common.requirements', 'exigences') + ')' },
          { value: 'important', text: 'IMPORTANT (133 ' + t('common.requirements', 'exigences') + ')' },
          { value: 'essential', text: 'ESSENTIAL (218 ' + t('common.requirements', 'exigences') + ')' }
        ].forEach(function(opt) {
          var option = document.createElement('option');
          option.value = opt.value;
          option.textContent = opt.text;
          levelSelect.appendChild(option);
        });
        levelSelect.value = (typeof appData !== 'undefined' && appData.cyfunLevel) || 'basic';
        // Verrou « lecture seule » : refreshReadOnlyLocks() désactive ce sélecteur pour un
        // utilisateur non-éditeur (viewer en mode serveur). Le verrou org-wide ci-dessous le
        // rend définitif (permanentReadonly) même pour un éditeur.
        levelSelect.dataset.readonlyLock = 'true';

        // FIX-CYFUN-LEVEL-LOCK: si _cyfunLevelLocked === true dans server-data.json,
        // le niveau est verrouillé (configuration définitive). Le sélecteur devient
        // read-only avec un message explicatif. Évite tout downgrade accidentel et le
        // data-loss associé. Pour déverrouiller : éditer server-data.json côté serveur.
        var _isCyfunLocked = (typeof appData !== 'undefined' && appData._cyfunLevelLocked === true);
        if (_isCyfunLocked) {
          levelSelect.disabled = true;
          // permanentReadonly : reste désactivé même hors mode lecture seule (verrou définitif).
          levelSelect.dataset.permanentReadonly = 'true';
          levelSelect.title = t('settings.cyfunLevelLocked', 'Niveau verrouillé pour cette organisation. Pour déverrouiller, contactez l\'administrateur.');
          var lockNote = document.createElement('div');
          lockNote.style.cssText = 'margin-top:6px;font-size:12px;color:#888;font-style:italic;';
          lockNote.textContent = '🔒 ' + t('settings.cyfunLevelLockedNote',
            'Niveau verrouillé pour cette organisation. Modification désactivée.');
          levelWrap.appendChild(lockNote);
        }

        levelSelect.addEventListener('change', function() {
          // FIX-CYFUN-LEVEL-LOCK: garde-fou côté event handler (au cas où le disabled
          // serait contourné via DevTools ou autre)
          if (typeof appData !== 'undefined' && appData._cyfunLevelLocked === true) {
            this.value = appData.cyfunLevel || 'basic';  // restaure la valeur
            showToast2(t('settings.cyfunLevelLockedToast',
              'Niveau verrouillé : modification refusée.'), 'warning');
            return;
          }
          var newLevel = this.value;
          if (typeof setCyfunAssuranceLevel === 'function') setCyfunAssuranceLevel(newLevel);
          if (typeof recalcNis2Maturity === 'function') recalcNis2Maturity();
          if (typeof loadNis2ControlsTable === 'function') loadNis2ControlsTable();
          if (typeof updateNis2RadarChart === 'function') updateNis2RadarChart();
          if (typeof renderNis2ProgramRadar === 'function') renderNis2ProgramRadar();
          if (typeof renderNis2Plan === 'function') renderNis2Plan();
          if (typeof saveData === 'function') saveData();
          // FIX-CYFUN-PERSIST: en mode serveur, saveData est no-op sur l'onglet Settings
          // (bundle.js:12197). Forcer un push immédiat pour persister cyfunLevel + nis2Controls.
          // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
          if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode &&
              window.CSI_SyncManager.isServerMode() && window.CSI_SyncManager.forceServerSync) {
            setTimeout(function() {
              window.CSI_SyncManager.forceServerSync(['cyfunLevel', 'cyfunLang', '_cyfunLevelChosen', 'nis2Controls', '_cyfunEvalArchive', 'targetMaturity']).catch(function(e) {
                void 0;
              });
            }, 500);
          }
          if (typeof updateDashboard === 'function') updateDashboard();
          showToast2(t('settings.cyfunLevelChanged', 'Niveau NIS2 changé : ') + newLevel.toUpperCase(), 'info');
        });

        levelWrap.appendChild(levelSelect);
        body.appendChild(levelWrap);

        // FEAT-CYFUN-NL (2026-07-10) : langue du contenu CyFun modifiable après le 1er
        // lancement (le wizard/la modale sont one-shot). Changer la langue resnapshotte
        // les textes (refreshNis2ControlTexts) sans toucher aux évaluations — pas soumis
        // au verrou de niveau (_cyfunLevelLocked), qui ne protège que du data-loss de niveau.
        var langWrap = document.createElement('div');
        langWrap.className = 'form-group';
        langWrap.style.cssText = 'margin-top:12px;';
        var langLabel = document.createElement('label');
        langLabel.setAttribute('for', 'cyfunLangSelect');
        langLabel.textContent = t('settings.cyfunLang', 'Langue du référentiel CyFun');
        langWrap.appendChild(langLabel);
        var langSelect = document.createElement('select');
        langSelect.id = 'cyfunLangSelect';
        langSelect.className = 'form-control';
        [
          { value: 'en', text: 'English' },
          { value: 'fr', text: 'Français' },
          { value: 'nl', text: 'Nederlands' }
        ].forEach(function(opt) {
          var option = document.createElement('option');
          option.value = opt.value;
          option.textContent = opt.text;
          langSelect.appendChild(option);
        });
        langSelect.value = (typeof appData !== 'undefined' && ['en', 'fr', 'nl'].indexOf(appData.cyfunLang) >= 0) ? appData.cyfunLang : 'en';
        langSelect.dataset.readonlyLock = 'true';
        langSelect.addEventListener('change', function() {
          appData.cyfunLang = this.value;
          if (typeof refreshNis2ControlTexts === 'function') refreshNis2ControlTexts();
          if (typeof loadNis2ControlsTable === 'function') loadNis2ControlsTable();
          if (typeof updateNis2RadarChart === 'function') updateNis2RadarChart();
          if (typeof renderNis2Plan === 'function') renderNis2Plan();
          if (typeof saveData === 'function') saveData();
          if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode &&
              window.CSI_SyncManager.isServerMode() && window.CSI_SyncManager.forceServerSync) {
            setTimeout(function() {
              window.CSI_SyncManager.forceServerSync(['cyfunLang', 'nis2Controls']).catch(function(e) {
                void 0;
              });
            }, 500);
          }
          showToast2(t('settings.cyfunLangChanged', 'Langue du référentiel CyFun changée.'), 'info');
        });
        langWrap.appendChild(langSelect);
        body.appendChild(langWrap);
      }

      // ReCyF (France) — sélecteur de degré EI/EE — carte recyf2025, et seulement quand la
      // France est le référentiel NIS2 actif.
      if (fw.id === 'recyf2025' && appData.nis2Referential === 'recyf2025') {
        var fwR = (window._csiFrameworks || {})['recyf2025'];
        var _cntEI = fwR ? fwR.requirements.filter(function(r) { return r.level === 'importante'; }).length : 76;
        var _cntEE = fwR ? fwR.requirements.length : 152;
        var recyfWrap = document.createElement('div');
        recyfWrap.className = 'form-group';
        recyfWrap.style.cssText = 'margin-top:16px;';
        var degLabel = document.createElement('label');
        degLabel.setAttribute('for', 'recyfDegreeSelect');
        degLabel.textContent = t('settings.recyfDegree', "Degré de l'entité (ReCyF)");
        recyfWrap.appendChild(degLabel);
        var degSelect = document.createElement('select');
        degSelect.id = 'recyfDegreeSelect';
        degSelect.className = 'form-control';
        (window.RECYF_LEVELS || []).forEach(function(lvl) {
          var option = document.createElement('option');
          option.value = lvl.id;
          option.textContent = lvl.short + ' — ' + lvl.label + ' (' + (lvl.id === 'essentielle' ? _cntEE : _cntEI) + ' ' + t('common.measures', 'mesures') + ')';
          degSelect.appendChild(option);
        });
        degSelect.value = appData.recyfLevel || 'importante';
        // Verrou lecture seule : désactivé pour un viewer (mode serveur) via refreshReadOnlyLocks.
        degSelect.dataset.readonlyLock = 'true';
        degSelect.addEventListener('change', function() {
          if (typeof setRecyfDegree === 'function') setRecyfDegree(this.value);
          if (typeof loadRecyfControlsTable === 'function') loadRecyfControlsTable();
          if (typeof updateDashboard === 'function') updateDashboard();
          // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
          if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode &&
              window.CSI_SyncManager.isServerMode() && window.CSI_SyncManager.forceServerSync) {
            setTimeout(function() { window.CSI_SyncManager.forceServerSync(['recyfLevel', 'recyfControls']).catch(function() {}); }, 500);
          }
          showToast2(t('settings.recyfDegreeChanged', 'Degré ReCyF : ') + this.value.toUpperCase(), 'info');
        });
        recyfWrap.appendChild(degSelect);
        body.appendChild(recyfWrap);
      }

      // Bascule du référentiel NIS2 actif (Belgique/CyFun ↔ France/ReCyF) — sur les deux cartes NIS2
      if (fw.id === 'cyfun2025' || fw.id === 'recyf2025') {
        var refWrap = document.createElement('div');
        refWrap.className = 'form-group';
        refWrap.style.cssText = 'margin-top:16px;padding-top:12px;border-top:1px dashed var(--color-border,#e5e7eb);';
        var refLabel = document.createElement('label');
        refLabel.setAttribute('for', 'nis2ReferentialSelect_' + fw.id);
        refLabel.textContent = t('settings.nis2Referential', 'Référentiel NIS2 actif');
        refWrap.appendChild(refLabel);
        var refSelect = document.createElement('select');
        refSelect.id = 'nis2ReferentialSelect_' + fw.id;
        refSelect.className = 'form-control';
        [
          { value: 'cyfun2025', text: t('settings.nis2RefBE', 'NIS2 Belgique — CyFun 2025 (CCB)') },
          { value: 'recyf2025', text: t('settings.nis2RefFR', 'NIS2 France — ReCyF (ANSSI)') }
        ].forEach(function(opt) {
          var option = document.createElement('option');
          option.value = opt.value; option.textContent = opt.text;
          refSelect.appendChild(option);
        });
        refSelect.value = appData.nis2Referential || 'cyfun2025';
        // Verrou lecture seule : désactivé pour un viewer (mode serveur) via refreshReadOnlyLocks.
        refSelect.dataset.readonlyLock = 'true';
        // Verrou « référentiel figé » : une fois le référentiel choisi (wizard/modale), la
        // bascule CyFun<->ReCyF est désactivée pour ne pas masquer par erreur un encodage
        // existant. Seule une réinitialisation rouvre le choix. Le niveau reste modifiable.
        var _isRefLocked = (typeof isNis2ReferentialLocked === 'function') && isNis2ReferentialLocked();
        if (_isRefLocked) {
          refSelect.disabled = true;
          // permanentReadonly : reste désactivé même hors mode lecture seule (verrou définitif).
          refSelect.dataset.permanentReadonly = 'true';
          refSelect.title = t('settings.nis2RefLocked', 'Référentiel verrouillé. Une réinitialisation est nécessaire pour en changer.');
        }
        refSelect.addEventListener('change', function() {
          // Garde-fou (au cas où le disabled serait contourné via DevTools) : refuser toute
          // bascule si le référentiel est verrouillé, et restaurer la valeur affichée.
          if (typeof isNis2ReferentialLocked === 'function' && isNis2ReferentialLocked()) {
            this.value = appData.nis2Referential || 'cyfun2025';
            showToast2(t('settings.nis2RefLockedToast', 'Référentiel verrouillé : une réinitialisation est nécessaire pour en changer.'), 'warning');
            return;
          }
          appData.nis2Referential = this.value;
          // Modèle exclusif : aligner enabledFrameworks sur le référentiel choisi, puis
          // ré-appliquer TOUTE la visibilité (catégorie + onglets + widgets dashboard), pas
          // seulement le filtrage des onglets NIS2.
          if (typeof reconcileNis2Frameworks === 'function') reconcileNis2Frameworks();
          if (this.value === 'recyf2025' && typeof ensureRecyfControls === 'function') ensureRecyfControls();
          if (typeof saveData === 'function') saveData();
          // Ré-applique toute la visibilité (catégorie + onglets + widgets dashboard).
          // applyFrameworkVisibility appelle déjà applyNis2Referential(false) en interne. On NE
          // navigue PAS ici : le sélecteur est dans les Réglages, l'utilisateur y reste pour voir
          // les cartes mises à jour (renderSettingsContent ci-dessous). Évite toute redirection
          // parasite vers le tableau de bord.
          if (window.CSI_FrameworkMapper && typeof CSI_FrameworkMapper.applyFrameworkVisibility === 'function') {
            CSI_FrameworkMapper.applyFrameworkVisibility();
          } else if (typeof applyNis2Referential === 'function') {
            applyNis2Referential(false);
          }
          if (typeof updateDashboard === 'function') updateDashboard();
          // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
          if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode &&
              window.CSI_SyncManager.isServerMode() && window.CSI_SyncManager.forceServerSync) {
            setTimeout(function() { window.CSI_SyncManager.forceServerSync(['nis2Referential', 'cyfunLevel', 'recyfLevel', 'nis2Controls', '_cyfunEvalArchive', 'recyfControls', 'frameworkMapper']).catch(function() {}); }, 500);
          }
          showToast2(t('settings.nis2ReferentialChanged', 'Référentiel NIS2 actif : ') + (this.value === 'recyf2025' ? 'France (ReCyF)' : 'Belgique (CyFun)'), 'info');
          // Re-render la section Réglages pour refléter le statut actif/inactif des deux cartes
          // NIS2 et afficher le bon réglage (niveau CyFun ou degré ReCyF).
          if (typeof renderSettingsContent === 'function') renderSettingsContent();
        });
        refWrap.appendChild(refSelect);
        var refHint = document.createElement('div');
        refHint.style.cssText = 'margin-top:6px;font-size:12px;color:#888;';
        refHint.textContent = t('settings.nis2ReferentialHint', 'Détermine les onglets NIS2 affichés (CyFun Belgique ou ReCyF France).');
        refWrap.appendChild(refHint);
        if (_isRefLocked) {
          var refLockNote = document.createElement('div');
          refLockNote.style.cssText = 'margin-top:6px;font-size:12px;color:#888;font-style:italic;';
          refLockNote.textContent = '🔒 ' + t('settings.nis2RefLockedNote', 'Référentiel verrouillé. Pour en changer, réinitialisez l\'application (les données seront effacées).');
          refWrap.appendChild(refLockNote);
        }
        body.appendChild(refWrap);
      }

      container.appendChild(result.card);
    });
  }

  // === SECTION: ADVANCED ===
  function renderAdvancedSection(container) {
    // Tests Card
    const { card: testCard, body: testBody } = createSettingsCard({
      title: t('settings.testsTitle', 'Tests'),
      description: t('settings.testsDesc', 'Vérification rapide des fonctions principales.'),
      icon: "fa-flask-vial"
    });

    const testActions = document.createElement("div");
    testActions.className = "settings-card__actions";
    const btnTests = document.createElement("button");
    btnTests.id = "settingsTests";
    btnTests.className = "btn btn--secondary btn--sm";
    btnTests.innerHTML = '<i class="fas fa-flask"></i> ' + escapeHtml(t('settings.runTests', 'Lancer les tests'));
    btnTests.addEventListener("click", runTestSuite);
    testActions.appendChild(btnTests);
    testBody.appendChild(testActions);
    container.appendChild(testCard);

  }

  // === SECTION: RESET ===
  function renderResetSection(container) {
    // En mode DEMO, afficher une section spécifique pour recharger les données démo
    if (DEMO_MODE) {
      const { card, body } = createSettingsCard({
        title: t('settings.demoResetTitle', 'Réinitialisation des données démo'),
        description: t('settings.demoResetDesc', 'Recharge les données de démonstration originales.'),
        icon: "fa-sync-alt",
        tone: "warning"
      });

      const info = document.createElement("div");
      info.className = "info-box info-box--warning";
      info.innerHTML = `
        <i class="fas fa-info-circle info-box__icon"></i>
        <div class="info-box__text">
          <strong>${escapeHtml(t('settings.demoModeLabel', 'Mode démonstration'))}</strong><br>
          ${escapeHtml(t('settings.demoResetInfo', 'Les données sont automatiquement rechargées toutes les heures. Utilisez ce bouton pour réinitialiser manuellement.'))}
        </div>
      `;
      body.appendChild(info);

      const actions = document.createElement("div");
      actions.className = "settings-card__actions";
      const btnReloadDemo = document.createElement("button");
      btnReloadDemo.id = "settingsReloadDemo";
      btnReloadDemo.className = "btn btn--warning btn--sm";
      btnReloadDemo.innerHTML = '<i class="fas fa-sync-alt"></i> ' + escapeHtml(t('settings.reloadDemoBtn', 'Recharger les données démo'));
      btnReloadDemo.addEventListener("click", () => {
        showConfirmModal(t('common.confirm', 'Confirmer'), t('confirms.loadDemoData', 'Charger les données de démonstration ? Les données actuelles seront écrasées.'), function() {
          loadDemoDataSilent();
          showToast2(t('toasts.demoDataReloaded'), "success");
        }, { type: 'warning' });
      });
      actions.appendChild(btnReloadDemo);
      body.appendChild(actions);

      container.appendChild(card);
      return;
    }

    // Mode normal : réinitialisation complète
    const { card, body } = createSettingsCard({
      title: t('settings.resetTitle', 'Réinitialisation'),
      description: t('settings.resetDesc', "Réinitialise l'application aux paramètres d'origine."),
      icon: "fa-triangle-exclamation",
      tone: "danger"
    });

    const warning = document.createElement("div");
    warning.className = "danger-warning";
    warning.innerHTML = `
      <i class="fas fa-exclamation-triangle danger-warning__icon"></i>
      <div class="danger-warning__text">
        <strong>${escapeHtml(t('settings.resetWarningTitle', 'Attention :'))}</strong> ${escapeHtml(t('settings.resetWarningText', 'Cette action supprimera toutes vos données. Un snapshot de sécurité sera créé automatiquement avant la réinitialisation.'))}
      </div>
    `;
    body.appendChild(warning);

    const actions = document.createElement("div");
    actions.className = "settings-card__actions";
    const btnReset = document.createElement("button");
    btnReset.id = "settingsReset";
    btnReset.className = "btn btn--danger btn--sm";
    btnReset.innerHTML = '<i class="fas fa-trash"></i> ' + escapeHtml(t('settings.resetBtn', "Réinitialiser l'application"));
    btnReset.dataset.readonlyLock = "true";
    btnReset.addEventListener("click", resetApplication);
    actions.appendChild(btnReset);
    body.appendChild(actions);

    container.appendChild(card);
  }

  // ========================================
  // SETTINGS PAGE OPEN FUNCTION
  // ========================================

  function openSettings() {
    closeModal();
    const navItems = document.querySelectorAll(".nav__item");
    const modules = document.querySelectorAll(".module");
    navItems.forEach(nav => nav.classList.remove("nav__item--active"));
    modules.forEach(m => m.classList.remove("module--active"));
    const settings = document.getElementById("settings");
    if (settings) settings.classList.add("module--active");
    renderSettingsPage();
  }
  async function resetApplication() {
    // En mode serveur, la réinitialisation est réservée aux administrateurs
    // (défense en profondeur : l'entrée « Zone Danger » est déjà masquée aux non-admins).
    var AMreset = window.CSI_AuthManager;
    if (isServerModeUnlocked() && !(AMreset && AMreset.isAdmin())) {
      showToast2(t('settings.resetAdminOnly', 'Réinitialisation réservée à un compte administrateur.'), "warning");
      return;
    }
    if (!ensureEditMode("reset the application")) return;
    const runReset = async () => {
      try {
        try {
          addHistorySnapshot(t('snapshots.backupBeforeReset', 'Sauvegarde avant réinitialisation'), { tag: "auto" });
        } catch (err) {
          void 0;
        }
        appData.controls = await loadISO27002Controls();
        appData.soa = (appData.controls || []).map(c => ({ controlId: c.id, applicable: true, justification: "" }));
        appData.nis2Controls = loadNis2Controls();
        appData.nis2Plan = getEmptyNis2Plan();
        appData.soclePillars = cloneDefaultSoclePillars();
        appData.actions = [];
        appData.actionsHistory = {};
        appData.criticalAssets = [];
        appData.criticalAssetsMeta = getDefaultCriticalAssetsMeta();
        appData.projectRisks = [];
        appData.risks = [];
        appData.threats = [];
        appData.nonconformities = [];
        appData.audits = [];
        appData.kpis = [];
        appData.reviews = [];
        appData.stakeholders = [];
        appData.documents = [];
        appData.targetMaturity = {
          iso27002: 3.5,
          nis2: Object.assign({}, DEFAULT_NIS2_MATURITY),
          nis2Target: 0
        };
        appData.nextActionId = 0;
        appData.documentReview = [];
        appData.riskAnalyses = [];
        appData.impactMatrices = {};
        appData.riskThresholds = {};
        appData.frameworkMapper = { enabledFrameworks: [], customMappings: [], userNotes: {} };
        appData.kpiHistory = {};
        appData.auditLog = [];
        appData.cyfunLevel = '';
        appData.cyfunLang = '';
        appData._cyfunLevelChosen = false;
        appData.title = '';
        // FIX-RESET-RECYF (audit 2026-06-10) : la réinitialisation oubliait les
        // évaluations ReCyF, le référentiel actif et les settings (logo, nom d'orga).
        appData.recyfControls = [];
        appData.recyfLevel = 'importante';
        appData.nis2Referential = 'cyfun2025';
        appData._nis2ReferentialLocked = false; // Reset = seul chemin pour rouvrir le choix du référentiel
        appData.settings = {};
        appData._cyfunEvalArchive = {}; // FIX-CYFUN-LEVEL-LOSS : purger l'archive aussi
        const tbody = document.getElementById("controlsTableBody");
        if (tbody) tbody.innerHTML = "";
        saveData();
        // FIX-RESET-SYNC (audit 2026-06-10) : en mode serveur, saveData() ne pousse que
        // l'onglet courant (et ignore dashboard/settings) — la réinitialisation n'était
        // jamais propagée et les anciennes données revenaient du serveur au rechargement.
        // Remplacement complet intentionnel → forceServerSync() sans clés (l'état vient
        // d'être entièrement réinitialisé en mémoire).
        // M9 (audit 2026-07-10) : ATTENDRE le résultat. executeServerSync peut résoudre
        // {success:false} SANS throw (non authentifié, sync en cours, syncEnabled=false) —
        // le .catch fire-and-forget ne le voyait jamais, le toast « terminé » mentait alors
        // que le serveur conservait l'état antérieur (enjeu RGPD/fin de PoC).
        var _serverResetOk = true;
        if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode &&
            window.CSI_SyncManager.isServerMode() && window.CSI_SyncManager.forceServerSync) {
          try {
            var _resetPush = await window.CSI_SyncManager.forceServerSync();
            if (_resetPush && _resetPush.success === false) _serverResetOk = false;
          } catch (e) {
            _serverResetOk = false;
            void 0;
          }
        }
        loadControlsTable();
        loadNis2ControlsTable();
        loadSoaTable();
        loadActionsTable();
        loadCriticalAssetsTable();
        loadProjectRisksTable();
        loadRisksTable();
        updateRiskMatrix();
        loadThreatsTable();
        loadNonConformitiesTable();
        loadAuditsTable();
        loadReviewsTable();
        loadStakeholdersTable();
        loadDocumentsTable();
        loadKpiTable();
        updateActionsChart();
        updateDashboard();
        // M9 : ne revendiquer le succès que si le serveur a bien été mis à jour.
        if (_serverResetOk) {
          showToast2(t('toasts.resetDone'), "success");
        } else {
          showToast2(t('toasts.resetServerFailed', "Réinitialisation locale effectuée, mais le serveur n'a pas pu être mis à jour. Reprenez la main (mode édition) et réessayez, sinon les anciennes données réapparaîtront au rechargement."), "error", 14000);
        }
      } catch (error) {
        void 0;
        showToast2(t('toasts.resetError', "Impossible de réinitialiser les données."), "error");
      }
    };
    openModal(
      t('settings.confirmReset', 'Confirmer la réinitialisation'),
      [],
      () => {
        runReset();
      },
      null,
      {
        saveLabel: t('common.yes', 'Oui'),
        cancelLabel: t('common.no', 'Non'),
        hideClose: true,
        message: t('settings.confirmResetMessage', 'Cette action va réinitialiser l\'application à son état initial. Toutes vos données seront définitivement supprimées. Êtes-vous sûr ?')
      }
    );
  }

  /**
   * Charge les données de démonstration silencieusement (pour mode DEMO)
   * @param {boolean} silent - Si true, pas de confirmation ni de vérification edit mode
   */
  function loadDemoDataSilent() {
    try {
      if (!demoData) {
        void 0;
        return false;
      }
      appData = JSON.parse(JSON.stringify(demoData));
      window.appData = appData;
      appData.soclePillars = normalizeSoclePillars(appData.soclePillars);
      ensureNis2Plan();
      ensureCriticalAssets();
      ensureProjectRisks();
      normalizeCriticalAssetLinks(appData.actions);
      normalizeCriticalAssetLinks(appData.risks);
      normalizeCriticalAssetLinks(appData.threats);
      normalizeCriticalAssetLinks(appData.nonconformities);
      appData.targetMaturity = {
        iso27002: 4,
        nis2: buildNis2Maturity(demoNis2Maturity),
        nis2Target: 4
      };
      if (!appData.actionsHistory) appData.actionsHistory = {};
      NIS2_CATEGORIES.forEach(cat => {
        if (typeof appData.targetMaturity.nis2[cat] !== 'number') {
          appData.targetMaturity.nis2[cat] = 0;
        }
      });
      const defaultNis2 = loadNis2Controls();
      if (Array.isArray(appData.nis2Controls)) {
        appData.nis2Controls = defaultNis2.map(d => {
          const found = appData.nis2Controls.find(c => c.id === d.id) || {};
          return { ...d, ...found };
        });
      } else {
        appData.nis2Controls = defaultNis2;
      }
      saveData();
      void 0;
      return true;
    } catch (e) {
      void 0;
      return false;
    }
  }
  window.loadDemoDataSilent = loadDemoDataSilent;

  function loadDemoData(options = {}) {
    if (!demoData) {
      void 0;
      showToast2(t('adminUi.demoDataUnavailable', 'Données de démonstration non disponibles'), 'warning');
      return;
    }
    const allowReadOnly = options.allowReadOnly === true;
    if (!allowReadOnly && !ensureEditMode("load demo data")) return;
    showConfirmModal(t('common.confirm', 'Confirmer'), t('confirms.loadDemoData', 'Charger les données de démonstration ? Les données actuelles seront écrasées.'), function() {
    try {
      try {
        addHistorySnapshot(t('snapshots.backupBeforeDemo', 'Sauvegarde avant données de démonstration'), { tag: "auto" });
      } catch (err) {
        void 0;
      }
      appData = JSON.parse(JSON.stringify(demoData));
      window.appData = appData;
      appData.soclePillars = normalizeSoclePillars(appData.soclePillars);
      ensureNis2Plan();
      ensureCriticalAssets();
      ensureProjectRisks();
      normalizeCriticalAssetLinks(appData.actions);
      normalizeCriticalAssetLinks(appData.risks);
      normalizeCriticalAssetLinks(appData.threats);
      normalizeCriticalAssetLinks(appData.nonconformities);
      appData.targetMaturity = {
        iso27002: 4,
        nis2: buildNis2Maturity(demoNis2Maturity),
        nis2Target: 4
      };
      if (!appData.actionsHistory) appData.actionsHistory = {};
      if (!appData.targetMaturity) {
        appData.targetMaturity = { iso27002: 3, nis2: Object.assign({}, DEFAULT_NIS2_MATURITY), nis2Target: 3 };
      } else if (!appData.targetMaturity.nis2) {
        appData.targetMaturity.nis2 = Object.assign({}, DEFAULT_NIS2_MATURITY);
      } else {
        NIS2_CATEGORIES.forEach(cat => {
          if (typeof appData.targetMaturity.nis2[cat] !== 'number') {
            appData.targetMaturity.nis2[cat] = 0;
          }
        });
      }
      if (typeof appData.targetMaturity.nis2Target !== 'number') {
        const v = parseInt(appData.targetMaturity.nis2Target, 10);
        appData.targetMaturity.nis2Target = isNaN(v) ? 3 : v;
      }
      const defaultNis2 = loadNis2Controls();
      if (Array.isArray(appData.nis2Controls)) {
        appData.nis2Controls = defaultNis2.map(d => {
          const found = appData.nis2Controls.find(c => c.id === d.id) || {};
          return { ...d, ...found };
        });
      } else {
        appData.nis2Controls = defaultNis2;
      }
      appData.nextActionId = Array.isArray(appData.actions) ? appData.actions.length : 0;
      saveData();
      loadControlsTable();
      loadNis2ControlsTable();
      loadSoaTable();
      loadActionsTable();
      loadCriticalAssetsTable();
      loadProjectRisksTable();
      loadRisksTable();
      updateRiskMatrix();
      loadThreatsTable();
      loadNonConformitiesTable();
      loadAuditsTable();
      loadReviewsTable();
      loadStakeholdersTable();
      loadDocumentsTable();
      loadKpiTable();
      updateActionsChart();
      updateDashboard();
      showToast2(t('toasts.demoDataLoaded'), "success");
      // Re-queue password setup (may have been skipped while confirm dialog was visible)
      queueEditPasswordSetup();
    } catch (error) {
      void 0;
      showToast2(t('toasts.demoDataLoadError', "Impossible de charger les données."), "error");
    }
    }, { type: 'info' });
  }
  window.exportData = exportData;
  window.importData = importData;
  window.runTestSuite = runTestSuite;
  window.showToast = showToast2;
  window.showToast2 = showToast2;
  window.hideLoadingSpinner = hideLoadingSpinner2;
  window.openSettings = openSettings;
  window.resetApplication = resetApplication;
  window.loadDemoData = loadDemoData;
  window.updateAppTitle = updateAppTitle;
  window.escapeHtml = escapeHtml;
  window.fetchWithTimeout = fetchWithTimeout;
  window.saveData = saveData;

  // iso27002-data.js

var iso27002_data_default = [
  {
    "numero": "5.1",
    "titre": "Politiques de sécurité de l'information",
    "mesure de sécurité": "Il convient de définir une politique de sécurité de l'information ainsi que des politiques spécifiques par thématique, de les faire approuver par la direction, de les publier, de les communiquer aux personnels et parties intéressées concernés en attestant de leur prise de connaissance, et de les examiner à intervalles planifiés ainsi qu'en cas de changements significatifs.",
    "capability": [
      "Gouvernance"
    ]
  },
  {
    "numero": "5.2",
    "titre": "Fonctions et responsabilités liées à la sécurité de l'information",
    "mesure de sécurité": "Il convient de définir et d'attribuer les rôles et responsabilités en matière de sécurité de l'information en fonction des besoins de l'organisation.",
    "capability": [
      "Gouvernance"
    ]
  },
  {
    "numero": "5.3",
    "titre": "Séparation des tâches",
    "mesure de sécurité": "Il convient de séparer les tâches et les domaines de responsabilité incompatibles.",
    "capability": [
      "Gouvernance"
    ]
  },
  {
    "numero": "5.4",
    "titre": "Responsabilités de la direction",
    "mesure de sécurité": "Il convient que la direction exige de l'ensemble du personnel l'application de la sécurité de l'information conformément à la politique de sécurité de l'information établie, aux politiques thématiques spécifiques et aux procédures de l'organisation.",
    "capability": [
      "Gouvernance"
    ]
  },
  {
    "numero": "5.5",
    "titre": "Contacts avec les autorités",
    "mesure de sécurité": "Il convient d'établir et de maintenir des contacts avec les autorités appropriées (p. ex. autorités réglementaires ou forces de l'ordre) en matière de sécurité de l'information, afin de pouvoir agir efficacement en cas d'incident de sécurité de l'information.",
    "capability": [
      "Gouvernance"
    ]
  },
  {
    "numero": "5.6",
    "titre": "Contacts avec des groupes d’intérêt spécifiques",
    "mesure de sécurité": "Il convient que l'organisation établisse et maintienne des contacts avec des groupes d’intérêt spécifiques, forums spécialisés ou associations professionnelles pertinents en sécurité de l'information, afin de se tenir informée des bonnes pratiques et des menaces émergentes.",
    "capability": [
      "Gouvernance"
    ]
  },
  {
    "numero": "5.7",
    "titre": "Renseignements sur les menaces",
    "mesure de sécurité": "Il convient de collecter et d'analyser les renseignements relatifs aux menaces pour la sécurité de l'information, afin de produire des renseignements exploitables sur ces menaces (intelligence des menaces).",
    "capability": [
      "Gouvernance",
      "Gestion_des_menaces_et_des_vulnérabilités"
    ]
  },
  {
    "numero": "5.8",
    "titre": "Sécurité de l'information dans la gestion de projet",
    "mesure de sécurité": "Il convient d'intégrer la sécurité de l'information dans la gestion de projet, afin de s'assurer que les risques de sécurité liés aux projets et aux livrables sont pris en compte dès le début et tout au long du projet.",
    "capability": [
      "Gouvernance"
    ]
  },
  {
    "numero": "5.9",
    "titre": "Inventaire des informations et autres actifs associés",
    "mesure de sécurité": "Il convient d'établir et de tenir à jour un inventaire des informations et des autres actifs associés, à un niveau de granularité approprié, afin de savoir en permanence quels actifs doivent être protégés.",
    "capability": [
      "Gestion_des_actifs"
    ]
  },
  {
    "numero": "5.10",
    "titre": "Utilisation correcte des informations et autres actifs associés",
    "mesure de sécurité": "Il convient d'identifier, de documenter et de mettre en œuvre des règles d'utilisation acceptable des informations et autres actifs associés, et de s'assurer que ces règles sont communiquées aux utilisateurs concernés.",
    "capability": [
      "Gestion_des_actifs"
    ]
  },
  {
    "numero": "5.11",
    "titre": "Restitution des actifs",
    "mesure de sécurité": "Il convient de s'assurer que tous les actifs de l'organisation en possession du personnel sont restitués lors de la cessation ou du changement d'emploi de ce personnel, conformément aux conditions établies.",
    "capability": [
      "Gestion_des_actifs"
    ]
  },
  {
    "numero": "5.12",
    "titre": "Classification des informations",
    "mesure de sécurité": "Il convient de classifier les informations en fonction de leur valeur, de leur sensibilité et de leur criticité, conformément aux besoins métier de l'organisation en matière de sécurité de l'information.",
    "capability": [
      "Gestion_des_actifs"
    ]
  },
  {
    "numero": "5.13",
    "titre": "Marquage des informations",
    "mesure de sécurité": "Il convient de marquer les informations conformément à leur classification, au moyen de procédés d'étiquetage appropriés, de sorte que leur niveau de sensibilité soit clairement indiqué.",
    "capability": [
      "Gestion_des_actifs"
    ]
  },
  {
    "numero": "5.14",
    "titre": "Transfert des informations",
    "mesure de sécurité": "Il convient de protéger le transfert des informations par des politiques, des procédures et des mesures de sécurité appropriées, afin de prévenir tout accès, altération ou perte non autorisés pendant le transfert.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "5.15",
    "titre": "Contrôle d'accès",
    "mesure de sécurité": "Il convient de contrôler l'accès aux informations et aux autres actifs associés, de façon à ce que seules les entités autorisées (utilisateurs, processus, dispositifs) puissent y accéder, conformément aux exigences de sécurité de l'organisation.",
    "capability": [
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "5.16",
    "titre": "Gestion des identités",
    "mesure de sécurité": "Il convient de gérer les identités des utilisateurs et autres entités tout au long de leur cycle de vie (création, gestion, suppression), de manière à permettre une authentification et un contrôle d'accès appropriés.",
    "capability": [
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "5.17",
    "titre": "Informations d'authentification",
    "mesure de sécurité": "Il convient de gérer et de protéger de manière appropriée les informations d'authentification (ex. mots de passe, clés, dispositifs d'authentification) pendant tout leur cycle de vie, afin d'empêcher tout accès non autorisé.",
    "capability": [
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "5.18",
    "titre": "Droits d'accès",
    "mesure de sécurité": "Il convient de gérer l'octroi, la modification et la révocation des droits d'accès aux informations et autres actifs associés, conformément aux politiques de sécurité et au principe du moindre privilège, et de tenir à jour les enregistrements correspondants.",
    "capability": [
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "5.19",
    "titre": "Sécurité de l'information dans les relations avec les fournisseurs",
    "mesure de sécurité": "Il convient de définir et de mettre en œuvre des exigences de sécurité de l'information pour les relations avec les fournisseurs, afin d'assurer la protection des informations échangées ou auxquelles les fournisseurs ont accès.",
    "capability": [
      "Sécurité_des_relations_fournisseurs"
    ]
  },
  {
    "numero": "5.20",
    "titre": "Sécurité de l'information dans les accords avec les fournisseurs",
    "mesure de sécurité": "Il convient d'inclure des exigences de sécurité de l'information dans les accords conclus avec les fournisseurs, de façon à formaliser les obligations de chaque partie en matière de protection des informations.",
    "capability": [
      "Sécurité_des_relations_fournisseurs"
    ]
  },
  {
    "numero": "5.21",
    "titre": "Gestion de la sécurité de l'information dans la chaîne d'approvisionnement TIC",
    "mesure de sécurité": "Il convient de mettre en place des processus pour gérer la sécurité de l'information dans la chaîne d'approvisionnement des technologies de l'information et de la communication (TIC), en abordant les risques de sécurité liés aux fournisseurs de produits et services TIC.",
    "capability": [
      "Sécurité_des_relations_fournisseurs"
    ]
  },
  {
    "numero": "5.22",
    "titre": "Surveillance, révision et gestion des changements des services fournisseurs",
    "mesure de sécurité": "Il convient de surveiller et de réviser régulièrement les services fournis par les fournisseurs, et de gérer les changements apportés à ces services, afin de s'assurer que les contrôles de sécurité convenus restent efficaces et que les exigences de sécurité de l'information continuent d'être respectées.",
    "capability": [
      "Sécurité_des_relations_fournisseurs"
    ]
  },
  {
    "numero": "5.23",
    "titre": "Sécurité de l'information dans l'utilisation de services en nuage",
    "mesure de sécurité": "Il convient d'établir des processus pour l'acquisition, l'utilisation, la gestion et la cessation des services en nuage, conformément aux exigences de sécurité de l'information de l'organisation.",
    "capability": [
      "Sécurité_des_relations_fournisseurs"
    ]
  },
  {
    "numero": "5.24",
    "titre": "Planification et préparation de la gestion des incidents de sécurité de l'information",
    "mesure de sécurité": "Il convient de planifier et de préparer la gestion des incidents de sécurité de l'information en élaborant des procédures et des plans d'intervention, de façon à pouvoir réagir efficacement aux incidents et limiter leurs impacts.",
    "capability": [
      "Réglementation_et_conformité"
    ]
  },
  {
    "numero": "5.25",
    "titre": "Évaluation des événements de sécurité de l'information et prise de décision",
    "mesure de sécurité": "Il convient d'évaluer les événements de sécurité de l'information et de décider s'ils doivent être considérés comme des incidents de sécurité de l'information, afin de déclencher, le cas échéant, les procédures de réponse appropriées.",
    "capability": [
      "Gestion_des_événements_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "5.26",
    "titre": "Réponse aux incidents de sécurité de l'information",
    "mesure de sécurité": "Il convient de répondre aux incidents de sécurité de l'information en appliquant les procédures de gestion des incidents définies, afin de maîtriser l'incident, d'en minimiser l'impact et d'en informer les parties concernées dans les meilleurs délais.",
    "capability": [
      "Gestion_des_événements_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "5.27",
    "titre": "Tirer des enseignements des incidents de sécurité de l'information",
    "mesure de sécurité": "Il convient de tirer des enseignements de chaque incident de sécurité de l'information en analysant ses causes et son traitement, puis de mettre en œuvre les améliorations nécessaires des contrôles de sécurité pour éviter qu'il ne se reproduise.",
    "capability": [
      "Gestion_des_événements_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "5.28",
    "titre": "Collecte des preuves",
    "mesure de sécurité": "Il convient d'établir des procédures pour la collecte, la conservation, l'analyse et la présentation des preuves relatives aux incidents de sécurité de l'information, de façon à pouvoir soutenir d'éventuelles actions juridiques ou enquêtes ultérieures.",
    "capability": [
      "Gestion_des_événements_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "5.29",
    "titre": "Sécurité de l'information pendant une perturbation",
    "mesure de sécurité": "Il convient de s'assurer que la sécurité de l'information est maintenue pendant une perturbation des activités de l'organisation, en prévoyant des mesures de continuité appropriées pour protéger les informations et actifs critiques durant cette perturbation.",
    "capability": [
      "Continuité"
    ]
  },
  {
    "numero": "5.30",
    "titre": "Préparation des TIC pour la continuité d'activité",
    "mesure de sécurité": "Il convient de planifier, de mettre en œuvre, de maintenir et de tester la préparation des technologies de l'information et de la communication (TIC) en vue de la continuité d'activité, sur la base des objectifs de continuité d'activité et des exigences de continuité des TIC.",
    "capability": [
      "Continuité"
    ]
  },
  {
    "numero": "5.31",
    "titre": "Exigences légales, statutaires, réglementaires et contractuelles",
    "mesure de sécurité": "Il convient d'identifier et de documenter les exigences légales, statutaires, réglementaires et contractuelles applicables à la sécurité de l'information, ainsi que la démarche adoptée par l'organisation pour satisfaire à ces exigences.",
    "capability": [
      "Réglementation_et_conformité"
    ]
  },
  {
    "numero": "5.32",
    "titre": "Droits de propriété intellectuelle",
    "mesure de sécurité": "Il convient de mettre en œuvre des procédures appropriées pour garantir le respect des droits de propriété intellectuelle et des licences, en particulier lors de l'utilisation de logiciels et d'informations protégés par le droit d'auteur ou d'autres droits.",
    "capability": [
      "Réglementation_et_conformité"
    ]
  },
  {
    "numero": "5.33",
    "titre": "Protection des enregistrements",
    "mesure de sécurité": "Il convient de protéger les enregistrements (données archivées, journaux, documents officiels, etc.) contre la perte, la destruction, la falsification, l'accès non autorisé et la divulgation non autorisée, pendant toute leur durée de conservation.",
    "capability": [
      "Réglementation_et_conformité"
    ]
  },
  {
    "numero": "5.34",
    "titre": "Protection de la vie privée et des DCP",
    "mesure de sécurité": "Il convient d'assurer la confidentialité et la protection des données à caractère personnel (DCP) conformément aux lois et réglementations en vigueur, notamment en appliquant les principes de privacy by design et en mettant en œuvre des mesures techniques et organisationnelles appropriées.",
    "capability": [
      "Réglementation_et_conformité"
    ]
  },
  {
    "numero": "5.35",
    "titre": "Révision indépendante de la sécurité de l'information",
    "mesure de sécurité": "Il convient de faire procéder à des révisions indépendantes de la sécurité de l'information à intervalles planifiés, ou à la suite de changements significatifs, afin d'évaluer l'efficacité et la conformité du système de management de la sécurité de l'information de l'organisation.",
    "capability": [
      "Gouvernance",
      "Assurance_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "5.36",
    "titre": "Conformité aux politiques, règles et normes de sécurité de l'information",
    "mesure de sécurité": "Il convient de s'assurer de la conformité aux politiques, règles et normes de sécurité de l'information de l'organisation, en réalisant des vérifications régulières et des audits internes, et en prenant des mesures correctives en cas de non-conformité.",
    "capability": [
      "Gouvernance",
      "Assurance_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "5.37",
    "titre": "Procédures d'exploitation documentées",
    "mesure de sécurité": "Il convient de documenter les procédures d'exploitation et de les tenir à jour. Ces procédures d'exploitation documentées doivent être examinées régulièrement et mises à jour lorsque nécessaire. Les modifications apportées aux procédures doivent être autorisées et, lorsque cela est techniquement possible, les systèmes d'information doivent être gérés de manière cohérente en utilisant les mêmes procédures, outils et utilitaires.",
    "capability": [
      "Configuration_sécurisée"
    ]
  },
  {
    "numero": "6.1",
    "titre": "Sélection des candidats",
    "mesure de sécurité": "Il convient d'effectuer des vérifications d'antécédents pour tous les candidats avant leur entrée en fonction, et de façon périodique par la suite, en tenant compte des lois, réglementations et principes éthiques applicables, et en proportionnant le niveau de vérification aux exigences métier, à la sensibilité des informations qui seront accessibles et aux risques perçus.",
    "capability": [
      "Sécurité_des_ressources_humaines"
    ]
  },
  {
    "numero": "6.2",
    "titre": "Termes et conditions du contrat de travail",
    "mesure de sécurité": "Il convient que le contrat de travail définisse les responsabilités de l'employé et de l'organisation en matière de sécurité de l'information, notamment l'obligation de respecter les politiques et procédures de sécurité de l'information de l'organisation.",
    "capability": [
      "Sécurité_des_ressources_humaines"
    ]
  },
  {
    "numero": "6.3",
    "titre": "Sensibilisation, enseignement et formation en sécurité de l'information",
    "mesure de sécurité": "Il convient de mettre en œuvre des programmes de sensibilisation, d'enseignement et de formation à la sécurité de l'information pour tout le personnel de l'organisation, avec des mises à jour régulières, de sorte que chacun dispose des connaissances et compétences nécessaires adaptées à son rôle pour appliquer correctement les mesures de sécurité.",
    "capability": [
      "Sécurité_des_ressources_humaines"
    ]
  },
  {
    "numero": "6.4",
    "titre": "Processus disciplinaire",
    "mesure de sécurité": "Il convient de définir un processus disciplinaire permettant de prendre des sanctions appropriées en cas de manquement d'un employé aux politiques ou aux mesures de sécurité de l'information.",
    "capability": [
      "Sécurité_des_ressources_humaines"
    ]
  },
  {
    "numero": "6.5",
    "titre": "Responsabilités après la fin ou le changement d’un emploi",
    "mesure de sécurité": "Il convient de définir et de communiquer à la personne concernée les responsabilités en matière de sécurité de l'information qui continuent de s'appliquer après la fin ou le changement de son emploi (par exemple, la confidentialité à respecter, la restitution des actifs, etc.).",
    "capability": [
      "Sécurité_des_ressources_humaines"
    ]
  },
  {
    "numero": "6.6",
    "titre": "Accords de confidentialité ou de non-divulgation",
    "mesure de sécurité": "Il convient d'identifier les besoins de l'organisation en matière de confidentialité et de non-divulgation, et de mettre en place des accords de confidentialité ou de non-divulgation conformes à ces besoins. Ces accords doivent être revus régulièrement afin de s'assurer qu'ils demeurent adaptés aux exigences de sécurité de l'information.",
    "capability": [
      "Sécurité_des_ressources_humaines"
    ]
  },
  {
    "numero": "6.7",
    "titre": "Travail à distance",
    "mesure de sécurité": "Il convient de mettre en œuvre des mesures de sécurité pour le travail à distance, afin de protéger les informations consultées, traitées ou stockées en dehors des locaux de l'organisation (par exemple, utilisation de VPN, chiffrement des communications et des dispositifs, politiques de sécurité pour les postes de travail à distance, etc.).",
    "capability": [
      "Sécurité_des_ressources_humaines"
    ]
  },
  {
    "numero": "6.8",
    "titre": "Déclaration des événements de sécurité de l'information",
    "mesure de sécurité": "Il convient d'exiger de tous les employés et contractuels qu'ils déclarent rapidement, par les canaux appropriés, tout événement de sécurité de l'information observé ou suspecté, de manière à permettre une évaluation et une réponse efficaces à ces événements.",
    "capability": [
      "Sécurité_des_ressources_humaines",
      "Gestion_des_événements_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "7.1",
    "titre": "Périmètres de sécurité physique",
    "mesure de sécurité": "Il convient de définir des périmètres de sécurité physique autour des locaux et des zones critiques contenant des informations sensibles ou des actifs importants, et de mettre en place des protections appropriées à ces périmètres (p. ex. clôtures, murs, contrôles d'accès aux entrées) pour empêcher toute intrusion non autorisée.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.2",
    "titre": "Les entrées physiques",
    "mesure de sécurité": "Il convient de contrôler les points d'entrée physiques des zones sécurisées au moyen de dispositifs de sécurité appropriés, de manière à ce que seules les personnes autorisées puissent pénétrer dans ces zones.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.3",
    "titre": "Sécurisation des bureaux, des salles et des installations",
    "mesure de sécurité": "Il convient de mettre en œuvre des mesures de sécurité physique pour protéger les bureaux, les salles et les installations de l'organisation contenant des informations ou des actifs sensibles, par exemple en verrouillant les locaux, en utilisant des systèmes d'alarme et en limitant l'accès aux seules personnes autorisées.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.4",
    "titre": "Surveillance de la sécurité physique",
    "mesure de sécurité": "Il convient de surveiller la sécurité physique des locaux de l'organisation (par exemple, par des systèmes de vidéosurveillance CCTV, des dispositifs de détection d'intrusion ou des rondes de sécurité) afin de détecter et dissuader toute tentative d'accès physique non autorisé ou tout incident de sécurité physique.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.5",
    "titre": "Protection contre les menaces physiques et environnementales",
    "mesure de sécurité": "Il convient de mettre en place des mesures de protection contre les menaces physiques et environnementales – telles que les catastrophes naturelles, les incidents environnementaux (incendie, inondation) ou les actes malveillants – afin de réduire les risques pour les actifs de l'organisation. Par exemple, installer des détecteurs de fumée et des extincteurs, des systèmes anti-inondation, un contrôle de la température et de l'humidité, etc.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.6",
    "titre": "Travail dans les zones sécurisées",
    "mesure de sécurité": "Il convient d'établir des procédures pour le travail dans les zones sécurisées, de façon à ce que seules les personnes autorisées y travaillent et qu'elles respectent des protocoles de sécurité spécifiques (par exemple, surveillance accrue, enregistrement des entrées/sorties, interdiction d'introduire des dispositifs non autorisés).",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.7",
    "titre": "Bureau vide et écran vide",
    "mesure de sécurité": "Il convient d'adopter une politique de bureau vide et d'écran vide, afin de s'assurer qu'aucune information sensible n'est laissée sans protection sur les bureaux ou affichée sur les écrans en l'absence des utilisateurs. Concrètement, les documents papier et supports amovibles doivent être rangés en lieu sûr lorsque non utilisés, et les sessions utilisateur verrouillées ou terminées dès qu'un poste de travail est inoccupé.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.8",
    "titre": "Emplacement et protection du matériel",
    "mesure de sécurité": "Il convient d'installer le matériel informatique et les autres équipements dans des emplacements appropriés et de les protéger pour réduire les risques liés à des accès non autorisés ou à des menaces environnementales. Par exemple, placer les serveurs dans des salles dédiées avec contrôle d'accès et conditions environnementales maîtrisées, afin de préserver la sécurité et le bon fonctionnement des équipements.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.9",
    "titre": "Sécurité des actifs hors des locaux",
    "mesure de sécurité": "Il convient d'assurer la sécurité des actifs (informations et équipements) lorsqu'ils sont utilisés ou stockés en dehors des locaux de l'organisation. Des mesures appropriées doivent être appliquées, telles que le chiffrement des données sur les ordinateurs portables, l'utilisation de dispositifs sécurisés pour le transport de documents, et des politiques encadrant le stockage d'informations à l'extérieur.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.10",
    "titre": "Supports de stockage",
    "mesure de sécurité": "Il convient de gérer les supports de stockage (tels que disques durs, clés USB, CD/DVD, bandes magnétiques) tout au long de leur cycle de vie de manière sécurisée. Cela inclut de contrôler leur accès, de les stocker dans des conditions sûres, de les chiffrer si nécessaire, et de procéder à leur élimination ou réutilisation de manière sécurisée (par exemple par destruction physique ou effacement sécurisé des données).",
    "capability": [
      "Gestion_des_actifs",
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.11",
    "titre": "Services supports",
    "mesure de sécurité": "Il convient de protéger les services supports essentiels (tels que l'alimentation électrique, le réseau électrique, le système de climatisation, l'alimentation en eau et autres utilités) qui permettent le fonctionnement des systèmes d'information. Des solutions de secours ou de redondance doivent être prévues (comme des onduleurs, générateurs, doubles alimentations, etc.) afin d'assurer la continuité des opérations en cas de défaillance de ces services.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.12",
    "titre": "Sécurité du câblage",
    "mesure de sécurité": "Il convient de protéger le câblage des réseaux et télécommunications contre les écoutes, les interférences ou les détériorations. Par exemple, utiliser des conduits ou chemins de câbles sécurisés, réduire les émissions électromagnétiques ou mettre en place des mécanismes de détection de coupure ou d'interception, afin d'empêcher qu'un tiers ne puisse compromettre la confidentialité ou l'intégrité des données en manipulant les câbles.",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.13",
    "titre": "Maintenance du matériel",
    "mesure de sécurité": "Il convient d'assurer une maintenance régulière et adéquate du matériel informatique, conformément aux recommandations du fabricant ou aux besoins de sécurité. Cette maintenance doit être réalisée par du personnel autorisé, et tout équipement de remplacement temporaire doit offrir un niveau de sécurité équivalent. L'objectif est de garantir que les équipements fonctionnent correctement et ne compromettent pas la sécurité de l'information (par exemple à cause de pannes non corrigées).",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "7.14",
    "titre": "Élimination ou recyclage sécurisé(e) du matériel",
    "mesure de sécurité": "Il convient d'éliminer ou de recycler le matériel de manière sécurisée lorsqu'il arrive en fin de vie ou qu'il n'est plus utilisé. Cela implique de s'assurer que toutes les informations et logiciels sensibles qu'il contient sont définitivement supprimés ou rendus inaccessibles avant sa mise au rebut, son recyclage ou sa réutilisation (par exemple en effectuant un effacement sécurisé des données ou en détruisant physiquement le support de stockage).",
    "capability": [
      "Sécurité_physique"
    ]
  },
  {
    "numero": "8.1",
    "titre": "Terminaux finaux des utilisateurs",
    "mesure de sécurité": "Il convient de mettre en œuvre des mesures de sécurité pour protéger les terminaux finaux des utilisateurs (postes de travail, ordinateurs portables, smartphones, tablettes, etc.) et les informations auxquelles ils ont accès. Par exemple, installer des logiciels de sécurité (antivirus, pare-feu personnel), appliquer des correctifs à jour, chiffrer les données sensibles stockées sur ces appareils, et contrôler les accès aux ports et interfaces.",
    "capability": [
      "Sécurité_système_et_réseau"
    ]
  },
  {
    "numero": "8.2",
    "titre": "Droits d'accès privilégiés",
    "mesure de sécurité": "Il convient de restreindre et de gérer l'attribution et l'utilisation des droits d'accès privilégiés (administrateurs systèmes, comptes avec privilèges élevés). Des processus formels doivent encadrer l'octroi de ces droits, et un suivi régulier doit être effectué pour vérifier leur utilisation appropriée et retirer les privilèges non nécessaires.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.3",
    "titre": "Restrictions d'accès aux informations",
    "mesure de sécurité": "Il convient de restreindre l'accès aux informations et aux fonctions des systèmes en fonction des besoins et des autorisations de chaque utilisateur, conformément à la politique de contrôle d'accès de l'organisation. Cela signifie que chaque utilisateur ou entité ne doit pouvoir accéder qu'aux informations et fonctionnalités qui lui sont explicitement autorisées.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.4",
    "titre": "Accès aux codes source",
    "mesure de sécurité": "Il convient de restreindre et de contrôler strictement l'accès aux codes source des logiciels, de manière à ce que seuls les développeurs et personnels autorisés puissent y accéder. Des mécanismes de gestion du code source (dépôts sécurisés, contrôle de versions avec gestion des droits) doivent être en place pour prévenir toute modification non autorisée ou fuite de propriété intellectuelle.",
    "capability": [
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "8.5",
    "titre": "Authentification sécurisée",
    "mesure de sécurité": "Il convient de mettre en place des mécanismes d'authentification sécurisés pour contrôler l'accès aux systèmes et aux informations. Par exemple, utiliser des méthodes robustes telles que l'authentification multi-facteur (MFA) lorsque cela est approprié, gérer les informations d'authentification (mots de passe, certificats, tokens) de façon sécurisée, et s'assurer que les procédures d'authentification répondent aux exigences de sécurité de l'organisation.",
    "capability": [
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "8.6",
    "titre": "Dimensionnement",
    "mesure de sécurité": "Il convient de surveiller et de gérer la capacité et les performances des ressources informatiques (processeurs, mémoire, stockage, bande passante réseau, etc.) afin de répondre aux besoins de l'organisation. Des ajustements proactifs doivent être réalisés pour anticiper les évolutions de charge et éviter toute saturation ou dégradation des performances qui pourrait affecter la disponibilité des services.",
    "capability": [
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "8.7",
    "titre": "Protection contre les programmes malveillants (malware)",
    "mesure de sécurité": "Il convient de déployer des contrôles de sécurité pour prévenir et détecter les programmes malveillants (malwares) et pour s'en remettre. Cela inclut l'installation de logiciels anti-malware à jour, l'analyse régulière des systèmes, la sensibilisation des utilisateurs aux risques liés aux logiciels malveillants (phishing, pièces jointes suspectes, etc.), et la mise en place de mesures de confinement et de récupération en cas d'infection (par exemple, plans de reprise).",
    "capability": [
      "Sécurité_système_et_réseau"
    ]
  },
  {
    "numero": "8.8",
    "titre": "Gestion des vulnérabilités techniques",
    "mesure de sécurité": "Il convient de gérer les vulnérabilités techniques des systèmes d'information en se tenant informé des vulnérabilités connues affectant les matériels, logiciels et services utilisés, et en appliquant en temps utile des mesures correctives (comme l'installation de correctifs de sécurité ou l'application de contournements) pour atténuer les risques associés.",
    "capability": [
      "Configuration_sécurisée",
      "Gestion_des_menaces_et_des_vulnérabilités"
    ]
  },
  {
    "numero": "8.9",
    "titre": "Gestion des configurations",
    "mesure de sécurité": "Il convient de définir, de documenter, de contrôler et de tenir à jour la configuration de tous les composants critiques des systèmes d'information (équipements, logiciels, services). Un processus de gestion des configurations doit s'assurer que seules des configurations approuvées sont utilisées, que tout changement est évalué, approuvé et suivi, et que des configurations sécurisées par défaut sont appliquées pour réduire les vulnérabilités.",
    "capability": [
      "Configuration_sécurisée"
    ]
  },
  {
    "numero": "8.10",
    "titre": "Suppression des informations",
    "mesure de sécurité": "Il convient de supprimer des systèmes d'information les données et informations qui ne sont plus nécessaires, de manière sécurisée et irréversible, sauf si leur conservation est requise par la loi ou par les politiques internes. Des procédures d'effacement sécurisé ou de destruction doivent être utilisées en fonction de la sensibilité des informations, afin d'empêcher toute restitution ou reconstruction ultérieure des données supprimées.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.11",
    "titre": "Masquage des données",
    "mesure de sécurité": "Il convient d'utiliser le masquage des données (techniques d'anonymisation ou de pseudonymisation) pour limiter l'exposition des données sensibles ou personnelles lorsque cela est pertinent, notamment dans les environnements de test, de développement ou lors de partages d'informations. Le masquage réduit le risque de divulgation non autorisée en remplaçant les données réelles par des données fictives mais cohérentes.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.12",
    "titre": "Prévention de la fuite de données",
    "mesure de sécurité": "Il convient de mettre en place des mesures de prévention des fuites de données (Data Loss Prevention, DLP) sur les systèmes et les réseaux de l'organisation, afin de détecter et prévenir la divulgation non autorisée d'informations sensibles. Par exemple, déployer des outils DLP qui contrôlent les transferts de données (courriels, téléchargements, copies sur supports amovibles) et bloquent ou alertent en cas de tentative d'exfiltration de données confidentielles.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.13",
    "titre": "Sauvegarde des informations",
    "mesure de sécurité": "Il convient de maintenir des copies de sauvegarde des informations, des logiciels et des systèmes, et de tester régulièrement ces sauvegardes, conformément à la politique de sauvegarde convenue.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.14",
    "titre": "Redondance des moyens de traitement de l'information",
    "mesure de sécurité": "Il convient de prévoir des moyens redondants pour les installations de traitement de l'information critiques, afin d'assurer la continuité des services en cas de défaillance d'un composant. Par exemple, mettre en place des serveurs redondants, des liaisons réseau doubles ou des centres de données de secours, de sorte qu'une panne matérielle ou logicielle n'affecte pas la disponibilité globale des systèmes.",
    "capability": [
      "Sécurité_système_et_réseau",
      "Continuité"
    ]
  },
  {
    "numero": "8.15",
    "titre": "Journalisation",
    "mesure de sécurité": "Il convient d'activer la journalisation des événements de sécurité et des activités significatives sur les systèmes d'information, et de protéger les journaux ainsi produits contre toute altération ou accès non autorisé. De plus, ces journaux doivent être examinés régulièrement pour détecter des événements inhabituels ou des signes d'incidents de sécurité nécessitant une enquête.",
    "capability": [
      "Assurance_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "8.16",
    "titre": "Activités de surveillance",
    "mesure de sécurité": "Il convient de mettre en place des activités de surveillance continues (monitoring) des réseaux, systèmes et applications de l'organisation, afin de détecter en temps opportun les comportements anormaux, les tentatives d'accès non autorisé ou autres événements de sécurité. Ceci peut inclure l'utilisation de systèmes de détection d'intrusion (IDS/IPS), de systèmes de gestion des informations et des événements de sécurité (SIEM) et d'outils d'analyse des journaux.",
    "capability": [
      "Assurance_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "8.17",
    "titre": "Synchronisation des horloges",
    "mesure de sécurité": "Il convient de synchroniser les horloges de tous les systèmes informatiques pertinents de l'organisation sur une source de temps commune de référence, afin que les horodatages des événements consignés dans les journaux soient cohérents et précis. Cette synchronisation facilite la corrélation des événements de sécurité à travers différents systèmes et peut être réalisée, par exemple, via le protocole NTP.",
    "capability": [
      "Assurance_de_sécurité_de_l_information"
    ]
  },
  {
    "numero": "8.18",
    "titre": "Utilisation de programmes utilitaires à privilèges",
    "mesure de sécurité": "Il convient de restreindre et de contrôler l'utilisation des programmes utilitaires à privilèges (par exemple, outils d'administration système capables de contourner les contrôles de sécurité). Ces programmes ne doivent être accessibles qu'aux personnes autorisées et pour des besoins justifiés, et leur utilisation doit être surveillée et enregistrée afin d'éviter tout abus ou usage malveillant.",
    "capability": [
      "Configuration_sécurisée"
    ]
  },
  {
    "numero": "8.19",
    "titre": "Installation de logiciels sur des systèmes opérationnels",
    "mesure de sécurité": "Il convient de contrôler l'installation de logiciels sur les systèmes opérationnels par le biais de procédures formelles. Toute installation de logiciel doit être autorisée, testée et approuvée préalablement afin de garantir qu'elle n'introduit pas de vulnérabilités ou de perturbations, et de vérifier qu'elle respecte les politiques de l'organisation (notamment en matière de licences et de sécurité).",
    "capability": [
      "Configuration_sécurisée"
    ]
  },
  {
    "numero": "8.20",
    "titre": "Sécurité des réseaux",
    "mesure de sécurité": "Il convient de gérer et de protéger les réseaux de l'organisation afin de préserver la confidentialité, l'intégrité et la disponibilité des informations qui y transitent. Des mesures de sécurité appropriées doivent être mises en œuvre, telles que la segmentation du réseau, l'utilisation de pare-feux, le chiffrement des communications sensibles et la surveillance du trafic réseau pour détecter d'éventuelles activités malveillantes.",
    "capability": [
      "Sécurité_système_et_réseau"
    ]
  },
  {
    "numero": "8.21",
    "titre": "Sécurité des services réseau",
    "mesure de sécurité": "Il convient de définir les exigences de sécurité, les niveaux de service attendus et les responsabilités de gestion pour chaque service réseau utilisé par l'organisation (par exemple, service de télécommunication, hébergement internet, services cloud). Ces éléments doivent être formalisés dans les contrats ou accords de service correspondants, que les services réseau soient fournis en interne ou externalisés, afin d'assurer un niveau de sécurité conforme aux besoins de l'organisation.",
    "capability": [
      "Sécurité_système_et_réseau",
      "Gestion_des_identités_et_des_accès"
    ]
  },
  {
    "numero": "8.22",
    "titre": "Cloisonnement des réseaux",
    "mesure de sécurité": "Il convient de cloisonner les réseaux de l'organisation en zones ou segments, selon les besoins de sécurité, afin d'isoler les systèmes et informations sensibles. Ce cloisonnement (via des VLAN, des sous-réseaux séparés, des pare-feux entre zones, etc.) vise à limiter la propagation des incidents et à appliquer des règles de sécurité spécifiques par zone en fonction du niveau de confiance ou de criticité des actifs qui y résident.",
    "capability": [
      "Sécurité_système_et_réseau"
    ]
  },
  {
    "numero": "8.23",
    "titre": "Filtrage web",
    "mesure de sécurité": "Il convient de mettre en place un mécanisme de filtrage web pour contrôler et restreindre l'accès des utilisateurs aux sites web, en accord avec la politique de l'organisation. Par exemple, cela peut inclure le blocage des sites malveillants ou inappropriés, la catégorisation des sites autorisés/interdits, et la surveillance des activités web pour prévenir les menaces en ligne (comme l'accès à des sites de phishing ou de malware).",
    "capability": [
      "Sécurité_système_et_réseau"
    ]
  },
  {
    "numero": "8.24",
    "titre": "Utilisation de la cryptographie",
    "mesure de sécurité": "Il convient d'utiliser des techniques cryptographiques appropriées pour protéger la confidentialité, l'intégrité et, le cas échéant, l'authenticité des informations. Une politique d'utilisation de la cryptographie doit être définie, couvrant des aspects tels que les algorithmes autorisés, la gestion des clés cryptographiques, et les cas d'utilisation (chiffrement des données stockées, chiffrement des communications, signatures numériques, etc.).",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.25",
    "titre": "Cycle de vie de développement sécurisé",
    "mesure de sécurité": "Il convient d'intégrer des pratiques de sécurité tout au long du cycle de vie de développement des systèmes d'information. Cela implique d'établir des règles et contrôles de sécurité lors des phases de spécification, conception, développement, test, déploiement et maintenance, afin de s'assurer que les logiciels et systèmes sont conçus et développés en respectant les principes de sécurité (par exemple, en effectuant des revues de code, des tests de sécurité applicative, en suivant des guides de codage sécurisé, etc.).",
    "capability": [
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.26",
    "titre": "Exigences de sécurité des applications",
    "mesure de sécurité": "Il convient d'identifier les exigences de sécurité pertinentes pour les nouvelles applications ou évolutions d'applications dès les premières étapes de leur acquisition ou développement, et de s'assurer que ces exigences sont prises en compte dans les spécifications et mises en œuvre dans les solutions finales.",
    "capability": [
      "Sécurité_système_et_réseau",
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.27",
    "titre": "Principes d'ingénierie et d'architecture des systèmes sécurisés",
    "mesure de sécurité": "Il convient de définir, documenter et appliquer des principes d'architecture et d'ingénierie pour la conception de systèmes sécurisés. Ces principes (par exemple, le principe du moindre privilège, la défense en profondeur, la simplicité, la modularité, la résilience) doivent guider le développement ou l'acquisition de nouvelles technologies afin de s'assurer que la sécurité est intégrée de manière cohérente et efficace dans l'architecture des systèmes d'information.",
    "capability": [
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.28",
    "titre": "Codage sécurisé",
    "mesure de sécurité": "Il convient d'appliquer des pratiques de codage sécurisé lors du développement de logiciels, de façon à réduire les vulnérabilités dans le code. Cela peut inclure le respect de normes de codage, l'utilisation d'outils d'analyse de code statique pour détecter les failles courantes, la sensibilisation des développeurs aux erreurs de programmation susceptibles d'affecter la sécurité, et la réalisation de revues de code focalisées sur la sécurité.",
    "capability": [
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.29",
    "titre": "Tests de sécurité dans le développement et l'acceptation",
    "mesure de sécurité": "Il convient d'effectuer des tests de sécurité lors des phases de développement et d'acceptation des nouveaux systèmes ou des modifications système. Ces tests (tels que des tests d'intrusion, des analyses de vulnérabilités, des tests de charge pour vérifier la résilience) doivent permettre de vérifier que les contrôles de sécurité fonctionnent comme prévu et que le système répond aux critères de sécurité définis avant sa mise en production.",
    "capability": [
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.30",
    "titre": "Développement externalisé",
    "mesure de sécurité": "Il convient de prendre en compte les exigences de sécurité lorsque le développement de logiciels ou de systèmes est externalisé. L'organisation doit s'assurer que les prestataires respectent des critères de sécurité équivalents (via des clauses contractuelles, des contrôles réguliers, des droits d'audit) et doit suivre l'avancement du projet pour vérifier que les mesures de sécurité convenues sont bien appliquées pendant le développement externalisé.",
    "capability": [
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.31",
    "titre": "Séparation des environnements de développement, de test et opérationnels",
    "mesure de sécurité": "Il convient de séparer les environnements de développement, de test et d'exploitation (production) afin de réduire les risques de modifications non autorisées ou d'accès inappropriés aux données de production. Chaque environnement doit être isolé avec des jeux de données distincts (les tests ne devant pas se faire sur des données de production réelles sans précautions), et les droits d'accès des personnels doivent être limités à l'environnement dans lequel ils opèrent.",
    "capability": [
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.32",
    "titre": "Gestion des changements",
    "mesure de sécurité": "Il convient de contrôler et d'approuver formellement tous les changements apportés aux systèmes d'information, aux logiciels ou aux services, au moyen d'un processus de gestion des changements. Ce processus doit inclure l'évaluation préalable des impacts de sécurité des changements, l'autorisation par les instances appropriées, le test et la documentation des modifications, et un suivi pour s'assurer que les changements sont correctement implémentés et qu'ils n'ont pas d'effets indésirables sur la sécurité.",
    "capability": [
      "Sécurité_des_applications"
    ]
  },
  {
    "numero": "8.33",
    "titre": "Informations de test",
    "mesure de sécurité": "Il convient de sélectionner soigneusement les informations utilisées pour les tests (jeux de données de test) et de les protéger au même titre que des données réelles si elles sont sensibles. Idéalement, des données fictives ou anonymisées devraient être utilisées. Toute information de test doit être gérée et effacée une fois les tests terminés, afin de prévenir toute fuite ou utilisation abusive.",
    "capability": [
      "Protection_des_informations"
    ]
  },
  {
    "numero": "8.34",
    "titre": "Protection des systèmes d'information pendant les tests d'audit",
    "mesure de sécurité": "Il convient de contrôler l'accès aux systèmes d'information lors de la réalisation de tests d'audit (internes ou externes) afin de s'assurer que ces tests n'altèrent pas la sécurité ou la disponibilité des systèmes et des données. Par exemple, les auditeurs ne devraient accéder qu'aux informations et ressources nécessaires à leur audit, et leurs activités de test devraient être surveillées. Des mesures doivent être en place pour restaurer l'état normal du système après les tests d'audit et corriger toute faiblesse identifiée.",
    "capability": [
      "Sécurité_des_applications"
    ]
  }
];

// ---- CyFun 2025 / NIS2 framework helpers ----
// The old nis2_controls_data (23 category-level controls) is replaced by the CyFun 2025 framework
// loaded from data/frameworks/cyfun2025.js. Legacy nis2_controls_data kept as empty for compat.
var nis2_controls_data = []; // legacy stub - data now comes from _csiFrameworks['cyfun2025']

// CATEGORY_LABELS is now built dynamically from the CyFun framework
// FEAT-CYFUN-NL (2026-07-10) : résolution trilingue du contenu CyFun (textes officiels CCB).
// Exigences : base EN (title/description) + suffixes Fr/Nl. Libellés fonctions/catégories :
// base FR (label) + labelEn/labelNl. Cascade : langue demandée -> EN -> base.
function cyfunContentLang() {
  return (typeof appData !== 'undefined' && appData.cyfunLang) || 'en';
}
function cyfunReqText(req, base) {
  if (!req) return '';
  var lang = cyfunContentLang();
  if (lang === 'fr' && req[base + 'Fr']) return req[base + 'Fr'];
  if (lang === 'nl' && req[base + 'Nl']) return req[base + 'Nl'];
  return req[base] || '';
}
function cyfunLabelText(obj) {
  if (!obj) return '';
  var lang = cyfunContentLang();
  if (lang === 'fr') return obj.label;
  if (lang === 'nl' && obj.labelNl) return obj.labelNl;
  return obj.labelEn || obj.label;
}

var CATEGORY_LABELS = {};
function rebuildCategoryLabels() {
  CATEGORY_LABELS = {};
  var fw = (window._csiFrameworks || {})['cyfun2025'];
  if (!fw || !fw.meta || !fw.meta.categories) return;
  fw.meta.categories.forEach(function(cat) {
    var lbl = cyfunLabelText(cat);
    CATEGORY_LABELS[cat.id] = lbl + ' (' + cat.id + ')';
  });
}
// Build labels once framework is available (called during init)

// Get the CyFun function label for a functionId
function getCyfunFunctionLabel(functionId) {
  var fn = (window.CYFUN_FUNCTIONS || []).find(function(f) { return f.id === functionId; });
  return fn ? cyfunLabelText(fn) + ' (' + fn.id + ')' : functionId;
}

// Get the CyFun category label for a categoryId
function getCyfunCategoryLabel(categoryId) {
  return CATEGORY_LABELS[categoryId] || categoryId;
}

// FEAT-CYFUN-NL (2026-07-10) : rafraîchit les textes snapshotés dans nis2Controls depuis
// le référentiel (langue CyFun courante). Idempotent, ne touche jamais aux évaluations.
// Nécessaire pour que les installations EXISTANTES reçoivent les textes officiels CCB
// (FR révisé, NL) sans re-choisir le niveau — appelé à chaque rendu du tableau NIS2.
function refreshNis2ControlTexts() {
  if ((appData.nis2Referential || 'cyfun2025') !== 'cyfun2025') return 0;
  var fw = (window._csiFrameworks || {})['cyfun2025'];
  if (!fw || !fw.requirements || !Array.isArray(appData.nis2Controls)) return 0;
  var byRef = {};
  fw.requirements.forEach(function(r) { byRef[r.ref] = r; });
  var changed = 0;
  appData.nis2Controls.forEach(function(c) {
    var req = c && c.ref ? byRef[c.ref] : null;
    if (!req) return;
    var funcObj = (fw.meta.functions || []).find(function(f) { return f.id === req.functionId; });
    var catObj = (fw.meta.categories || []).find(function(k) { return k.id === req.categoryId; });
    var fresh = {
      title: cyfunReqText(req, 'title'),
      titleEn: req.title || '',
      description: cyfunReqText(req, 'description'),
      descriptionEn: req.description || '',
      functionLabel: funcObj ? (cyfunLabelText(funcObj) + ' (' + funcObj.id + ')') : req.functionId,
      categoryLabel: catObj ? (cyfunLabelText(catObj) + ' (' + catObj.id + ')') : req.categoryId
    };
    fresh.fonction = fresh.functionLabel;
    fresh.categorie = fresh.categoryLabel;
    Object.keys(fresh).forEach(function(k) {
      if (c[k] !== fresh[k]) { c[k] = fresh[k]; changed++; }
    });
  });
  if (changed) rebuildCategoryLabels();
  return changed;
}

// CyFun assurance level management
function setCyfunAssuranceLevel(level) {
  if (!level || !['basic', 'important', 'essential'].includes(level)) level = 'basic';
  appData.cyfunLevel = level;
  var fw = (window._csiFrameworks || {})['cyfun2025'];
  if (!fw || !fw.requirements) return;

  // Rebuild categories for this level
  if (typeof window.rebuildCyfunCategories === 'function') {
    window.rebuildCyfunCategories(level);
  }
  rebuildCategoryLabels();

  // Filter requirements by cumulative level
  var levelOrder = { basic: 1, important: 2, essential: 3 };
  var maxOrder = levelOrder[level];
  var requirements = fw.requirements.filter(function(r) {
    return (levelOrder[r.level] || 99) <= maxOrder;
  });

  // Preserve existing scores by ref
  var _FM = window.CSI_FrameworkMapper;
  var existingMap = {};
  var _existingControls = _FM ? _FM.getAppDataControls('cyfun2025') : (appData.nis2Controls || []);
  _existingControls.forEach(function(c) {
    if (c.ref) existingMap[c.ref] = c;
  });

  // FIX-CYFUN-LEVEL-LOSS (audit 2026-06-10) : un passage à un niveau inférieur supprimait
  // définitivement les évaluations des exigences important/essential (aller-retour
  // essential→basic→essential = ~99/133 évaluations perdues). Les évaluations des
  // exigences hors du niveau choisi sont désormais archivées par ref et restaurées
  // automatiquement quand le niveau les réintègre.
  if (!appData._cyfunEvalArchive || typeof appData._cyfunEvalArchive !== 'object' || Array.isArray(appData._cyfunEvalArchive)) {
    appData._cyfunEvalArchive = {};
  }
  var _archive = appData._cyfunEvalArchive;

  // Rebuild nis2Controls from filtered requirements
  var _newNis2Controls = requirements.map(function(req, index) {
    var existing = existingMap[req.ref] || _archive[req.ref] || {};
    var funcObj = (fw.meta.functions || []).find(function(f) { return f.id === req.functionId; });
    var catObj = (fw.meta.categories || []).find(function(c) { return c.id === req.categoryId; });
    // Trilingue : textes snapshotés dans la langue CyFun courante (helpers FEAT-CYFUN-NL)
    var displayTitle = cyfunReqText(req, 'title');
    var displayDesc = cyfunReqText(req, 'description');
    var funcLabel = funcObj ? (cyfunLabelText(funcObj) + ' (' + funcObj.id + ')') : req.functionId;
    var catLabel = catObj ? (cyfunLabelText(catObj) + ' (' + catObj.id + ')') : req.categoryId;
    var rebuilt = {
      // FIX-CYFUN-DUP-ID (audit 2026-06-10) : les nouveaux ids étaient dérivés de l'index
      // de la liste filtrée ('CYFUN-001'...) et entraient en collision avec les ids
      // préservés après une montée de niveau (26 paires dupliquées sur basic→important
      // → modale et sauvegarde sur la MAUVAISE exigence). Les nouveaux ids sont
      // désormais dérivés de la ref, unique et stable ('CYFUN-GV.OC-01.1').
      id: existing.id || 'CYFUN-' + req.ref,
      ref: req.ref,
      title: displayTitle,
      titleEn: req.title || '',
      description: displayDesc,
      descriptionEn: req.description || '',
      functionId: req.functionId,
      functionLabel: funcLabel,
      categoryId: req.categoryId,
      categoryLabel: catLabel,
      subcategoryRef: req.subcategoryRef || '',
      level: req.level,
      isKeyMeasure: req.isKeyMeasure || false,
      isManagementLinked: req.isManagementLinked || false,
      docMaturity: existing.docMaturity || 0,
      implMaturity: existing.implMaturity || 0,
      justification: existing.justification || '',
      evidence: existing.evidence || '',
      owner: existing.owner || '',
      reviewer: existing.reviewer || '',
      reviewFrequency: existing.reviewFrequency || '',
      lastEvaluation: existing.lastEvaluation || '',
      nextReviewDate: existing.nextReviewDate || '',
      // Legacy compat fields (computed)
      fonction: funcLabel,
      categorie: catLabel,
      status: existing.status || 'non_implemente',
      cmm: existing.cmm || 'Initial'
    };
    // FIX-CYFUN-LEVEL-LOSS : préserver les champs non structurels existants
    // (comments, lastEvaluation, ...) qui étaient effacés à chaque changement de niveau.
    Object.keys(existing).forEach(function(k) {
      if (!(k in rebuilt)) rebuilt[k] = existing[k];
    });
    return rebuilt;
  });

  // FIX-CYFUN-LEVEL-LOSS : archiver les évaluations des exigences qui sortent du
  // périmètre du niveau choisi, et retirer de l'archive celles qui viennent d'être
  // restaurées dans la liste active.
  var _keptRefs = {};
  requirements.forEach(function(r) { _keptRefs[r.ref] = true; });
  _existingControls.forEach(function(c) {
    if (c.ref && !_keptRefs[c.ref]) _archive[c.ref] = c;
  });
  requirements.forEach(function(r) {
    if (!existingMap[r.ref] && _archive[r.ref]) delete _archive[r.ref];
  });
  // Store via FrameworkMapper if available, otherwise direct assignment
  if (window.CSI_FrameworkMapper) {
    CSI_FrameworkMapper.setAppDataControls('cyfun2025', _newNis2Controls);
  } else {
    appData.nis2Controls = _newNis2Controls;
  }

  // Rebuild DEFAULT_NIS2_MATURITY
  window.DEFAULT_NIS2_MATURITY = {};
  CYFUN_CATEGORIES.forEach(function(catId) {
    window.DEFAULT_NIS2_MATURITY[catId] = 0;
  });

  // Update targetMaturity.nis2 to match new categories
  if (appData.targetMaturity) {
    var oldNis2 = appData.targetMaturity.nis2 || {};
    appData.targetMaturity.nis2 = {};
    CYFUN_CATEGORIES.forEach(function(catId) {
      appData.targetMaturity.nis2[catId] = typeof oldNis2[catId] === 'number' ? oldNis2[catId] : 0;
    });
  }

  // Recalculate maturity averages from controls
  recalcNis2Maturity();
  saveData();
}

// Recalculate maturity averages from nis2Controls
function recalcNis2Maturity() {
  if (!appData.targetMaturity) appData.targetMaturity = {};
  var sums = {};
  var counts = {};
  CYFUN_CATEGORIES.forEach(function(c) { sums[c] = 0; counts[c] = 0; });
  var _fmNis2 = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls('cyfun2025') : (appData.nis2Controls || []);
  _fmNis2.forEach(function(c) {
    var catId = c.categoryId || '';
    if (!sums.hasOwnProperty(catId)) return;
    var doc = c.docMaturity || 0;
    var impl = c.implMaturity || 0;
    if (doc > 0 || impl > 0) {
      sums[catId] += (doc + impl) / 2;
      counts[catId]++;
    }
  });
  var averages = {};
  CYFUN_CATEGORIES.forEach(function(c) {
    averages[c] = counts[c] > 0 ? +(sums[c] / counts[c]).toFixed(2) : 0;
  });
  appData.targetMaturity.nis2 = averages;
}

// CyFun scoring engine
function computeCyfunScoring() {
  var level = appData.cyfunLevel || 'basic';
  var controls = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls('cyfun2025') : (appData.nis2Controls || []);
  var fw = (window._csiFrameworks || {})['cyfun2025'];
  if (!fw) return null;
  var levelConfig = (fw.meta.assuranceLevels || []).find(function(l) { return l.id === level; });
  if (!levelConfig) return null;

  var totalDoc = 0, totalImpl = 0, scoredCount = 0, totalCount = controls.length;
  var keyDoc = 0, keyImpl = 0, keyCount = 0, keyScoredCount = 0;
  var categoryScores = {};

  controls.forEach(function(c) {
    var doc = c.docMaturity || 0;
    var impl = c.implMaturity || 0;
    if (doc > 0 || impl > 0) {
      totalDoc += doc;
      totalImpl += impl;
      scoredCount++;
    }
    if (c.isKeyMeasure) {
      keyCount++;
      if (doc > 0 || impl > 0) {
        keyDoc += doc;
        keyImpl += impl;
        keyScoredCount++;
      }
    }
    var catId = c.categoryId;
    if (!categoryScores[catId]) categoryScores[catId] = { docSum: 0, implSum: 0, count: 0, scored: 0 };
    categoryScores[catId].count++;
    if (doc > 0 || impl > 0) {
      categoryScores[catId].docSum += doc;
      categoryScores[catId].implSum += impl;
      categoryScores[catId].scored++;
    }
  });

  // M4 (audit 2026-07-10) : verdict de conformité « coverage-aware » conforme à la méthode
  // CCB (une mesure non évaluée compte pour 0). Le dénominateur est le nombre TOTAL de
  // mesures (et de mesures clés), pas seulement les évaluées — sinon une seule mesure à 5/5
  // suffirait à franchir le seuil (même travers que le KPI dashboard corrigé en M3). La
  // moyenne par catégorie utilise déjà cs.count → l'ensemble du moteur est cohérent.
  var avgDoc = totalCount > 0 ? +(totalDoc / totalCount).toFixed(2) : 0;
  var avgImpl = totalCount > 0 ? +(totalImpl / totalCount).toFixed(2) : 0;
  var avgTotal = totalCount > 0 ? +((totalDoc + totalImpl) / (2 * totalCount)).toFixed(2) : 0;
  var avgKeyDoc = keyCount > 0 ? +(keyDoc / keyCount).toFixed(2) : 0;
  var avgKeyImpl = keyCount > 0 ? +(keyImpl / keyCount).toFixed(2) : 0;
  var avgKeyTotal = keyCount > 0 ? +((keyDoc + keyImpl) / (2 * keyCount)).toFixed(2) : 0;

  var thresholds = levelConfig.thresholds || {};
  var passes = {
    keyMeasures: avgKeyTotal >= (thresholds.keyMeasures || 0),
    total: avgTotal >= (thresholds.total || 0),
    categories: true
  };

  // Per-category scores and category minimum check (essential only)
  var catDetails = {};
  Object.keys(categoryScores).forEach(function(catId) {
    var cs = categoryScores[catId];
    var catAvg = cs.count > 0 ? +((cs.docSum + cs.implSum) / (2 * cs.count)).toFixed(2) : 0;
    catDetails[catId] = { avg: catAvg, doc: cs.count > 0 ? +(cs.docSum / cs.count).toFixed(2) : 0, impl: cs.count > 0 ? +(cs.implSum / cs.count).toFixed(2) : 0, count: cs.count, scored: cs.scored };
    if (thresholds.categoryMin && catAvg < thresholds.categoryMin && cs.count > 0) {
      passes.categories = false;
    }
  });

  // Detect unevaluated categories for UI warnings
  var unevaluatedCategories = [];
  Object.keys(categoryScores).forEach(function(catId) {
    if (categoryScores[catId].count > 0 && categoryScores[catId].scored === 0) {
      unevaluatedCategories.push(catId);
    }
  });

  var overallPass = passes.keyMeasures && passes.total && passes.categories;

  return {
    level: level,
    levelLabel: levelConfig.label,
    totalRequirements: totalCount,
    scoredRequirements: scoredCount,
    avgDoc: avgDoc,
    avgImpl: avgImpl,
    avgTotal: avgTotal,
    keyMeasureCount: keyCount,
    keyScoredCount: keyScoredCount,
    avgKeyDoc: avgKeyDoc,
    avgKeyImpl: avgKeyImpl,
    avgKeyTotal: avgKeyTotal,
    categoryScores: catDetails,
    thresholds: thresholds,
    passes: passes,
    overallPass: overallPass,
    unevaluatedCategories: unevaluatedCategories
  };
}

// Detect legacy NIS2 format (old 23-control format)
function detectLegacyNis2Format(controls) {
  if (!Array.isArray(controls) || controls.length === 0) return 'empty';
  var first = controls[0];
  // Old format: has 'fonction' and 'categorie' fields, no 'ref' or 'docMaturity'
  if (first.fonction && first.categorie && !first.ref) return 'legacy_23';
  // New CyFun format
  if (first.ref && typeof first.docMaturity !== 'undefined') return 'cyfun';
  return 'unknown';
}

// Migrate old 23-control NIS2 data to CyFun format
function migrateLegacyNis2Controls(oldControls) {
  // Map old NIST CSF 1.1 category codes to new NIST CSF 2.0 codes
  var migrationMap = {
    'ID.AM': 'ID.AM', 'ID.BE': 'GV.OC', 'ID.GV': 'GV.PO', 'ID.RA': 'ID.RA',
    'ID.RM': 'GV.RM', 'ID.SC': 'GV.SC', 'PR.AC': 'PR.AA', 'PR.AT': 'PR.AT',
    'PR.DS': 'PR.DS', 'PR.IP': 'PR.PS', 'PR.MA': 'PR.PS', 'PR.PT': 'PR.IR',
    'DE.AE': 'DE.AE', 'DE.CM': 'DE.CM', 'DE.DP': 'GV.OV',
    'RS.RP': 'RS.MA', 'RS.CO': 'RS.CO', 'RS.AN': 'RS.AN', 'RS.MI': 'RS.MI',
    'RS.IM': 'ID.IM', 'RC.RP': 'RC.RP', 'RC.IM': 'ID.IM', 'RC.CO': 'RC.CO'
  };
  var cmmToVal = { 'Initial': 1, 'R\u00e9p\u00e9table': 2, 'D\u00e9fini': 3, 'G\u00e9r\u00e9': 4, 'Optimis\u00e9': 5 };
  var categoryMaturity = {};
  (oldControls || []).forEach(function(c) {
    var match = (c.categorie || '').match(/\(([^)]+)\)/);
    var oldCat = match ? match[1] : '';
    var newCat = migrationMap[oldCat];
    if (!newCat) return;
    var val = typeof c.cmm === 'number' ? c.cmm : (cmmToVal[c.cmm] || 0);
    if (!categoryMaturity[newCat] || val > categoryMaturity[newCat]) {
      categoryMaturity[newCat] = val;
    }
  });
  return categoryMaturity;
}

// ============================================================
// DEMO DATA - Extracted to demo-data.js (loaded only in WEBSITE/app/)
// In DEMO_MODE, deps are exposed on window for demo-data.js to use.
// ============================================================
var demoData = null;
var demoNis2Maturity = null;

// Setter for external demo-data.js injection
window._csiSetDemoData = function(data, maturity) {
  demoData = data;
  demoNis2Maturity = maturity;
};

// Expose deps for demo-data.js (used by WEBSITE/app/ demo only).
// Always exposed: demo-data.js is never bundled outside of demo packages,
// so this has no effect in Community/Corporate builds.
window._csiDemoDeps = {
  iso27002_data_default: iso27002_data_default,
  normalizeCapabilityKey: normalizeCapabilityKey,
  DOMAIN_BY_PREFIX: DOMAIN_BY_PREFIX,
  loadNis2Controls: loadNis2Controls,
  cloneDefaultNis2Plan: cloneDefaultNis2Plan
};

// Legacy compat: keep demoRiskIds for any remaining references
const demoRiskIds = [
  "risk-demo-1", "risk-demo-2", "risk-demo-3", "risk-demo-4", "risk-demo-5",
  "risk-demo-6", "risk-demo-7", "risk-demo-8", "risk-demo-9", "risk-demo-10"
];

// END DEMO DATA STUB
// (Original ~920 lines of demo data removed - see demo-data.js)

const _demoDataStubEnd = null; // anchor for search - do not remove
  // modules/data.js
  var appData = {
    controls: [],
    actions: [],
    actionsHistory: {},
    criticalAssets: [],
    criticalAssetsMeta: getDefaultCriticalAssetsMeta(),
    projectRisks: [],
    risks: [],
    threats: [],
    nonconformities: [],
    audits: [],
    kpis: [],
    reviews: [],
    stakeholders: [],
    documents: [],
    documentReview: [],
    nis2Controls: [],
    nis2Plan: getEmptyNis2Plan(),
    recyfControls: [],
    recyfLevel: 'importante',
    nis2Referential: 'cyfun2025',
    _nis2ReferentialLocked: false,
    soclePillars: cloneDefaultSoclePillars(),
    soa: [],
    frameworkMapper: { enabledFrameworks: _defaultEnabledFrameworks(), customMappings: [], userNotes: {} },
    nextActionId: 0,
    title: "Cyber-Assistant",
    targetMaturity: {
      iso27002: 3,
      nis2: Object.assign({}, DEFAULT_NIS2_MATURITY),
      nis2Target: 0
    }
  };
  window.appData = appData;
  var charts = {};
  window.charts = charts;
  async function loadData() {
    if (window._csiInvalidateSearchIndex) window._csiInvalidateSearchIndex(); // (audit 2026-07-03) index recherche périmé au (re)chargement
    var _isServerMode = window.CSI_SyncManager && window.CSI_SyncManager.isServerMode && window.CSI_SyncManager.isServerMode()
        && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn();
    var _serverPullHasData = false; // FIX-SERVER-PULL-AUTHORITY (revue concurrence 2026-06-11)

    // ──────────────────────────────────────────────────────────
    // MODE SERVEUR : charger UNIQUEMENT depuis le serveur (pas de localStorage)
    // ──────────────────────────────────────────────────────────
    if (_isServerMode) {
      // FIX-RETRY: Retry une fois avant de déclarer le serveur inaccessible
      var pulled = null;
      for (var _pullAttempt = 0; _pullAttempt < 2; _pullAttempt++) {
        try {
          pulled = await window.CSI_SyncManager.pullFromServer();
          if (pulled !== null && pulled !== undefined) break;
          if (_pullAttempt === 0) {
            void 0;
            await new Promise(function(r) { setTimeout(r, 1000); });
          }
        } catch(e) {
          void 0;
          pulled = null;
          if (_pullAttempt === 0) await new Promise(function(r) { setTimeout(r, 1000); });
        }
      }
      if (pulled && typeof pulled === 'object' && !Array.isArray(pulled)) {
        // FIX-H2: Accepter les réponses vides comme valides (nouvelle installation)
        appData = pulled;
        window.appData = appData;
        // FIX-SERVER-PULL-AUTHORITY (revue concurrence 2026-06-11) : un pull serveur qui
        // ramène des données métier (≥1 clé) fait AUTORITÉ — même sans `controls`. Sinon le
        // garde `_hasLoadedData` (basé sur controls.length) déclenchait initializeDefaultData()
        // qui ÉCRASAIT les autres données pullées (actions, risques…) d'un utilisateur
        // rejoignant un SI dont le référentiel de contrôles n'est pas (encore) peuplé.
        _serverPullHasData = Object.keys(pulled).length > 0;
      } else if (pulled === null || pulled === undefined) {
        // FIX-SETUP: Après setup du premier admin, null = installation fraîche (pas d'erreur)
        var _justSetup = window.CSI_AuthManager && window.CSI_AuthManager.wasSetup && window.CSI_AuthManager.wasSetup();
        if (_justSetup) {
          void 0;
        } else {
          void 0;
          window._csiServerLoadFailed = true;
        }
      }
    }

    // ──────────────────────────────────────────────────────────
    // MODE LOCAL : charger depuis localStorage (comportement inchangé)
    // ──────────────────────────────────────────────────────────
    if (!_isServerMode) {
      // FIX-CORRUPT-BACKUP (audit 2026-06-10) : un blob smsi_data corrompu était chargé
      // en silence (app vide sans explication) puis écrasé définitivement à la première
      // sauvegarde. Désormais : téléchargement de secours du blob brut + toast erreur
      // persistant, et plus de return prématuré qui sautait la normalisation post-chargement.
      var _rawSaved = null;
      try {
        _rawSaved = localStorage.getItem("smsi_data");
      } catch(e) {
        void 0;
      }
      if (_rawSaved) {
        var _smsiCorrupted = false;
        try {
          var parsed = JSON.parse(_rawSaved);
          if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
            void 0;
            _smsiCorrupted = true;
          } else {
            appData = parsed;
            window.appData = appData;
          }
        } catch(e) {
          void 0;
          _smsiCorrupted = true;
        }
        if (_smsiCorrupted) {
          // Copie de secours du blob d'origine AVANT qu'une sauvegarde puisse l'écraser
          downloadJsonBackup(_rawSaved, 'smsi_backup_corrupted.json');
          setTimeout(function() {
            if (typeof showToast2 === 'function') {
              showToast2(
                t('toasts.dataCorrupted',
                  'Données locales corrompues ou illisibles : une copie de secours (smsi_backup_corrupted.json) a été téléchargée. L\'application a démarré vide — ne sauvegardez pas avant d\'avoir mis cette copie à l\'abri.'),
                'error', 600000);
            }
          }, 500);
        }
      }
    }

    // ──────────────────────────────────────────────────────────
    // POST-LOAD : normalisation des données (commun aux deux modes)
    // ──────────────────────────────────────────────────────────
    try {
        if (typeof window.validateBeforeSave === 'function') {
          try {
            var loadValidation = window.validateBeforeSave(appData);
            if (!loadValidation.valid) {
              void 0;
            }
          } catch(e) { void 0; }
        }
        if (!appData.actionsHistory) appData.actionsHistory = {};
        // FIX-KPIHISTORY: garantir les sous-clés attendues par les sparklines du dashboard
        // (bundle.js:3174-3178). Sans ces arrays, drawSparkline crash sur data.length undefined.
        if (!appData.kpiHistory || typeof appData.kpiHistory !== 'object') appData.kpiHistory = {};
        if (!Array.isArray(appData.kpiHistory.conformity)) appData.kpiHistory.conformity = [];
        if (!Array.isArray(appData.kpiHistory.controls))   appData.kpiHistory.controls   = [];
        if (!Array.isArray(appData.kpiHistory.actions))    appData.kpiHistory.actions    = [];
        if (!Array.isArray(appData.kpiHistory.risks))      appData.kpiHistory.risks      = [];
        if (!Array.isArray(appData.kpiHistory.nc))         appData.kpiHistory.nc         = [];
        if (!appData.reviews) appData.reviews = [];
        if (!appData.stakeholders) appData.stakeholders = [];
        if (!appData.documents) appData.documents = [];
        if (!appData.documentReview) appData.documentReview = createDefaultDocumentReview();
        else appData.documentReview = createDefaultDocumentReview(appData.documentReview);
        if (!appData.projectRisks) appData.projectRisks = [];
        if (!appData.riskAnalyses) appData.riskAnalyses = [];
        if (!appData.riskThresholds) appData.riskThresholds = null;
        if (!appData.auditLog) appData.auditLog = [];
        if (!appData.impactMatrices) appData.impactMatrices = null;
        if (!appData.threats) appData.threats = [];
        var _fwEnabled = (appData.frameworkMapper && appData.frameworkMapper.enabledFrameworks) || _defaultEnabledFrameworks();
        if (_fwEnabled.indexOf('nis2') >= 0 || _fwEnabled.indexOf('cyfun2025') >= 0) {
          // Detect and migrate legacy 23-control format on load
          var _legacyFmt = detectLegacyNis2Format(appData.nis2Controls);
          if (_legacyFmt === 'legacy_23') {
            void 0;
            var _legacyMaturity = migrateLegacyNis2Controls(appData.nis2Controls);
            appData.cyfunLevel = appData.cyfunLevel || 'basic';
            var _cyfLevel2 = appData.cyfunLevel;
            if (typeof window.rebuildCyfunCategories === 'function') window.rebuildCyfunCategories(_cyfLevel2);
            rebuildCategoryLabels();
            appData.nis2Controls = loadNis2Controls();
            // Apply migrated maturity values
            appData.nis2Controls.forEach(function(c) {
              if (_legacyMaturity[c.categoryId]) {
                c.docMaturity = _legacyMaturity[c.categoryId];
                c.implMaturity = _legacyMaturity[c.categoryId];
              }
            });
            if (typeof showToast === 'function') {
              setTimeout(function() { showToast(t('toasts.nis2Migration', 'Migration vers NIS2 terminée. Veuillez vérifier vos scores.'), 'info'); }, 1000);
            }
            // FIX-NIS2-MIGRATION-PERSIST: persister la migration au serveur pour ne pas la
            // redéclencher à chaque chargement. Sans ce push, server-data.json garde les 23
            // items legacy et la détection legacy_23 se relance sans fin → toast récurrent.
            // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
            if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode &&
                window.CSI_SyncManager.isServerMode() && window.CSI_SyncManager.forceServerSync) {
              setTimeout(function() {
                // FIX-MIGRATION-LOCK (revue 2026-06-11) : avec EDIT_LOCK_ENFORCED, un push
                // hors mode édition est refusé (423) → la migration (et son toast) se
                // rejouait à chaque chargement. On acquiert brièvement le verrou : si un
                // autre utilisateur édite, on n'insiste pas (lui ou le prochain éditeur
                // persistera la migration).
                var _doMigrationPush = function(releaseAfter) {
                  window.CSI_SyncManager.forceServerSync(['nis2Controls', '_cyfunEvalArchive', 'cyfunLevel', 'cyfunLang', 'targetMaturity']).then(function(r) {
                    if (r && r.success) void 0;
                    else void 0;
                  }).catch(function(e) {
                    void 0;
                  }).then(function() {
                    if (releaseAfter && window.CSI_EditLockManager && window.CSI_EditLockManager.release) {
                      window.CSI_EditLockManager.release().catch(function() {});
                    }
                  });
                };
                var ELM = window.CSI_EditLockManager;
                var _alreadyEditing = !(typeof isReadOnlyMode === 'function' && isReadOnlyMode());
                if (_alreadyEditing || !ELM || !ELM.acquire) {
                  _doMigrationPush(false);
                } else {
                  ELM.acquire().then(function(res) {
                    if (res && res.granted) _doMigrationPush(true);
                    else void 0;
                  }).catch(function() {
                    void 0;
                  });
                }
              }, 2500);
            }
          } else {
            // Always rebuild CyFun categories for the current level
            var _cyfLevel = appData.cyfunLevel || 'basic';
            if (typeof window.rebuildCyfunCategories === 'function') window.rebuildCyfunCategories(_cyfLevel);
            rebuildCategoryLabels();
            if (!Array.isArray(appData.nis2Controls) || appData.nis2Controls.length === 0) appData.nis2Controls = loadNis2Controls();
          }
        } else {
          appData.nis2Controls = appData.nis2Controls || [];
        }
        // ReCyF (France) — degré + référentiel actif ; migre les mesures vers l'ensemble complet
        // (152) en préservant les évaluations par réf. Le degré ne filtre que l'affichage/score,
        // jamais le stockage (aucune perte d'évaluation lors d'un changement de degré).
        if (!appData.recyfLevel) appData.recyfLevel = 'importante';
        if (!appData.nis2Referential) appData.nis2Referential = 'cyfun2025';
        if (typeof window.rebuildRecyfThemes === 'function') window.rebuildRecyfThemes(appData.recyfLevel);
        if (!Array.isArray(appData.recyfControls)) appData.recyfControls = [];
        // Construit/complète les 152 mesures si ReCyF est le référentiel NIS2 actif OU si des
        // évaluations existent déjà (n'alourdit pas les installations 100 % CyFun). Garantit
        // que la table ReCyF a ses données dès le chargement quand la France est sélectionnée.
        if ((appData.recyfControls.length > 0 || appData.nis2Referential === 'recyf2025') && typeof ensureRecyfControls === 'function') ensureRecyfControls();
        if (!appData.soa) appData.soa = [];
        if (typeof appData.nextActionId !== "number") appData.nextActionId = 0;
        if (!appData.title) appData.title = "Cyber-Assistant";
        if (!appData.targetMaturity) {
          appData.targetMaturity = {
            iso27002: 3,
            nis2: Object.assign({}, DEFAULT_NIS2_MATURITY),
            nis2Target: 0
          };
        } else {
          const tm = appData.targetMaturity;
          if (typeof tm.iso27002 === "object") {
            const values = Object.values(tm.iso27002)
              .map(v => parseInt(v, 10))
              .filter(v => !isNaN(v));
            tm.iso27002 = values.length
              ? Math.round(values.reduce((a, b) => a + b, 0) / values.length)
              : 3;
          } else if (typeof tm.iso27002 !== "number") {
            const v = parseInt(tm.iso27002, 10);
            tm.iso27002 = isNaN(v) ? 3 : v;
          }
          tm.nis2 = buildNis2Maturity(tm.nis2 || {});
          const hasNonZeroNis2 = Object.values(tm.nis2 || {}).some(v => typeof v === "number" && v > 0);
          const rawNis2Target = tm.nis2Target;
          const parsedNis2Target = typeof rawNis2Target === "number"
            ? rawNis2Target
            : parseInt(rawNis2Target, 10);
          tm.nis2Target = Number.isFinite(parsedNis2Target) ? parsedNis2Target : (hasNonZeroNis2 ? 3 : 0);
          if (tm.nis2Target < 0) tm.nis2Target = 0;
          if (!tm.iso27002 || tm.iso27002 <= 0) tm.iso27002 = 3;
        }
        // Framework mapper initialization
        if (!appData.frameworkMapper) {
          appData.frameworkMapper = JSON.parse(JSON.stringify(window.FRAMEWORK_MAPPER_DEFAULTS || { enabledFrameworks: _defaultEnabledFrameworks(), customMappings: [], userNotes: {} }));
        }
        // Normalize enabledFrameworks: ensure the controls framework ID matches the loaded edition
        // (fixes stale saved data referencing 'iso27002' in Community or 'open-security-controls' in Corporate)
        (function _normalizeEnabledFws() {
          var ef = appData.frameworkMapper.enabledFrameworks;
          if (!Array.isArray(ef)) return;
          var fws = window._csiFrameworks || {};
          var activeCtrl = _getActiveControlsFrameworkId();
          var activeMgmt = _getActiveManagementFrameworkId();
          // Replace iso27002 <-> open-security-controls depending on what's loaded
          var ctrlPairs = [['iso27002', 'open-security-controls']];
          var mgmtPairs = [['iso27001', 'open-security-management']];
          [ctrlPairs, mgmtPairs].forEach(function(pairs) {
            pairs.forEach(function(pair) {
              var absent = !fws[pair[0]] ? pair[0] : (!fws[pair[1]] ? pair[1] : null);
              var present = absent === pair[0] ? pair[1] : pair[0];
              if (!absent) return; // both loaded (Corporate has all)
              var idx = ef.indexOf(absent);
              if (idx >= 0) {
                // Replace with the loaded variant if not already present
                if (ef.indexOf(present) < 0) {
                  ef[idx] = present;
                } else {
                  ef.splice(idx, 1);
                }
              }
            });
          });
        })();
        // Forcer les frameworks de l'édition (écrase les données sauvegardées)
        var _edForce = window._csiEdition;
        if (_edForce && _edForce.forcedFrameworks) {
          appData.frameworkMapper.enabledFrameworks = _defaultEnabledFrameworks();
        }
        // Modèle NIS2 mutuellement exclusif : aligner enabledFrameworks sur le référentiel NIS2
        // actif dès le chargement. Auto-répare un état sauvegardé/edition obsolète où le
        // référentiel choisi (ex. ReCyF/France) n'apparaissait pas dans la liste activée — cause
        // du symptôme « aucun onglet NIS2 affiché alors que la France est sélectionnée ».
        if (typeof reconcileNis2Frameworks === 'function') reconcileNis2Frameworks();
        if (window.CSI_FrameworkMapper && typeof CSI_FrameworkMapper.onAppDataLoaded === 'function') {
          CSI_FrameworkMapper.onAppDataLoaded();
        }
        if (_fwEnabled.indexOf('nis2') >= 0 || _fwEnabled.indexOf('cyfun2025') >= 0) ensureNis2Plan();
        ensureCriticalAssets();
        ensureProjectRisks();
        appData.soclePillars = normalizeSoclePillars(appData.soclePillars);
        if (Array.isArray(appData.controls)) {
          const mapNumToLevel = { 1: "Initial", 2: "Répétable", 3: "Défini", 4: "Géré", 5: "Optimisé" };
          const legacyCategories = {
            "Organisationnelle": "Organisationnel",
            "Organisationnelles": "Organisationnel",
            "Organisationnels": "Organisationnel",
            "Humaines": "Humain",
            "Humaine": "Humain",
            "Humains": "Humain",
            "Physiques": "Physique",
            "Physique": "Physique",
            "Technologiques": "Technologique",
            "Technologique": "Technologique",
            "Techniques": "Technologique",
            "Technique": "Technologique",
            "Autre": "Non classé",
            "Autres": "Non classé"
          };
          const validCategories = new Set([...Object.values(DOMAIN_BY_PREFIX), "Autre", "Non classé"]);
          const needsSoaMigration = appData.soa.length === 0;
          (appData.controls || []).forEach(c => {
            if (typeof c.cmm === "undefined") {
              c.cmm = "Initial";
            } else if (typeof c.cmm === "number") {
              c.cmm = mapNumToLevel[c.cmm] || "Initial";
            }
            if (needsSoaMigration && (c.selected !== undefined || c.justification !== undefined)) {
              appData.soa.push({
                controlId: c.id,
                applicable: c.selected !== undefined ? c.selected : true,
                justification: c.justification || ""
              });
            }
            delete c.selected;
            delete c.justification;
            if (typeof c.justificationControl === "undefined") c.justificationControl = "";
            delete c.maturity;
            if (typeof c.category === "string") {
              let cat = c.category.trim();
              if (cat === "ISO27002") {
                c.category = "Non classé";
              } else if (legacyCategories[cat]) {
                c.category = legacyCategories[cat];
              } else if (!validCategories.has(cat)) {
                c.category = "Non classé";
              } else {
                c.category = cat;
              }
            } else {
              c.category = "Non classé";
            }
            if (!c.category || c.category === "Non classé") {
              c.category = inferIso27002Category(c);
            }
            if (typeof c.subcategory === "string") {
              c.subcategory = [c.subcategory];
            }
            if (Array.isArray(c.subcategory)) {
              c.subcategory = c.subcategory.map(sc => normalizeCapabilityKey(sc));
            }
            if (!c.subcategory || (Array.isArray(c.subcategory) && c.subcategory.length === 0)) {
              const numero = String(c.numero || "").trim();
              const title = String(c.title || "").trim();
              const match = iso27002_data_default.find(
                item => item.numero === numero || item.titre === title
              );
              if (match) {
                if (Array.isArray(match.capability) && match.capability.length > 0) {
                  c.subcategory = match.capability.map(sc => normalizeCapabilityKey(sc));
                } else if (typeof match.capability === "string" && match.capability) {
                  c.subcategory = [normalizeCapabilityKey(match.capability)];
                }
              }
              if (!c.subcategory || (Array.isArray(c.subcategory) && c.subcategory.length === 0)) {
                c.subcategory = [];
              }
            }
          });
        }
        if (Array.isArray(appData.actions)) {
          appData.actions.forEach(a => {
            // Unification 2026-07-10 : conversion legacy string -> [{text,date}] au lieu
            // d'écraser en [] (perte des commentaires d'actions des très anciens exports).
            a.comments = window.normalizeComments(a.comments);
          });
        }
        if (Array.isArray(appData.risks)) {
          appData.risks.forEach(r => {
            r.comments = window.normalizeComments(r.comments);
            r.score = (r.impact || 1) * (r.probability || 1);
            r.level = calculateRiskLevel(r.score);
          });
        }
        ensureRiskIds();
        if (Array.isArray(appData.reviews)) {
          appData.reviews.forEach(r => {
            r.comments = window.normalizeComments(r.comments);
          });
        }
        if (Array.isArray(appData.audits)) {
          appData.audits.forEach(a => {
            if (!a.type) a.type = "interne";
          });
        }
        if (Array.isArray(appData.nonconformities)) {
          appData.nonconformities.forEach(nc => {
            if (!nc.auditId) nc.auditId = "";
          });
        }
        // loadThreats() removed — normalizeThreats() at line ~9018 handles normalization
        normalizeCriticalAssetLinks(appData.actions);
        normalizeCriticalAssetLinks(appData.risks);
        normalizeCriticalAssetLinks(appData.threats);
        normalizeCriticalAssetLinks(appData.nonconformities);

        // Normalisation complète de toutes les sections pour compatibilité ascendante
        if (typeof normalizeSoa === 'function') appData.soa = normalizeSoa(appData.soa || []);
        if (typeof normalizeActions === 'function') appData.actions = normalizeActions(appData.actions || []);
        if (typeof normalizeRisks === 'function') appData.risks = normalizeRisks(appData.risks || []);
        if (typeof normalizeThreats === 'function') appData.threats = normalizeThreats(appData.threats || []);
        if (typeof normalizeNonconformities === 'function') appData.nonconformities = normalizeNonconformities(appData.nonconformities || []);
        if (typeof normalizeAudits === 'function') appData.audits = normalizeAudits(appData.audits || []);
        if (typeof normalizeKpis === 'function') appData.kpis = normalizeKpis(appData.kpis || []);
        if (typeof normalizeReviews === 'function') appData.reviews = normalizeReviews(appData.reviews || []);
        if (typeof normalizeStakeholders === 'function') appData.stakeholders = normalizeStakeholders(appData.stakeholders || []);
        if (typeof normalizeDocuments === 'function') appData.documents = normalizeDocuments(appData.documents || []);
        if (typeof normalizeControls === 'function') appData.controls = normalizeControls(appData.controls || []);
        // Enrich existing saved controls with localized fields from framework definition
        var _fwDef = _getActiveControlsFramework();
        var _fwCtrls = _fwDef && _fwDef.controls ? _fwDef.controls : [];
        if (_fwCtrls.length > 0) {
          (appData.controls || []).forEach(function(c) {
            var fwCtrl = _fwCtrls.find(function(fc) { return fc.ref === c.numero; });
            if (fwCtrl) {
              if (fwCtrl.title) { c.title = fwCtrl.title; c.reference = c.numero + ' ' + fwCtrl.title; }
              if (fwCtrl.description) { c.description = fwCtrl.description; c.measure = fwCtrl.description; }
              if (!c.titleEn) c.titleEn = fwCtrl.titleEn || '';
              if (!c.descriptionEn) c.descriptionEn = fwCtrl.descriptionEn || '';
            }
          });
        }

      // Vérifier si des données ont été chargées (serveur ou local)
      // FIX-SERVER-PULL-AUTHORITY : un pull serveur non vide fait autorité (voir plus haut).
      var _hasLoadedData = (appData && appData.controls && appData.controls.length > 0) || _serverPullHasData;
      if (!_hasLoadedData) {
        await initializeDefaultData();
      } else if (_serverPullHasData) {
        // SI serveur partiellement initialisé : garantir la FORME des collections essentielles
        // (tableaux/objets vides) SANS écraser ce que le serveur a renvoyé.
        ['controls', 'soa', 'nis2Controls', 'recyfControls', 'actions', 'risks', 'threats',
         'nonconformities', 'audits', 'kpis', 'reviews', 'stakeholders', 'documents',
         'documentReview', 'criticalAssets', 'projectRisks', 'soclePillars'].forEach(function(_k) {
          if (!Array.isArray(appData[_k])) appData[_k] = [];
        });
        if (typeof appData.nextActionId !== 'number') appData.nextActionId = (appData.actions || []).length;
        if (!appData.title) appData.title = 'Cyber-Assistant';
      }
    } catch (error) {
      void 0;
      await initializeDefaultData();
    }
      // PASS2-008: Don't block on validation errors during initial load — save silently
      // En mode serveur, pas de save initial (les données viennent du serveur, pas besoin de les repusher)
      if (!_isServerMode) {
        try { saveData(); } catch(e) { void 0; }
      }
      loadControlsTable();
      loadNis2ControlsTable();
      // updateDashboard() is called later via initDashboard() in the main init sequence
    }
  function ensureNis2Plan() {
    appData.nis2Plan = normalizeNis2Plan(appData.nis2Plan);
    // Normalize legacy budget field: if 'budget' exists but 'annualBudget' does not, copy it
    if (appData.nis2Plan) {
      if (appData.nis2Plan.budget && !appData.nis2Plan.annualBudget) {
        appData.nis2Plan.annualBudget = appData.nis2Plan.budget;
      }
    }
  }
  async function initializeDefaultData() {
    appData.controls = await loadISO27002Controls();
    appData.soa = (appData.controls || []).map(c => ({ controlId: c.id, applicable: true, justification: "" }));
    appData.cyfunLevel = appData.cyfunLevel || 'basic';
    appData.nis2Controls = loadNis2Controls();
    appData.nis2Plan = getEmptyNis2Plan();
    appData.soclePillars = cloneDefaultSoclePillars();
    appData.title = "Cyber-Assistant";
    appData.actions = [];
    appData.criticalAssets = [];
    appData.criticalAssetsMeta = getDefaultCriticalAssetsMeta();
    appData.projectRisks = [];
    appData.risks = [];
    appData.nonconformities = [];
    appData.audits = [];
    appData.kpis = [];
    appData.reviews = [];
      appData.stakeholders = [];
      appData.documents = [];
      appData.documentReview = createDefaultDocumentReview();
      appData.threats = [];
        appData.targetMaturity = {
          iso27002: 3.5,
          nis2: buildNis2Maturity(),
          nis2Target: 0
        };
      appData.nextActionId = 0;
      saveData();
    }
async function loadISO27002Controls() {
  // Look up bilingual data from the framework definition if available
  var fwDef = _getActiveControlsFramework();
  var fwControls = fwDef && fwDef.controls ? fwDef.controls : [];
  return iso27002_data_default.map((item, index) => {
    // Match framework control by ref number for localized fields
    var fwCtrl = fwControls.find(function(c) { return c.ref === item.numero; });
    var displayTitle = (fwCtrl && fwCtrl.title) ? fwCtrl.title : item.titre;
    var displayDesc = (fwCtrl && fwCtrl.description) ? fwCtrl.description : item["mesure de s\xE9curit\xE9"];
    return {
      id: `CTRL-${String(index + 1).padStart(3, "0")}`,
      reference: item.numero ? `${item.numero} ${displayTitle}` : displayTitle,
      numero: item.numero || String(index + 1),
      title: displayTitle,
      titleEn: fwCtrl ? fwCtrl.titleEn || '' : '',
      measure: displayDesc,
      description: displayDesc,
      descriptionEn: fwCtrl ? fwCtrl.descriptionEn || '' : '',
      recommendations: item.recommandations || [],
      category: DOMAIN_BY_PREFIX[item.numero?.split('.')[0]] || "Non classé",
      subcategory: Array.isArray(item.capability)
        ? item.capability.map(c => normalizeCapabilityKey(c))
        : [normalizeCapabilityKey(item.capability)],
      status: "non_implemente",
      cmm: "Initial",
      comments: "",
      evidence: "",
      justificationControl: ""
    };
  });
}

function loadNis2Controls() {
  // Load CyFun 2025 requirements for the current assurance level
  var level = appData.cyfunLevel || 'basic';
  var fw = (window._csiFrameworks || {})['cyfun2025'];
  if (!fw || !fw.requirements) {
    void 0;
    return [];
  }
  // Rebuild categories and labels
  if (typeof window.rebuildCyfunCategories === 'function') {
    window.rebuildCyfunCategories(level);
  }
  rebuildCategoryLabels();

  var levelOrder = { basic: 1, important: 2, essential: 3 };
  var maxOrder = levelOrder[level] || 1;
  var requirements = fw.requirements.filter(function(r) {
    return (levelOrder[r.level] || 99) <= maxOrder;
  });
  return requirements.map(function(req, index) {
    var funcObj = (fw.meta.functions || []).find(function(f) { return f.id === req.functionId; });
    var catObj = (fw.meta.categories || []).find(function(c) { return c.id === req.categoryId; });
    // Trilingue : textes snapshotés dans la langue CyFun courante (helpers FEAT-CYFUN-NL)
    var displayTitle = cyfunReqText(req, 'title');
    var displayDesc = cyfunReqText(req, 'description');
    var funcLabel = funcObj ? (cyfunLabelText(funcObj) + ' (' + funcObj.id + ')') : req.functionId;
    var catLabel = catObj ? (cyfunLabelText(catObj) + ' (' + catObj.id + ')') : req.categoryId;
    return {
      id: 'CYFUN-' + String(index + 1).padStart(3, '0'),
      ref: req.ref,
      title: displayTitle,
      titleEn: req.title || '',
      description: displayDesc,
      descriptionEn: req.description || '',
      functionId: req.functionId,
      functionLabel: funcLabel,
      categoryId: req.categoryId,
      categoryLabel: catLabel,
      subcategoryRef: req.subcategoryRef || '',
      level: req.level,
      isKeyMeasure: req.isKeyMeasure || false,
      isManagementLinked: req.isManagementLinked || false,
      docMaturity: 0,
      implMaturity: 0,
      justification: '',
      evidence: '',
      // Legacy compat
      fonction: funcLabel,
      categorie: catLabel,
      status: 'non_implemente',
      cmm: 'Initial'
    };
  });
}

  /**
   * Initialize data for a framework when activated via Settings.
   * Ensures controls, plan and target maturity exist.
   */
  function initFrameworkData(frameworkId) {
    if (frameworkId === 'nis2') {
      // Detect and migrate legacy 23-control format
      var format = detectLegacyNis2Format(appData.nis2Controls);
      if (format === 'legacy_23') {
        void 0;
        var legacyMaturity = migrateLegacyNis2Controls(appData.nis2Controls);
        appData.cyfunLevel = appData.cyfunLevel || 'basic';
        appData.nis2Controls = loadNis2Controls();
        // Apply migrated maturity values
        appData.nis2Controls.forEach(function(c) {
          if (legacyMaturity[c.categoryId]) {
            var val = legacyMaturity[c.categoryId];
            c.docMaturity = val;
            c.implMaturity = val;
          }
        });
        if (typeof showToast === 'function') {
          showToast(t('toasts.nis2Migration', 'Migration vers NIS2 terminée. Veuillez vérifier vos scores.'), 'info');
        }
      } else if (format === 'empty' || !Array.isArray(appData.nis2Controls) || appData.nis2Controls.length === 0) {
        appData.cyfunLevel = appData.cyfunLevel || 'basic';
        appData.nis2Controls = loadNis2Controls();
      } else if (format === 'cyfun') {
        // Already in CyFun format, just ensure categories are rebuilt
        var level = appData.cyfunLevel || 'basic';
        if (typeof window.rebuildCyfunCategories === 'function') {
          window.rebuildCyfunCategories(level);
        }
        rebuildCategoryLabels();
      }
      if (!appData.nis2Plan) appData.nis2Plan = getEmptyNis2Plan();
      ensureNis2Plan();
      if (!appData.targetMaturity) appData.targetMaturity = {};
      if (!appData.targetMaturity.nis2) {
        appData.targetMaturity.nis2 = {};
        CYFUN_CATEGORIES.forEach(function(c) { appData.targetMaturity.nis2[c] = 0; });
        appData.targetMaturity.nis2Target = 0;
      }
      recalcNis2Maturity();
    }
    // Future frameworks: add else-if here
  }

  function getLocalStorageUsage() {
    let totalSize = 0;
    for (let key in localStorage) {
      if (localStorage.hasOwnProperty(key)) {
        totalSize += (localStorage[key].length + key.length) * 2; // UTF-16 = 2 bytes per char
      }
    }
    return totalSize;
  }

  function checkLocalStorageQuota(dataSize) {
    const QUOTA_WARNING_THRESHOLD = 4 * 1024 * 1024; // 4MB warning
    const QUOTA_LIMIT = 5 * 1024 * 1024; // 5MB typical limit
    const currentUsage = getLocalStorageUsage();
    // AUDIT-M4: Soustraire la taille actuelle de smsi_data car elle sera REMPLACÉE, pas ajoutée.
    // Avant: projectedUsage = currentUsage + dataSize incluait l'ancien smsi_data,
    // causant des faux positifs de quota exceeded (ex: 3.8MB existant + 3.8MB nouveau = 7.6MB
    // alors que l'usage réel resterait 3.8MB après remplacement).
    var existingSmsiSize = 0;
    try {
      var existingSmsi = localStorage.getItem("smsi_data");
      if (existingSmsi) existingSmsiSize = (existingSmsi.length + "smsi_data".length) * 2;
    } catch(e) { /* ignore */ }
    const projectedUsage = (currentUsage - existingSmsiSize) + dataSize;

    if (projectedUsage > QUOTA_LIMIT) {
      return { ok: false, usage: currentUsage, projected: projectedUsage, limit: QUOTA_LIMIT };
    }
    if (projectedUsage > QUOTA_WARNING_THRESHOLD) {
      return { ok: true, warning: true, usage: currentUsage, projected: projectedUsage, limit: QUOTA_LIMIT };
    }
    return { ok: true, warning: false, usage: currentUsage, projected: projectedUsage, limit: QUOTA_LIMIT };
  }

  function refreshStorageHealth() {
    // FIX-STORAGEHEALTH-MODE (audit 2026-06-10) : garde aligné sur l'édition/licence
    // (isServerModeUnlocked) et non sur le protocole — une édition locale servie en http(s)
    // conserve ses données en localStorage et doit donc afficher la jauge de stockage.
    if (typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()) return;
    const usageText = document.getElementById("storageUsageText");
    const usageBar = document.getElementById("storageUsageBar");
    const datasetSizeEl = document.getElementById("storageDatasetSize");
    const lastSaveEl = document.getElementById("storageLastSave");
    const warningEl = document.getElementById("storageHealthWarning");
    if (!usageText && !usageBar && !datasetSizeEl && !lastSaveEl && !warningEl) return;

    let usage = 0;
    let limit = 5 * 1024 * 1024;
    let warn = false;
    try {
      const quota = checkLocalStorageQuota(0);
      usage = quota.usage || 0;
      limit = quota.limit || limit;
      warn = !!quota.warning;
    } catch (e) {
      try {
        usage = getLocalStorageUsage();
      } catch (err) {
        usage = 0;
      }
    }

    const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 5 * 1024 * 1024;
    const percent = safeLimit ? Math.min(100, Math.round((usage / safeLimit) * 100)) : 0;
    if (usageText) {
      usageText.textContent = `${formatSnapshotSize(usage)} / ${formatSnapshotSize(safeLimit)} (${percent}%)`;
    }
    if (usageBar) {
      usageBar.style.width = `${percent}%`;
      usageBar.classList.toggle("storage-meter__bar--warn", percent >= 80);
      usageBar.classList.toggle("storage-meter__bar--danger", percent >= 95);
    }

    let dataSize = 0;
    try {
      const saved = localStorage.getItem("smsi_data") || "";
      dataSize = saved.length * 2;
    } catch (e) {
      dataSize = 0;
    }
    if (datasetSizeEl) datasetSizeEl.textContent = formatSnapshotSize(dataSize);

    let lastSaveRaw = "";
    try {
      lastSaveRaw = localStorage.getItem("smsi_last_save") || "";
    } catch (e) {
      lastSaveRaw = "";
    }
    let lastSaveText = t('settings.never', 'Jamais');
    if (lastSaveRaw) {
      const ts = Date.parse(lastSaveRaw);
      if (Number.isFinite(ts)) lastSaveText = new Date(ts).toLocaleString();
    }
    if (lastSaveEl) lastSaveEl.textContent = lastSaveText;

    if (warningEl) {
      const nearLimit = warn || percent >= 80;
      if (nearLimit) {
        warningEl.textContent = t('settings.storageFull', 'Stockage presque plein : exportez vos données ou supprimez des snapshots.');
        warningEl.hidden = false;
      } else {
        warningEl.hidden = true;
      }
    }
  }

  // ============================================================
  // SERVER-ONLY MODE: Push helper with retry + beforeunload guard
  // ============================================================
  window._csiUnsavedServerData = false;

  /**
   * Push des données d'un tab au serveur avec retry (backoff exponentiel).
   * @param {string} tabId
   * @param {Object} tabData
   * @param {Object} lockMgr
   * @param {number} maxRetries
   * @returns {Promise<boolean>} true si push réussi
   */
  // FIX-D: Prevent concurrent push operations (multiple saveData calls in flight)
  var _pushInProgress = false;
  // FIX-PUSH-QUEUE (audit 2026-06-10, bundle.js:12710) : un push demandé pendant qu'un
  // autre était en vol était silencieusement JETÉ (mutex), puis le pull au retour sur
  // l'onglet écrasait les modifications locales jamais poussées — perte définitive.
  // Désormais : les onglets modifiés sont suivis (_csiDirtyTabs), un push refusé par le
  // mutex est rejoué automatiquement à la fin du push en vol (_pendingPushTabs, données
  // re-extraites FRAÎCHES d'appData au moment du rejeu), et le pull d'onglet ne remplace
  // jamais les clés d'un onglet encore « dirty » (voir loadModuleContent).
  var _csiDirtyTabs = {};     // tabId -> true : modifs locales non confirmées par le serveur
  var _pendingPushTabs = {};  // tabId -> true : push à rejouer dès la fin du push en vol
  // FIX-OPTIMISTIC-REV (2026-06-11) : numéro de révision serveur connu par onglet. Envoyé
  // comme baseRev au push ; mis à jour à chaque pull/push réussi. Permet au serveur de
  // détecter une écriture basée sur une version périmée (409) → parachute (pas d'écrasement).
  var _tabRevisions = {};     // tabId -> dernier _rev serveur connu
  var GLOBAL_META_TAB = 'global-meta';
  var GLOBAL_META_KEYS = ['auditLog', 'kpiHistory'];

  function _updateUnsavedFlag() {
    window._csiUnsavedServerData = Object.keys(_csiDirtyTabs).length > 0;
  }

  function _extractTabData(tabId) {
    if (!window.appData) return null;
    var keys;
    if (tabId === GLOBAL_META_TAB) {
      keys = GLOBAL_META_KEYS;
    } else {
      var mgr = window.CSI_PresenceManager || window.CSI_LockManager;
      keys = mgr && mgr.getTabDataKeys ? mgr.getTabDataKeys()[tabId] : null;
    }
    if (!keys) return null;
    var data = {};
    keys.forEach(function(key) { data[key] = window.appData[key]; });
    return data;
  }

  function _drainPendingPushes() {
    var tabs = Object.keys(_pendingPushTabs);
    if (!tabs.length) return;
    var tabId = tabs[0];
    delete _pendingPushTabs[tabId];
    var data = _extractTabData(tabId);
    if (data) {
      _serverDataPushWithRetry(tabId, data, window.CSI_PresenceManager || window.CSI_LockManager, 3);
    } else {
      delete _csiDirtyTabs[tabId];
      _updateUnsavedFlag();
      _drainPendingPushes();
    }
  }

  /**
   * FIX-PUSH-QUEUE : re-pousse tous les onglets dirty (filet appelé par l'autosave).
   */
  function _retryDirtyServerPushes() {
    if (!(typeof isServerModeUnlocked === 'function' && isServerModeUnlocked())) return;
    if (!window.CSI_AuthManager || !window.CSI_AuthManager.isLoggedIn()) return;
    var dirty = Object.keys(_csiDirtyTabs);
    if (!dirty.length) return;
    dirty.forEach(function(tb) { _pendingPushTabs[tb] = true; });
    if (!_pushInProgress) _drainPendingPushes();
  }

  /**
   * FIX-LOCK-RELEASE-RACE (audit 2026-06-10, bundle.js:12732) : attend la fin de tous les
   * push (en vol + en attente + dirty) avant de rendre la main — utilisé à la sortie du
   * mode édition pour ne pas libérer le verrou pendant qu'un push est encore en route.
   * Résout false au-delà du timeout (on ne retient pas le verrou indéfiniment).
   */
  function _flushServerPushes(timeoutMs) {
    return new Promise(function(resolve) {
      var deadline = Date.now() + (timeoutMs || 15000);
      (function tick() {
        var idle = !_pushInProgress
          && Object.keys(_pendingPushTabs).length === 0
          && Object.keys(_csiDirtyTabs).length === 0;
        if (idle) return resolve(true);
        if (Date.now() > deadline) return resolve(false);
        if (!_pushInProgress) {
          Object.keys(_csiDirtyTabs).forEach(function(tb) { _pendingPushTabs[tb] = true; });
          _drainPendingPushes();
        }
        setTimeout(tick, 300);
      })();
    });
  }

  // FIX-OPTIMISTIC-REV (2026-06-11) : l'import full (POST sans action) purge tous les
  // tab_*.json côté serveur → les révisions repartent de 0. Le client doit oublier ses
  // révisions mémorisées (sinon un baseRev élevé > révision serveur réinitialisée
  // empêcherait la détection de conflit). Appelé par sync-manager après chaque full-sync.
  window._csiResetTabRevisions = function() { _tabRevisions = {}; };

  // === CSI-ME === Abandonne toute écriture en file. Utilisé par la bascule de
  // périmètre APRÈS un flush réussi (ou en consultation où rien n'est légitimement
  // en attente) : on re-pull l'état autoritaire, donc l'état RAM courant est
  // remplacé — laisser des onglets « dirty » ferait boucler l'autosave en 423.
  window._csiDiscardPendingPushes = function() {
    _csiDirtyTabs = {};
    _pendingPushTabs = {};
    if (typeof _updateUnsavedFlag === 'function') _updateUnsavedFlag();
  };

  // === CSI-ME === Bascule de périmètre (compte principal). Passe en CONSULTATION
  // (aucun verrou pris — le verrou n'est acquis qu'à l'entrée en édition explicite,
  // décision PO), puis re-pull autoritaire de la partition cible + re-render.
  // Sécurité anti-perte : si on ÉDITAIT (verrou détenu), on flushe les écritures
  // AVANT de relâcher le verrou (tant qu'on peut encore pousser) ; un flush qui
  // échoue interrompt la bascule (données préservées). En consultation, il n'y a
  // rien de légitime à pousser → on abandonne la file résiduelle.
  // entityId : id de sous-entité, ou null/'' pour revenir au périmètre principal.
  window._csiSwitchScope = async function(entityId) {
    var EM = window.CSI_EntityManager;
    // === CSI-ME === (audit M5) accepte le compte principal ET un compte de sous-entité
    // MULTI-APPARTENANCE (sélecteur « Mon entité »). setActiveScope re-valide l'appartenance
    // (refuse tout id hors entityIds pour une sous-entité) → bascule sûre dans les deux cas.
    if (!EM || !EM.isMultiEntity() || (!EM.isGroupUser() && !EM.isSubEntityUser())) return { ok: false, reason: 'not-multi' };
    // === CSI-ME === (audit, mineur) garde de réentrance : deux bascules concurrentes
    // (double clic / sélecteur + tuile) se marcheraient dessus (re-pull entrelacés).
    if (window._csiScopeSwitching) return { ok: false, reason: 'busy' };
    var wasEditing = (typeof isReadOnlyMode === 'function') ? !isReadOnlyMode() : false;
    window._csiScopeSwitching = true;
    try {
      return await _csiDoSwitchScope(EM, entityId, wasEditing);
    } finally {
      window._csiScopeSwitching = false;
    }
  };

  async function _csiDoSwitchScope(EM, entityId, wasEditing) {
    if (wasEditing && typeof _flushServerPushes === 'function') {
      var flushed = await _flushServerPushes(15000);
      if (flushed === false) {
        if (typeof showToast === 'function') showToast(t('entities.switchFlushPending', 'Des modifications sont encore en cours d\'envoi — réessayez dans un instant.'), 'warning');
        return { ok: false, reason: 'flush-pending' };
      }
    }
    // Relâcher le verrou du périmètre courant + passer en consultation.
    try { if (window.CSI_EditLockManager && window.CSI_EditLockManager.release) await window.CSI_EditLockManager.release(); } catch (e) {}
    if (typeof setEditMode === 'function') { try { setEditMode(false); } catch (e) {} }
    // Cibler la nouvelle partition PUIS abandonner toute file résiduelle (le re-pull
    // qui suit fait autorité — aucun push ne doit partir vers le nouveau périmètre).
    // (audit 2026-07-03) mémoriser le périmètre courant pour ROLLBACK si le re-pull échoue.
    var _prevScope = EM.activeScope();
    EM.setActiveScope(entityId || null);
    window._csiDiscardPendingPushes();
    // === CSI-ME === (audit C2) L'historique d'annulation (undo) vit en RAM par état
    // appData courant. Après bascule, appData est REMPLACÉ par la partition cible :
    // conserver l'historique de la partition précédente permettrait, via « restaurer »,
    // d'injecter des données d'un AUTRE périmètre → fuite inter-partitions. On le purge.
    try {
      _ramHistorySnapshots = [];
      if (typeof renderHistoryList === 'function') renderHistoryList();
    } catch (e) {}
    var _switchFailed = null;
    try {
      if (typeof loadData === 'function') {
        // (audit 2026-07-03, MAJEUR) un pull raté ne LÈVE pas en mode serveur : loadData
        // pose _csiServerLoadFailed et laisse appData INCHANGÉ. Sans ce test, la bascule
        // « réussissait » en état hybride silencieux (bannière/titre = cible, données =
        // ancienne partition) — lecture des mauvais chiffres, écritures inter-partitions.
	        window._csiServerLoadFailed = false;
	        await loadData();
	        if (window._csiServerLoadFailed) throw new Error('server-pull-failed');
	        if (typeof _csiResetTabRevisions === 'function') _csiResetTabRevisions();
	        window._csiDiscardPendingPushes();
	        if (window.CSI_EntityConsolidation && window.CSI_EntityConsolidation.clear) window.CSI_EntityConsolidation.clear();
	        if (typeof updateDashboard === 'function') updateDashboard();
        var cur = (typeof _currentActiveTab !== 'undefined' && _currentActiveTab) ? _currentActiveTab : 'dashboard';
        if (typeof loadModuleContent === 'function') loadModuleContent(cur);
        // === CSI-ME === (certification 2026-07-03) navigate=TRUE : si la partition cible
        // utilise un AUTRE référentiel NIS2 (CyFun↔ReCyF) et que l'onglet actif est le
        // panneau de contrôles de l'ancien référentiel, replier vers le panneau primaire du
        // référentiel actif (sinon le panneau de l'ancien référentiel restait affiché).
        // No-op quand l'onglet actif reste valide (cas courant).
        if (typeof applyNis2Referential === 'function') applyNis2Referential(true);
      }
    } catch (e) {
      _switchFailed = e;
    }
    if (_switchFailed) {
      void 0;
      // ROLLBACK : périmètre inchangé + message explicite (l'échec était totalement
      // silencieux : sélecteur sur la cible, écran sur l'ancienne partition).
      EM.setActiveScope(_prevScope || null);
      window._csiDiscardPendingPushes();
      var _failName = entityId ? entityId : t('entities.mainEntity', 'Entité principale');
      EM.entities().forEach(function(e) { if (entityId && e && e.id === entityId) _failName = e.name || e.id; });
      if (typeof showToast2 === 'function') {
        showToast2(t('entities.switchFailed', 'Bascule vers « {{name}} » impossible (serveur injoignable) — périmètre inchangé.').replace('{{name}}', _failName), 'error', 10000);
      }
      return { ok: false, reason: 'reload-failed' };
    }
    // === CSI-ME === (audit 2026-07-03, MAJEUR) recherche globale : vider l'input et le
    // panneau de résultats — les actions par id des résultats de l'ANCIEN périmètre se
    // résolvent dans le NOUVEAU appData (ids séquentiels par partition → un clic ouvrait
    // un objet homonyme de l'autre partition), et la note de périmètre devenait fausse.
    try {
      var _gsInput = document.getElementById('globalSearchInput');
      if (_gsInput) _gsInput.value = '';
      if (typeof renderSearchResults === 'function') renderSearchResults([]);
    } catch (e) {}
    // === CSI-ME === (Lot 3c, anti-confusion) : armer la confirmation de première
    // entrée en édition sur le nouveau périmètre (hors retour au périmètre par défaut).
    if (entityId) {
      var _scopeLabel = entityId;
      EM.entities().forEach(function(e) { if (e && e.id === entityId) _scopeLabel = e.name || e.id; });
      window._csiConfirmScopeEdit = _scopeLabel;
    } else {
      window._csiConfirmScopeEdit = null;
    }
    return { ok: true, scope: entityId || 'group' };
  };

  // FIX-OPTIMISTIC-REV (2026-06-11) : clés appData d'un onglet (ou du pseudo-onglet global-meta).
  function _keysForTab(tabId) {
    if (tabId === GLOBAL_META_TAB) return GLOBAL_META_KEYS;
    var mgr = window.CSI_PresenceManager || window.CSI_LockManager;
    return (mgr && mgr.getTabDataKeys) ? (mgr.getTabDataKeys()[tabId] || null) : null;
  }

  // FIX-CASCADE-PERSIST (2026-06-29) : une suppression en cascade nettoie plusieurs sections en
  // RAM (ex. deleteRisk → risks + actions + soa), mais saveData() en mode serveur ne pousse que
  // l'onglet courant (+ global-meta). Sans ceci, les nettoyages des sections NON-courantes ne
  // sont jamais persistés et réapparaissent au prochain pull (références orphelines durables —
  // bug confirmé en E2E). On pousse explicitement chaque section mutée via le chemin éprouvé
  // _serverDataPushWithRetry (verrou 423, baseRev/409, parachute). _currentActiveTab et
  // global-meta sont déjà gérés par saveData() → on les saute pour éviter un double push.
  // No-op hors mode serveur (en file://, saveData persiste déjà tout appData en localStorage).
  function _tabsForKeys(keys) {
    if (!keys || !keys.length) return [];
    var mgr = window.CSI_PresenceManager || window.CSI_LockManager;
    var map = (mgr && mgr.getTabDataKeys) ? mgr.getTabDataKeys() : null;
    var seen = {}, result = [];
    keys.forEach(function(key) {
      var tabId = null;
      if (key === 'auditLog' || key === 'kpiHistory') {
        tabId = GLOBAL_META_TAB;
      } else if (map) {
        for (var tb in map) {
          if (map.hasOwnProperty(tb) && map[tb] && map[tb].indexOf(key) !== -1) { tabId = tb; break; }
        }
      }
      if (tabId && !seen[tabId]) { seen[tabId] = true; result.push(tabId); }
    });
    return result;
  }

  function _csiPersistAffectedTabs(keys) {
    if (!(typeof isServerModeUnlocked === 'function' && isServerModeUnlocked())) return;
    var mgr = window.CSI_PresenceManager || window.CSI_LockManager;
    var _elmC = window.CSI_EditLockManager;
    _tabsForKeys(keys).forEach(function(tabId) {
      if (tabId === _currentActiveTab || tabId === GLOBAL_META_TAB) return; // déjà poussés par saveData()
      var data = _extractTabData(tabId);
      if (data) {
        // === CSI-CO === (courtoisie §12) une cascade = travail réel → grappe touchée.
        if (_elmC && _elmC.markTabTouched) _elmC.markTabTouched(tabId);
        _serverDataPushWithRetry(tabId, data, mgr, 3);
      }
    });
  }
  // Exposé pour les modules hors bundle (ex. impact-matrices.js).
  window._csiPersistAffectedTabs = function(keys) { _csiPersistAffectedTabs(keys); };

  /**
   * FIX-OPTIMISTIC-REV (2026-06-11, durci par la revue concurrence) : PARACHUTE sur
   * conflit de révision (HTTP 409). Règle absolue : AUCUNE donnée ne disparaît — ni celles
   * du serveur (jamais écrasées en aveugle), ni celles saisies localement (backup ou rejeu).
   * Trois cas :
   *  A) Des modifications locales PLUS RÉCENTES attendent en file (_pendingPushTabs) :
   *     on adopte la révision serveur et on laisse le rejeu repousser l'état local le plus
   *     frais sur la bonne base (succès, nouveau 409, ou 423 — données préservées à chaque
   *     issue). Pas d'écrasement de l'écran, pas de perte de la saisie en cours.
   *  B) global-meta (auditLog/kpiHistory, append-only) : FUSION locale⊕serveur puis rejeu —
   *     les entrées des deux côtés coexistent, rien à sauvegarder en fichier.
   *  C) Cas standard : backup de l'état local actuel (le plus frais), application de l'état
   *     serveur (fourni dans la réponse, sinon récupéré par un pull), révision adoptée,
   *     onglet sorti de « dirty » uniquement une fois l'état autoritaire appliqué.
   */
  // === CSI-ME === (audit 2026-07-03, K9) qualifier les sauvegardes de conflit par
  // périmètre : la clé localStorage et le nom de fichier étaient partagés entre
  // partitions — un conflit sur risks du périmètre principal ÉCRASAIT le backup du
  // même onglet d'une sous-entité (et le fichier téléchargé ne disait pas d'où il venait).
  function _csiBackupScopeTag() {
    try {
      var EM = window.CSI_EntityManager;
      if (!EM || !EM.isMultiEntity || !EM.isMultiEntity()) return '';
      var id = EM.echoId && EM.echoId();
      return '_' + (id || 'principal');
    } catch (e) { return ''; }
  }
  // B2 (audit 2026-07-10) : parachute des onglets modifiés non confirmés AVANT un
  // remplacement autoritaire de appData (re-pull à l'acquisition du verrou). Réutilise
  // le filet du conflit 409 (Cas C) : copie de secours localStorage + téléchargement
  // fichier, par onglet dirty. Sans ceci, FIX-EDITLOCK-REPULL exécute loadData() et
  // écrase silencieusement les saisies faites après un push refusé en 423 — à rebours
  // du toast qui promet leur conservation. Retourne le nombre d'onglets sauvegardés.
  function _csiBackupDirtyTabs(reason) {
    var dirty = Object.keys(_csiDirtyTabs || {});
    if (!dirty.length) return 0;
    var tag = _csiBackupScopeTag();
    var stamp = new Date().toISOString();
    var n = 0;
    dirty.forEach(function(tabId) {
      try {
        var keys = _keysForTab(tabId) || [];
        var snap = {};
        keys.forEach(function(k) { snap[k] = appData[k]; });
        var backup = JSON.stringify({ _tabId: tabId, _reason: reason || 'repull', _backupAt: stamp, data: snap }, null, 2);
        try { localStorage.setItem('smsi_conflict_' + tabId + tag, backup); }
        catch (e2) { void 0; }
        if (typeof downloadJsonBackup === 'function') {
          downloadJsonBackup(backup, 'smsi_secours_' + tabId + tag + '.json');
        }
        n++;
      } catch (e) { void 0; }
    });
    return n;
  }

  function _handlePushConflict(tabId, localData, conflict) {
    var keys = _keysForTab(tabId);
    var fresh = conflict && conflict.fresh && conflict.fresh.data ? conflict.fresh.data : null;
    var _adoptRev = function() {
      if (conflict && typeof conflict.currentRev === 'number') _tabRevisions[tabId] = conflict.currentRev;
      else if (conflict && conflict.fresh && typeof conflict.fresh._rev === 'number') _tabRevisions[tabId] = conflict.fresh._rev;
      else delete _tabRevisions[tabId]; // révision inconnue → prochain push en mode legacy
    };
    var _who = (conflict && conflict.fresh && conflict.fresh._lastModifiedBy && conflict.fresh._lastModifiedBy.userName) ? (' (' + conflict.fresh._lastModifiedBy.userName + ')') : '';

    try {
      // ── Cas B : global-meta — fusionner (append-only), puis repousser l'union ──
      if (tabId === GLOBAL_META_TAB) {
        if (fresh) {
          if (Array.isArray(fresh.auditLog)) {
            var _byId = {};
            (fresh.auditLog || []).concat(appData.auditLog || []).forEach(function(e2) {
              if (e2 && e2.id && !_byId[e2.id]) _byId[e2.id] = e2;
            });
            var _merged = Object.keys(_byId).map(function(id) { return _byId[id]; });
            _merged.sort(function(a, b) { return String(a.timestamp || '').localeCompare(String(b.timestamp || '')); });
            appData.auditLog = _merged.slice(-100);
          }
          if (fresh.kpiHistory && (!appData.kpiHistory || !appData.kpiHistory._lastUpdate
              || (fresh.kpiHistory._lastUpdate || 0) > (appData.kpiHistory._lastUpdate || 0))) {
            appData.kpiHistory = fresh.kpiHistory;
          }
        }
        _adoptRev();
        // Repousser l'union sur la bonne base (rien n'est perdu — pas de backup nécessaire)
        _pendingPushTabs[tabId] = true;
        return;
      }

      // ── Cas A : des modifs locales plus récentes attendent — ne PAS écraser l'écran ──
      if (_pendingPushTabs[tabId]) {
        _adoptRev();
        // dirty + pending conservés : le drain (finally du push) rejouera l'état local le
        // plus frais avec la révision corrigée. Le travail en cours reste à l'écran.
        showToast2(
          t('toasts.pushConflictRetry',
            'Ces données ont été modifiées par un autre utilisateur' + _who + ' — nouvelle tentative d\'enregistrement de votre version en cours…'),
          'warning', 8000);
        return;
      }

      // ── Cas C : standard — backup de l'état local ACTUEL puis bascule sur le serveur ──
      // FIX-CONFLICT-BACKUP-SAFETY (revue prod 2026-06-11) : le téléchargement automatique
      // peut être bloqué par le navigateur (déclenché hors geste utilisateur). On écrit donc
      // AUSSI une copie de secours dans localStorage (récupérable même si le download est
      // bloqué) — c'est un filet, distinct des données métier qui, elles, vivent au serveur.
      try {
        var _localNow = {};
        (keys || []).forEach(function(k) { _localNow[k] = appData[k]; });
        var _backup = JSON.stringify({ _tabId: tabId, _conflictAt: new Date().toISOString(), data: keys ? _localNow : localData }, null, 2);
        try {
          localStorage.setItem('smsi_conflict_' + tabId + _csiBackupScopeTag(), _backup);
        } catch (e2) { void 0; }
        if (typeof downloadJsonBackup === 'function') {
          downloadJsonBackup(_backup, 'smsi_conflit_' + tabId + _csiBackupScopeTag() + '.json');
        }
      } catch (e) { void 0; }

      _adoptRev();

      var _applyAuthoritative = function(srvData) {
        if (srvData && keys) {
          keys.forEach(function(k) {
            if (srvData[k] !== undefined) appData[k] = srvData[k];
          });
          if (window._csiInvalidateSearchIndex) window._csiInvalidateSearchIndex(); // (audit 2026-07-03) appData muté par parachute conflit 409 → index recherche périmé
        }
        // Sorti de « dirty » seulement maintenant : l'état autoritaire est appliqué
        delete _csiDirtyTabs[tabId];
        delete _pendingPushTabs[tabId];
        _updateUnsavedFlag();
        if (tabId === _currentActiveTab && typeof _rerenderCurrentTab === 'function') {
          _rerenderCurrentTab(tabId);
        }
        if (typeof updateDashboard === 'function') updateDashboard();
      };

      if (fresh) {
        _applyAuthoritative(fresh);
      } else if (window.CSI_SyncManager && window.CSI_SyncManager.pullFromServer) {
        // fresh absent (tab purgé par un import, ou fichier illisible) : récupérer l'état
        // autoritaire complet (server-data.json + tabs fusionnés). En cas d'échec du pull,
        // l'onglet RESTE dirty (données locales protégées, re-tentées par l'autosave).
        window.CSI_SyncManager.pullFromServer().then(function(full) {
          if (full && typeof full === 'object') _applyAuthoritative(full);
          else void 0;
        }).catch(function() {
          void 0;
        });
      }
    } catch (e) {
      void 0;
    }
    // FIX-CONFLICT-BACKUP-SAFETY : message générique (clé de locale sans interpolation
    // de l'auteur — fr+en présentes). La copie de secours est en fichier ET en localStorage.
    showToast2(
      t('toasts.pushConflict', 'Ces données ont été modifiées par un autre utilisateur. Vos modifications n\'ont pas été écrasées : elles ont été téléchargées dans un fichier de secours, et l\'écran affiche la version du serveur.'),
      'warning', 15000);
  }

  /**
   * FIX-CONFIG-PERSIST-LOCK (revue prod 2026-06-11) : persiste une configuration globale
   * (niveau/référentiel NIS2…) au serveur en respectant EDIT_LOCK_ENFORCED. Si l'utilisateur
   * n'est pas déjà en mode édition, acquiert brièvement le verrou autour du push puis le
   * libère — sinon le push serait refusé en 423 et le choix perdu au rechargement (les
   * appelants historiques avalaient le 423 résolu dans un .catch jamais déclenché).
   * Hors mode serveur, simple forceServerSync. Affiche un message si le verrou est occupé.
   */
  function _persistConfigWithLock(keys) {
    var SM = window.CSI_SyncManager;
    if (!SM || !SM.forceServerSync) return;
    if (!(SM.isServerMode && SM.isServerMode())) { SM.forceServerSync(keys).catch(function() {}); return; }
    var ELM = window.CSI_EditLockManager;
    var alreadyEditing = !(typeof isReadOnlyMode === 'function' && isReadOnlyMode());
    var doPush = function(releaseAfter) {
      SM.forceServerSync(keys).then(function(r) {
        if (r && r.success === false && r.code === 423) {
          showToast2(t('toasts.configNotPersisted', "Configuration non enregistrée sur le serveur (un autre utilisateur édite) — elle sera appliquée à votre prochaine entrée en mode édition."), 'warning', 9000);
        }
      }).catch(function() {}).then(function() {
        if (releaseAfter && ELM && ELM.release) ELM.release().catch(function() {});
      });
    };
    if (alreadyEditing || !ELM || !ELM.acquire) { doPush(false); return; }
    ELM.acquire().then(function(res) { doPush(!!(res && res.granted)); }).catch(function() { doPush(false); });
  }

  // === CSI-ME === (certification 2026-07-03, BLOQUANT) Amorcer la révision d'un onglet
  // dont la rev serveur est INCONNUE, sans toucher appData. Nécessaire pour les cascades :
  // à l'acquisition du verrou _csiResetTabRevisions() vide toutes les révisions ; seul
  // l'onglet re-navigué est ré-amorcé. Les onglets de FOND d'une cascade (deleteRisk →
  // actions/soa) n'ont donc pas de baseRev → le gate serveur V7 (entEnforcePush) les refuse
  // en 400 LEGACY_CLIENT → cascade partielle → références orphelines durables (contexte
  // PARTITION uniquement). pullTabFromServer est un fetch PUR (retourne {_rev,data} sans
  // écrire dans appData) : on ne récupère que _rev, la donnée RAM nettoyée reste la source.
  async function _csiEnsureTabRevision(tabId) {
    if (typeof _tabRevisions[tabId] === 'number') return;
    var SyncMgr = window.CSI_SyncManager;
    if (!SyncMgr || typeof SyncMgr.pullTabFromServer !== 'function') return;
    try {
      var t = await SyncMgr.pullTabFromServer(tabId);
      // t null = section jamais écrite côté serveur ⇒ révision de base 0 (currentRev serveur = 0).
      _tabRevisions[tabId] = (t && typeof t._rev === 'number') ? t._rev : 0;
    } catch (e) { /* best-effort : le filet 400 LEGACY_CLIENT ci-dessous ré-amorce et rejoue */ }
  }

  async function _serverDataPushWithRetry(tabId, tabData, lockMgr, maxRetries) {
    // === CSI-ME === pendant une bascule de périmètre, aucun push ne doit partir
    // (l'état RAM est en cours de remplacement par le re-pull autoritaire). Évite
    // des 423 parasites vers le périmètre en transition.
    if (window._csiScopeSwitching) return false;
    _csiDirtyTabs[tabId] = true;
    _updateUnsavedFlag();
    // FIX-SYNC-DISABLED (audit 2026-06-10) : respecter la désactivation de la sync
    // (licence/trial expiré → handleLicenseExpired, ou setAutoSync(false)).
    if (window.CSI_SyncManager && window.CSI_SyncManager.isSyncEnabled
        && !window.CSI_SyncManager.isSyncEnabled()) {
      void 0;
      return false;
    }
    if (_pushInProgress) {
      // FIX-PUSH-QUEUE : rejouer ce push à la fin de celui en vol au lieu de le perdre
      _pendingPushTabs[tabId] = true;
      void 0;
      return false;
    }
    _pushInProgress = true;
    try {
      // === CSI-ME === (certification 2026-07-03) en mode partition, le serveur EXIGE baseRev
      // (gate V7). Si la révision de cet onglet est inconnue (onglet de fond d'une cascade,
      // jamais re-navigué depuis l'acquisition du verrou), on l'amorce AVANT de pousser —
      // sinon 400 LEGACY_CLIENT et orphelins. No-op hors mode multi-entités et si déjà connue.
      if (window.CSI_EntityManager && window.CSI_EntityManager.isMultiEntity() && typeof _tabRevisions[tabId] !== 'number') {
        await _csiEnsureTabRevision(tabId);
      }
      var delays = [1000, 2000, 4000]; // backoff exponentiel
      for (var attempt = 0; attempt <= maxRetries; attempt++) {
        try {
          // FIX-OPTIMISTIC-REV (2026-06-11) : joindre la révision serveur connue (baseRev)
          var _pushBody = { tabId: tabId, data: tabData };
          if (typeof _tabRevisions[tabId] === 'number') _pushBody.baseRev = _tabRevisions[tabId];
          // FIX-DATA-EPOCH (audit 2026-07-02, C1) : joindre l'époque de l'état sur lequel on
          // s'est basé — le serveur refuse (409 epochConflict) si un import/restore a eu lieu depuis.
          if (window.appData && typeof window.appData._dataEpoch === 'number') _pushBody.dataEpoch = window.appData._dataEpoch;
          // === CSI-ME === écho d'identité de partition (V16) + entity= ciblé (compte
          // principal en périmètre sous-entité). No-op hors mode multi-entités.
          var _emPushUrl = './data/sync.php?action=data-push';
          if (window.CSI_EntityManager && window.CSI_EntityManager.isMultiEntity()) {
            var _emExtra = window.CSI_EntityManager.pushBodyExtra();
            for (var _ek in _emExtra) { if (Object.prototype.hasOwnProperty.call(_emExtra, _ek)) _pushBody[_ek] = _emExtra[_ek]; }
            _emPushUrl = window.CSI_EntityManager.appendEntityParam(_emPushUrl);
          }
          var resp = await fetchWithTimeout(_emPushUrl, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': window.CSI_AuthManager.getAuthHeader()
            },
            body: JSON.stringify(_pushBody)
          });
          if (resp && resp.ok) {
            if (window.CSI_Storage) window.CSI_Storage.updateLastPush();
            // FIX-OPTIMISTIC-REV : mémoriser la nouvelle révision confirmée par le serveur
            try {
              var _okBody = await resp.json();
              if (_okBody && typeof _okBody.rev === 'number') _tabRevisions[tabId] = _okBody.rev;
            } catch (e) { /* corps non-JSON — ignorer */ }
            // Ne nettoyer que si aucune NOUVELLE modification n'attend pour cet onglet
            if (!_pendingPushTabs[tabId]) {
              delete _csiDirtyTabs[tabId];
              _updateUnsavedFlag();
            }
            void 0;
            return true;
          }
          if (resp && resp.status === 423) {
            // FIX-LOCK-423 (audit 2026-06-10, bundle.js:12732) : avec EDIT_LOCK_ENFORCED=true
            // le serveur répond 423 (verrou non détenu) — pas de retry aveugle : message
            // dédié + resynchronisation de l'état du verrou (si la perte est confirmée,
            // l'événement lock-lost fera sortir du mode édition). Données conservées en
            // mémoire (onglet reste dirty) — aucune perte.
            showToast2(t('toasts.editLockPushRefused', 'Verrou d\'édition non détenu — vos modifications restent en mémoire et ne sont pas enregistrées sur le serveur. Reprenez la main (mode édition) pour les enregistrer ; ne fermez pas l\'onglet avant.'), 'error', 12000);
            if (window.CSI_EditLockManager && window.CSI_EditLockManager.refreshStatus) {
              window.CSI_EditLockManager.refreshStatus();
            }
            return false;
          }
          if (resp && resp.status === 403) {
            // === CSI-ME === (audit co-édition 2026-07-03, MAJEUR) 403 sur data-push :
            // pas de retry (l'autosave rejouait un toast générique trompeur en boucle).
            var _forbidden = null;
            try { _forbidden = await resp.json(); } catch (e) { /* ignore */ }
            if (_forbidden && _forbidden.code === 'SECTION_READONLY') {
              // Droits par section d'une sous-entité : le serveur n'acceptera JAMAIS cette
              // écriture — message dédié, sortir l'onglet de la file dirty, re-pull de
              // l'état autoritaire pour que l'écran cesse d'afficher un état non enregistré.
              showToast2(t('toasts.sectionReadonly', "Cette section est en LECTURE SEULE pour votre périmètre — vos modifications n'ont pas été enregistrées."), 'error', 12000);
              delete _csiDirtyTabs[tabId]; delete _pendingPushTabs[tabId]; _updateUnsavedFlag();
              try {
                if (typeof loadData === 'function') {
                  await loadData();
                  if (typeof _csiResetTabRevisions === 'function') _csiResetTabRevisions();
                  if (typeof _rerenderCurrentTab === 'function' && typeof _currentActiveTab !== 'undefined' && _currentActiveTab) _rerenderCurrentTab(_currentActiveTab);
                }
              } catch (e) { /* re-pull best-effort */ }
              return false;
            }
            showToast2(t('toasts.pushForbidden', 'Écriture refusée par le serveur (droits insuffisants) — modifications non enregistrées.'), 'error', 10000);
            return false;
          }
          if (resp && resp.status === 409) {
            // FIX-OPTIMISTIC-REV (2026-06-11) : conflit de révision — un autre utilisateur a
            // modifié cet onglet. PARACHUTE : on n'écrase JAMAIS la version plus récente.
            // Sauvegarde locale des modifications refusées + rechargement de l'état frais.
            var _conflict = null;
            try { _conflict = await resp.json(); } catch (e) { /* ignore */ }
            if (_conflict && _conflict.code === 'ENTITY_CONTEXT_CHANGED') {
              // === CSI-ME === (audit co-édition 2026-07-03, MAJEUR) le rattachement du compte
              // a changé (réassignation par l'admin) — ce n'est PAS un conflit d'édition. Le
              // parachute générique affichait « modifiées par un autre utilisateur » (cause
              // fausse) et rechargeait les données de la NOUVELLE partition sous l'ancien
              // bandeau. Sauvegarde de l'onglet puis reload : le /me ré-ingère le contexte.
              try {
                var _eccBkp = JSON.stringify({ _tabId: tabId, _entityContextChangedAt: new Date().toISOString(), data: tabData }, null, 2);
                try { localStorage.setItem('smsi_conflict_' + tabId + _csiBackupScopeTag(), _eccBkp); } catch (e2) { /* quota */ }
                if (typeof downloadJsonBackup === 'function') downloadJsonBackup(_eccBkp, 'smsi_conflit_rattachement_' + tabId + _csiBackupScopeTag() + '.json');
              } catch (e) { /* best-effort */ }
              showToast2(t('toasts.entityContextChanged', "Votre rattachement d'entité a changé — l'application va se recharger pour appliquer votre nouveau périmètre. Une copie de vos modifications non enregistrées a été téléchargée."), 'warning', 8000);
              setTimeout(function() { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }, 3000);
              return false;
            }
            if (_conflict && _conflict.epochConflict) {
              // FIX-DATA-EPOCH (audit 2026-07-02, C1) : un import/restauration serveur a remplacé
              // TOUT l'état → re-pull autoritaire complet (loadData rafraîchit appData._dataEpoch,
              // ce qui évite la boucle de 409) après avoir sauvegardé l'onglet en cours. On ne
              // passe PAS par _handlePushConflict (parachute par onglet inadapté à un remplacement full).
              try {
                var _epBkp = JSON.stringify({ _tabId: tabId, _epochConflictAt: new Date().toISOString(), data: tabData }, null, 2);
                try { localStorage.setItem('smsi_conflict_' + tabId + _csiBackupScopeTag(), _epBkp); } catch (e2) { /* quota */ }
                if (typeof downloadJsonBackup === 'function') downloadJsonBackup(_epBkp, 'smsi_conflit_import_' + tabId + _csiBackupScopeTag() + '.json');
              } catch (e) { /* backup best-effort */ }
              try {
                if (typeof loadData === 'function') {
                  await loadData();
                  if (typeof _csiResetTabRevisions === 'function') _csiResetTabRevisions();
                  delete _csiDirtyTabs[tabId]; delete _pendingPushTabs[tabId]; _updateUnsavedFlag();
                  if (typeof updateDashboard === 'function') updateDashboard();
                  var _ecTab = (typeof _currentActiveTab !== 'undefined' && _currentActiveTab) ? _currentActiveTab : 'dashboard';
                  if (typeof _rerenderCurrentTab === 'function') _rerenderCurrentTab(_ecTab);
                }
              } catch (e) { void 0; }
              showToast2(t('toasts.importEpochConflict', "Les données ont été remplacées par un import/restauration d'un autre utilisateur. L'écran a été rechargé ; une copie de vos modifications non enregistrées a été sauvegardée."), 'warning', 15000);
              return false;
            }
            _handlePushConflict(tabId, tabData, _conflict);
            return false; // pas de retry : le parachute a re-synchronisé l'onglet
          }
          if (resp && resp.status === 402) {
            showToast2(t('toasts.licenseExpired', 'Licence expirée — mode lecture seule.'), 'error', 10000);
            return false;
          }
          if (resp && resp.status === 400) {
            // === CSI-ME === (certification 2026-07-03) filet anti-orphelin : le serveur a
            // refusé un push scopé sans baseRev (gate V7 LEGACY_CLIENT) — l'amorçage a dû
            // échouer (réseau). On ré-amorce la révision et on rejoue (JAMAIS d'orphelin
            // silencieux : sans ce filet, une cascade partielle laissait des liens cassés).
            var _b400 = null;
            try { _b400 = await resp.json(); } catch (e) { /* corps non-JSON */ }
            if (_b400 && _b400.code === 'LEGACY_CLIENT') {
              delete _tabRevisions[tabId];
              await _csiEnsureTabRevision(tabId);
              if (typeof _tabRevisions[tabId] === 'number' && attempt < maxRetries) {
                continue; // rev désormais connue → rejouer avec baseRev
              }
            }
            void 0;
            return false; // autre 400 (clé hors section, tabId invalide…) : pas de retry aveugle
          }
          void 0;
        } catch (err) {
          void 0;
        }
        // Retry avec délai si pas le dernier essai
        if (attempt < maxRetries) {
          await new Promise(function(resolve) { setTimeout(resolve, delays[attempt] || 4000); });
        }
      }
      // Tous les retries épuisés
      showToast2(t('toasts.syncFailedRetry', 'Échec de synchronisation serveur — vos données sont en mémoire. Ne fermez pas le navigateur.'), 'error', 15000);
      return false;
    } finally {
      _pushInProgress = false;
      // FIX-PUSH-QUEUE : rejouer les push mis en attente pendant celui-ci
      _drainPendingPushes();
    }
  }

  // beforeunload : avertissement navigateur si données non sauvegardées
  if (window.location.protocol !== 'file:') {
    window.addEventListener('beforeunload', function(e) {
      if (!window._csiUnsavedServerData) return;
      e.preventDefault();
      e.returnValue = '';
    });
  }

  // M28 (audit 2026-07-10) : garde multi-onglets en MODE LOCAL. Deux onglets ouverts sur le
  // même navigateur écrivent chacun le blob COMPLET (smsi_data) ; sans coordination, l'onglet
  // resté en arrière-plan écrase silencieusement les sauvegardes de l'autre. On écoute
  // 'storage' (émis uniquement dans les AUTRES onglets) : bannière non destructive invitant à
  // recharger (« refus propre > automatisme malin »), et parachute de la version de l'autre
  // onglet avant tout écrasement (voir saveData). Aucun effet en mode serveur (déjà couvert
  // par les révisions _rev/409 et le verrou d'édition).
  var _csiOtherTabWrote = false;
  function _csiIsLocalMode() {
    try { return !(typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()); }
    catch (e) { return window.location.protocol === 'file:'; }
  }
  function _csiShowMultiTabBanner() {
    if (document.getElementById('csiMultiTabBanner') || !document.body) return;
    var b = document.createElement('div');
    b.id = 'csiMultiTabBanner';
    b.style.cssText = 'position:fixed;left:0;right:0;bottom:0;z-index:99999;background:#d97706;color:#fff;padding:10px 16px;font-size:0.9rem;text-align:center;box-shadow:0 -2px 8px rgba(0,0,0,.25);';
    var msg = document.createElement('span');
    msg.textContent = (typeof t === 'function') ? t('toasts.multiTabConflict', 'Données modifiées dans un autre onglet. Rechargez cette page pour éviter d\'écraser votre travail.') : 'Données modifiées dans un autre onglet — rechargez.';
    var btn = document.createElement('button');
    btn.textContent = (typeof t === 'function') ? t('common.reload', 'Recharger') : 'Recharger';
    btn.style.cssText = 'margin-left:12px;padding:4px 12px;border:1px solid #fff;border-radius:6px;background:transparent;color:#fff;cursor:pointer;';
    btn.onclick = function() { try { window.onbeforeunload = null; } catch (e) {} location.reload(); };
    b.appendChild(msg); b.appendChild(btn);
    document.body.appendChild(b);
  }
  window.addEventListener('storage', function(ev) {
    if (!ev || (ev.key !== 'smsi_data' && ev.key !== 'smsi_last_save')) return;
    if (!_csiIsLocalMode()) return;
    _csiOtherTabWrote = true;
    _csiShowMultiTabBanner();
  });

  // PASS2-307: Track last save result for modal close guard
  var _lastSaveFailed = false;
  // M13 (audit 2026-07-10) : évite l'inondation du dossier Téléchargements. Sans ce flag,
  // chaque saveData() en situation de quota plein (autosave 30 s + ~89 sites UI) téléchargeait
  // un backup identique. Un seul backup par ÉPISODE de saturation ; remis à zéro dès qu'une
  // sauvegarde réussit.
  var _quotaBackupDownloaded = false;
  function saveData() {
    _lastSaveFailed = false;
    if (window._csiInvalidateSearchIndex) window._csiInvalidateSearchIndex(); // (audit 2026-07-03) index recherche périmé après mutation
    // Protection anti-abus en mode démo
    if (DEMO_MODE) {
      if (!demoRateLimiter.checkAction('save')) {
        demoRateLimiter.showRateLimitMessage();
        _lastSaveFailed = true; return false;
      }
      if (!checkDemoStorageLimit()) {
        _lastSaveFailed = true; return false;
      }
    }

    // BUG-301: Validation blocks save on critical errors
    if (typeof window.validateBeforeSave === 'function') {
      const validation = window.validateBeforeSave(appData);
      if (!validation.valid && validation.errors && validation.errors.length > 0) {
        void 0;
        var criticalErrors = validation.errors.filter(function(e) { return e && (typeof e === 'object' ? e.severity === 'critical' : /requis|missing|corrupt/i.test(e)); });
        if (criticalErrors.length > 0) {
          showToast2(t('validation.blocked', 'Sauvegarde bloquée : ') + (typeof criticalErrors[0] === 'string' ? criticalErrors[0] : criticalErrors[0].message), 'error');
          _lastSaveFailed = true; return false;
        }
        // Non-critical errors: warn but allow save
        showToast2(t('validation.warnings', 'Données sauvegardées avec avertissements'), 'warning');
      }
      if (validation.warnings && validation.warnings.length > 0) {
        void 0;
      }
    }

    warnMissingSubcategories();
    ensureNis2Plan();
    ensureCriticalAssets();

    // ──────────────────────────────────────────────────────────
    // MODE SERVEUR : push direct au serveur, PAS de localStorage
    // ──────────────────────────────────────────────────────────
    var _isServer = typeof isServerModeUnlocked === 'function' && isServerModeUnlocked();
    if (_isServer) {
      const autoSaveStatus = document.getElementById("autoSaveStatus");
      if (autoSaveStatus) {
        // FIX-I18N-AUTOSAVE (audit 2026-06-10, bundle.js:12866) : indicateur traduit
        autoSaveStatus.textContent = t('autoSave.saving', 'Sauvegarde en cours...');
        autoSaveStatus.style.opacity = "1";
      }

      if (window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn()) {
        var presenceMgr = window.CSI_PresenceManager || window.CSI_LockManager;
        var currentTab = _currentActiveTab || 'dashboard';
        // FIX-GLOBALMETA-SYNC (audit 2026-06-10, lock-manager.js:19) : auditLog et
        // kpiHistory n'appartiennent à aucun onglet et n'étaient JAMAIS poussés par la
        // sauvegarde granulaire — le journal d'audit disparaissait au rechargement et
        // n'était jamais visible des autres utilisateurs. Push d'un pseudo-onglet
        // 'global-meta' (fusionné comme les autres tab_*.json au pull complet du login).
        if (window._csiGlobalMetaDirty) {
          window._csiGlobalMetaDirty = false;
          _serverDataPushWithRetry(GLOBAL_META_TAB, _extractTabData(GLOBAL_META_TAB), presenceMgr, 3);
        }
        if (currentTab !== 'dashboard' && currentTab !== 'settings') {
          var tabKeys = presenceMgr && presenceMgr.getTabDataKeys ? presenceMgr.getTabDataKeys()[currentTab] : null;
          if (tabKeys) {
            var tabData = {};
            tabKeys.forEach(function(key) { tabData[key] = appData[key]; });
            // === CSI-CO === (courtoisie §12) saveData = travail RÉEL de l'utilisateur sur
            // cet onglet → sa grappe est « touchée » (jamais relâchée par courtoisie).
            // Le push MÉCANIQUE de bascule d'onglet (~l.24422) ne marque PAS, lui.
            var _elmT = window.CSI_EditLockManager;
            if (_elmT && _elmT.markTabTouched) _elmT.markTabTouched(currentTab);
            // Push direct — l'arbitrage de l'édition concurrente est géré par CSI_EditLockManager
            // (voir modules/edit-lock-manager.js + sync.php endpoint edit-lock-acquire).
            // FIX-PUSH-QUEUE : _csiUnsavedServerData est désormais dérivé des onglets
            // dirty (géré dans _serverDataPushWithRetry), plus d'écrasement direct ici.
            // Refonte header 2026-07 : pastille sync en « saving » (pulse) pendant le push
            // asynchrone — c'est le seul point d'accroche réel (le reste de saveData est
            // synchrone, un état « saving » n'y serait jamais peint).
            _syncDotSetState("saving", t('header.syncSaving', 'Sauvegarde…'));
            _serverDataPushWithRetry(currentTab, tabData, presenceMgr, 3).then(function(ok) {
              if (autoSaveStatus) {
                // FIX-I18N-AUTOSAVE (audit 2026-06-10) : indicateur traduit
                autoSaveStatus.textContent = ok ? t('autoSave.savedToServer', 'Sauvegardé sur le serveur') : t('autoSave.syncFailed', 'Échec synchronisation');
                setTimeout(function() { autoSaveStatus.style.opacity = "0"; }, 2000);
              }
              if (ok) updateAutoSaveIndicator();
              else _syncDotSetState("error", t('header.syncError', 'Échec sync'));
            });
          } else {
            if (autoSaveStatus) {
              autoSaveStatus.textContent = "";
              autoSaveStatus.style.opacity = "0";
            }
          }
        }
      }
      return true;
    }

    // ──────────────────────────────────────────────────────────
    // MODE LOCAL : sauvegarde localStorage (comportement inchangé)
    // ──────────────────────────────────────────────────────────
    // (audit global 2026-07-03, perf) sérialisation COMPLÈTE d'appData nécessaire ICI seulement
    // (quota + localStorage). En mode serveur (return ci-dessus) elle était calculée puis jetée
    // à CHAQUE saveData() (autosave 30s + 89 sites) — gaspillage O(taille appData) inutile.
    const serialized = JSON.stringify(appData);
    const dataSize = serialized.length * 2; // UTF-16
    const quotaCheck = checkLocalStorageQuota(dataSize);
    if (!quotaCheck.ok) {
      const usageMB = (quotaCheck.usage / (1024 * 1024)).toFixed(2);
      const limitMB = (quotaCheck.limit / (1024 * 1024)).toFixed(2);
      showToast2(t('settings.storageInsufficient', 'Espace de stockage insuffisant').replace('{{usage}}', usageMB).replace('{{limit}}', limitMB) + ` (${usageMB}MB / ${limitMB}MB)`, "error");
      // M13 : un seul backup par épisode de saturation (pas à chaque tick d'autosave).
      if (!_quotaBackupDownloaded) {
        downloadJsonBackup(serialized, "smsi_backup_quota_exceeded.json");
        _quotaBackupDownloaded = true;
      }
      _lastSaveFailed = true; return false;
    }
    if (quotaCheck.warning) {
      const usageMB = (quotaCheck.projected / (1024 * 1024)).toFixed(2);
      showToast2(t('settings.storageWarning', 'Attention : stockage presque plein') + ` (${usageMB}MB). ` + t('settings.storageExportHint', 'Pensez à exporter vos données.'), "warning");
    }

    try {
      // M28 : si un autre onglet a écrit depuis, sauvegarder SA version avant de l'écraser
      // (aucune perte silencieuse — l'utilisateur récupère les deux versions).
      if (_csiOtherTabWrote) {
        try {
          var _otherRaw = localStorage.getItem("smsi_data");
          if (_otherRaw && _otherRaw !== serialized) {
            localStorage.setItem("smsi_multitab_backup", JSON.stringify({ _at: new Date().toISOString(), data: _otherRaw }));
            if (typeof downloadJsonBackup === 'function') downloadJsonBackup(_otherRaw, "smsi_autre_onglet.json");
          }
        } catch (e) { /* quota : la version courante reste prioritaire */ }
        _csiOtherTabWrote = false;
      }
      localStorage.setItem("smsi_data", serialized);
      localStorage.setItem("smsi_last_save", (/* @__PURE__ */ new Date()).toISOString());
      const autoSaveStatus = document.getElementById("autoSaveStatus");
      if (autoSaveStatus) {
        // FIX-I18N-AUTOSAVE (audit 2026-06-10) : indicateur traduit
        autoSaveStatus.textContent = t('autoSave.saved', 'Sauvegarde réalisée');
        autoSaveStatus.style.opacity = "1";
        setTimeout(() => {
          autoSaveStatus.style.opacity = "0";
        }, 2e3);
      }
      refreshStorageHealth();
      _quotaBackupDownloaded = false; // M13 : sauvegarde OK → réarmer le backup pour un futur épisode
      return true;
    } catch (error) {
      void 0;
      const isQuotaError = error.name === 'QuotaExceededError' ||
                          error.code === 22 ||
                          error.code === 1014 ||
                          (error.name === 'NS_ERROR_DOM_QUOTA_REACHED');
      if (isQuotaError) {
        showToast2(t('toasts.quotaExceeded', 'Quota localStorage dépassé. Vos données ont été téléchargées en backup.'), "error");
      } else {
        showToast2(t('toasts.saveError', 'La sauvegarde a échoué. Vos données sont conservées localement.'), "error");
      }
      // M13 : idem — un seul backup d'urgence par épisode (le catch se répète à chaque autosave).
      if (!_quotaBackupDownloaded) {
        downloadJsonBackup(serialized, "smsi_backup_emergency.json");
        _quotaBackupDownloaded = true;
      }
      _lastSaveFailed = true; return false;
    }
  }
  function loadThreats() {
    // En mode serveur, les données sont déjà dans appData (chargées par loadData())
    var _isServer = window.CSI_Storage && window.CSI_Storage.isServerMode();
    var threats;
    if (_isServer) {
      threats = appData.threats || [];
    } else {
      const saved = localStorage.getItem("smsi_data");
      if (!saved) return;
      try {
        const data = JSON.parse(saved);
        threats = data.threats || [];
      } catch (e) {
        void 0;
        return;
      }
    }
    appData.threats = threats.map(t => {
      const prob = parseInt(t.likelihood, 10) ||
        (t.likelihood === "low" ? 3 : t.likelihood === "medium" ? 4 : t.likelihood === "high" ? 5 : 1);
      const imp = parseInt(t.impact, 10) ||
        (t.impact === "minor" ? 2 : t.impact === "major" ? 4 : t.impact === "critical" ? 5 : 1);
      const score = imp * prob;
      return { ...t, likelihood: prob, impact: imp, score, level: calculateRiskLevel(score) };
    });
  }
  function saveThreats() {
    saveData();
  }

  // modules/controls.js
function getControlStatusOptions() {
  return [
    { value: "non_implemente", label: t('controlStatuses.nonImplemente') },
    { value: "en_cours", label: t('controlStatuses.enCours') },
    { value: "implemente", label: t('controlStatuses.implemente') },
    { value: "verifie", label: t('controlStatuses.verifie') }
  ];
}
var CONTROL_STATUS_OPTIONS = getControlStatusOptions();
function getDomainOptions() {
  return [
    { value: "Organisationnel", label: t('domains.organisationnel') },
    { value: "Humain", label: t('domains.humain') },
    { value: "Physique", label: t('domains.physique') },
    { value: "Technologique", label: t('domains.technologique') },
    { value: "Non classé", label: t('domains.nonClasse') }
  ];
}
let DOMAIN_OPTIONS = getDomainOptions();
function getCmmLevelOptions() {
  return [
    { value: "Initial", label: t('cmmLevels.initial') },
    { value: "Répétable", label: t('cmmLevels.repetable') },
    { value: "Défini", label: t('cmmLevels.defini') },
    { value: "Géré", label: t('cmmLevels.gere') },
    { value: "Optimisé", label: t('cmmLevels.optimise') }
  ];
}
var CMM_LEVEL_OPTIONS = getCmmLevelOptions();

  // Vérifie si un contrôle est déclaré non-applicable dans la SoA
  function isControlNonApplicable(controlId) {
    const soaEntry = appData.soa.find(e => e.controlId === controlId);
    return soaEntry && soaEntry.applicable === false;
  }

  // Récupère la justification SoA pour un contrôle
  function getSoaJustification(controlId) {
    const soaEntry = appData.soa.find(e => e.controlId === controlId);
    return soaEntry ? soaEntry.justification || "" : "";
  }

  // Navigue vers l'onglet SoA et met en évidence le contrôle
  function navigateToSoaControl(controlId) {
    // Naviguer vers l'onglet SoA
    const soaNavBtn = document.querySelector('.nav__item[data-tab="soa"]');
    if (soaNavBtn) {
      soaNavBtn.click();
      // Après navigation, mettre en évidence la ligne du contrôle
      setTimeout(() => {
        const soaRow = document.querySelector(`tr[data-control-id="${controlId}"]`);
        if (soaRow) {
          soaRow.scrollIntoView({ behavior: "smooth", block: "center" });
          soaRow.classList.add("highlight-row");
          setTimeout(() => soaRow.classList.remove("highlight-row"), 2500);
        }
      }, 150);
    }
  }

  function loadControlsTable() {
    const tbody = document.getElementById("controlsTableBody");
    if (!tbody) return;
    tbody.innerHTML = "";

    const _f = SectionToolbar.getFilters("controls");
    const _q = (SectionToolbar.getSearch("controls") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const catFilter = _f.category || "";
    const statusFilter = _f.status || "";
    const cmmFilter = _f.cmm || "";
    const capabilityFilter = "";

    var isoControls = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    const filtered = isoControls.filter(c => {
      const capabilityMatch = !capabilityFilter || (Array.isArray(c.subcategory) ? c.subcategory.includes(capabilityFilter) : c.subcategory === capabilityFilter);
      return (!catFilter || c.category === catFilter) &&
             (!statusFilter || c.status === statusFilter) &&
             (!cmmFilter || c.cmm === cmmFilter) &&
             (!_q || ((c.reference || "") + " " + (c.title || "")).toLowerCase().indexOf(_q) >= 0) &&
             capabilityMatch;
    });

    const _s = SectionToolbar.getSort("controls");
    const _sortKey = _s ? _s.key : "reference";
    const _sortDir = _s ? _s.dir : "asc";
    const sortedControls = [...filtered].sort((a, b) => {
      let diff = 0;
      const statusOrder = { non_implemente: 0, en_cours: 1, implemente: 2, verifie: 3 };
      const cmmOrder = { "Initial": 0, "R\u00e9p\u00e9table": 1, "D\u00e9fini": 2, "G\u00e9r\u00e9": 3, "Optimis\u00e9": 4 };
      switch (_sortKey) {
        case "title": diff = SortUtils.text(a.title, b.title); break;
        case "status": diff = SortUtils.orderedMap(statusOrder, 99)(a.status, b.status); break;
        case "maturity": diff = SortUtils.orderedMap(cmmOrder, 99)(a.cmm, b.cmm); break;
        default: diff = SortUtils.reference(a.reference, b.reference); break;
      }
      return _sortDir === "desc" ? -diff : diff;
    });
    sortedControls.forEach((control) => {
      const row = document.createElement("tr");
      const actionIds = appData.actions
        .filter((a) => a.linkType === "control" && a.linkId === control.id)
        .map((a) => a.id)
        .join(", ");
      const statusClasses = {
        "non_implemente": "status-badge--non_implemente",
        "en_cours": "status-badge--en_cours",
        "implemente": "status-badge--implemente",
        "verifie": "status-badge--verifie"
      };
      const statusLabels = {};
      CONTROL_STATUS_OPTIONS.forEach(function(o) { statusLabels[o.value] = o.label; });
      const capabilityDisplay = Array.isArray(control.subcategory)
        ? control.subcategory.map(sc => {
            const cap = OPERATIONAL_CAPABILITIES.find((c) => c.key === sc);
            return cap ? cap.label : sc.replace(/_/g, " ");
          }).join(", ")
        : (() => {
            const cap = OPERATIONAL_CAPABILITIES.find((c) => c.key === control.subcategory);
            return cap ? cap.label : (control.subcategory || "").replace(/_/g, " ");
          })();

      // Vérifier si le contrôle est déclaré non-applicable dans la SoA
      const isNonApplicable = isControlNonApplicable(control.id);
      const nonApplicableBadge = isNonApplicable
        ? `<span class="status-badge status-badge--non-applicable" title="${escapeHtml(t('soa.declaredNonApplicable', 'Déclaré non applicable dans la DdA'))}">
             <i class="fas fa-ban"></i> ${escapeHtml(t('soa.nonApplicable', 'Non applicable'))}
           </span>
           <a href="#" class="soa-link" data-control-id="${escapeHtml(control.id)}" title="${escapeHtml(t('soa.viewInSoa', 'Voir dans la Déclaration d\'applicabilité'))}">
             <i class="fas fa-external-link-alt"></i>
           </a>`
        : "";

      // Attributs ISO 27002:2022
      const domain = getControlDomain(control.reference);
      const domainBadgeClass = domain.toLowerCase().replace(/\s+/g, '-');

      // Efficacité
      const effectivenessLabels = {
        effective: "✓ " + t('effectiveness.effective', 'Efficace'),
        partially: "◐ " + t('effectiveness.partial', 'Partiel'),
        ineffective: "✗ " + t('effectiveness.ineffective', 'Non efficace'),
        not_tested: "○ " + t('effectiveness.notTested', 'Non testé')
      };
      const effectivenessClasses = {
        effective: "effectiveness--good",
        partially: "effectiveness--medium",
        ineffective: "effectiveness--bad",
        not_tested: "effectiveness--neutral"
      };

      const controlTypeLabel = control.isPreventive ? t('controlType.preventive', 'Préventif') : control.isDetective ? t('controlType.detective', 'Détectif') : control.isCorrective ? t('controlType.corrective', 'Correctif') : '';
      const controlTypeIcon = control.isPreventive ? 'fa-shield-halved' : control.isDetective ? 'fa-magnifying-glass' : control.isCorrective ? 'fa-wrench' : '';

      row.innerHTML = `
            <td><strong>${escapeHtml(control.numero)}</strong></td>
            <td>
              <div class="control-title">${escapeHtml(isoCtrlTitle(control))} <i class="fas fa-caret-down control-expand-icon" title="${escapeHtml(t('controls.clickToExpand', 'Cliquer pour développer/réduire'))}"></i></div>
              <div class="control-description control-description--collapsed">${escapeHtml(isoCtrlDesc(control))}</div>
              ${nonApplicableBadge ? `<div class="control-soa-indicator">${nonApplicableBadge}</div>` : ""}
              <div class="control-attrs">
                <span class="badge badge--domain badge--${domainBadgeClass}">${escapeHtml(getTranslatedDomain(domain))}</span>
                ${controlTypeLabel ? `<span class="badge badge--type" title="${escapeHtml(controlTypeLabel)}"><i class="fas ${controlTypeIcon}"></i> ${escapeHtml(controlTypeLabel)}</span>` : ''}
                ${capabilityDisplay ? `<span class="control-capability" title="${escapeHtml(capabilityDisplay)}">${escapeHtml(capabilityDisplay.substring(0, 25))}${capabilityDisplay.length > 25 ? '...' : ''}</span>` : ''}
              </div>
            </td>
            <td>${escapeHtml(control.owner || '-')}</td>
            <td><span class="status-badge ${statusClasses[control.status]}">${statusLabels[control.status]}</span></td>
            <td class="cmm-cell">
              <span class="cmm-badge cmm-badge--${escapeHtml((control.cmm || 'initial').toLowerCase().replace('é', 'e'))}">${escapeHtml(control.cmm || 'Initial')}</span>
              ${control.effectiveness ? `<div class="effectiveness ${effectivenessClasses[control.effectiveness] || ''}">${effectivenessLabels[control.effectiveness] || '-'}</div>` : ''}
            </td>
            <td>${escapeHtml(control.lastEvaluation || '-')}</td>
            <td class="evidence-cell">
              ${control.evidence ? `<span class="evidence-yes" title="${escapeHtml(control.evidence)}"><i class="fas fa-check-circle"></i> ${escapeHtml(t('common.yes', 'Oui'))}</span>` : `<span class="evidence-no"><i class="fas fa-times-circle"></i> ${escapeHtml(t('common.no', 'Non'))}</span>`}
              ${control.kpiRef ? `<span class="kpi-ref" title="KPI: ${escapeHtml(control.kpiRef)}"><i class="fas fa-chart-line"></i></span>` : ''}
            </td>
        `;

      // Gestionnaire pour l'expansion de la description
      const descDiv = row.querySelector('.control-description');
      const expandIcon = row.querySelector('.control-expand-icon');
      const toggleDesc = function(e) {
        e.stopPropagation();
        if (descDiv) {
          descDiv.classList.toggle('control-description--collapsed');
          if (expandIcon) expandIcon.classList.toggle('expanded');
        }
      };
      if (descDiv) descDiv.addEventListener('click', toggleDesc);
      if (expandIcon) expandIcon.addEventListener('click', toggleDesc);

      // Gestionnaire pour le lien vers la SoA
      const soaLink = row.querySelector(".soa-link");
      if (soaLink) {
        soaLink.addEventListener("click", (e) => {
          e.preventDefault();
          e.stopPropagation();
          navigateToSoaControl(control.id);
        });
      }

      row.classList.add("row-clickable");
      row.addEventListener("click", () => {
        editControl(control.id);
      });
      tbody.appendChild(row);
    });
  }
  // Génère le HTML des badges de risques liés pour la SoA
  function renderSoaRiskBadges(linkedRiskIds) {
    if (!linkedRiskIds || linkedRiskIds.length === 0) {
      return '<span class="soa-risks__empty">—</span>';
    }
    return linkedRiskIds.map(riskId => {
      const risk = (appData.risks || []).find(r => r.id === riskId);
      if (!risk) return '';
      const rawLevelClass = (risk.level || 'insignifiant').toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
      const levelClass = /^[a-zA-Z0-9-]+$/.test(rawLevelClass) ? rawLevelClass : 'unknown';
      return `<span class="soa-risks__badge soa-risks__badge--${levelClass}" title="${escapeHtml(risk.id)}">${escapeHtml(risk.title)}</span>`;
    }).join('');
  }

  // Ouvre la modale pour éditer un contrôle SOA complet (même pattern que editAction/editRisk)
  function editSoaControl(controlId) {
    const entry = appData.soa.find(e => e.controlId === controlId);
    var _isoCtrlsSoa = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    const control = _isoCtrlsSoa.find(c => c.id === controlId);
    if (!entry || !control) return;

    // Options des risques pour multiselect
    const riskOptions = (appData.risks || []).map(r => ({
      value: r.id,
      label: `[${r.level}] ${r.title}`
    }));

    // Commentaires existants en lecture seule
    const commentsDisplay = formatCommentsReadonly(entry.comments);

    const fields = [
      { name: "description", label: t('fields.description', 'Description'), type: "textarea", value: isoCtrlTitle(control), readOnly: true },
      { name: "applicable", label: t('soa.colApplicable', 'Applicabilité'), type: "checkbox", value: entry.applicable },
      { name: "source", label: t('soa.colSource', "Source de l'exigence"), type: "select", value: entry.source, options: getSoaSourceOptions() },
      { name: "implStatus", label: t('soa.colImplStatus', "État d'implémentation"), type: "select", value: entry.implStatus, options: getSoaImplStatusOptions() },
      { name: "implDate", label: t('soa.implDate', "Date d'implémentation"), type: "date", value: entry.implDate },
      { name: "owner", label: t('fields.owner', 'Responsable'), type: "text", value: entry.owner, placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(entry.owner), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "linkedRiskIds", label: t('soa.colRisks', 'Risques liés'), type: "multiselect", value: entry.linkedRiskIds || [], options: riskOptions, help: t('soa.multiSelectHelp', 'Maintenez Ctrl (ou Cmd) pour sélectionner plusieurs risques') },
      { name: "evidenceRef", label: t('soa.evidenceRef', 'Référence preuves'), type: "text", value: entry.evidenceRef },
      { name: "lastReviewDate", label: t('soa.lastReviewDate', 'Dernière revue'), type: "date", value: entry.lastReviewDate },
      { name: "justification", label: t('fields.justification', 'Justification'), type: "textarea", value: entry.justification },
      { name: "exclusionReason", label: t('soa.exclusionReason', "Motif d'exclusion"), type: "textarea", value: entry.exclusionReason }
    ];

    // Ajouter commentaires si existants
    if (commentsDisplay) {
      fields.push({ name: "existingComments", label: t('fields.existingComments', 'Commentaires existants'), type: "textarea", value: commentsDisplay, readOnly: true });
    }
    fields.push({ name: "newComment", label: t('fields.newComment', 'Nouveau commentaire'), type: "textarea" });

    openModal(
      t('modals.editSoaControl', 'Modifier le contrôle') + ' — ' + control.reference,
      fields,
      (data) => {
        if (!ensureEditMode("update the SoA")) return false;
        entry.applicable = !!data.applicable;
        entry.source = data.source || '';
        entry.implStatus = data.implStatus || '';
        entry.implDate = data.implDate || '';
        entry.owner = data.owner || '';
        entry.linkedRiskIds = Array.isArray(data.linkedRiskIds) ? data.linkedRiskIds : [];
        entry.evidenceRef = data.evidenceRef || '';
        entry.lastReviewDate = data.lastReviewDate || '';
        entry.justification = data.justification || '';
        entry.exclusionReason = entry.applicable ? '' : (data.exclusionReason || '');
        if (appendComment(entry, data.newComment, 10) === 'max') {
          showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', 10), 'warning');
        }
        addAuditEntry('edit', 'soa', control.reference);
        saveData();
        loadSoaTable();
        updateDashboard();
      }
    );
  }

  // ============================================================
  // Normalisation des entrées SOA pour compatibilité ascendante
  // ============================================================

  /**
   * Normalise une entrée SOA pour s'assurer que tous les champs requis existent
   * Gère la compatibilité avec les anciennes versions qui n'avaient que:
   * - controlId, applicable, justification
   */
  function normalizeSoaEntry(entry) {
    if (!entry || typeof entry !== 'object') {
      return {
        controlId: '',
        applicable: true,
        justification: '',
        linkedRiskIds: [],
        source: '',
        implStatus: '',
        implDate: '',
        exclusionReason: '',
        owner: '',
        evidenceRef: '',
        lastReviewDate: '',
        comments: []
      };
    }
    return {
      controlId: entry.controlId || '',
      applicable: typeof entry.applicable === 'boolean' ? entry.applicable : true,
      justification: entry.justification || '',
      // Nouveaux champs avec valeurs par défaut
      linkedRiskIds: Array.isArray(entry.linkedRiskIds) ? entry.linkedRiskIds : [],
      source: entry.source || '',
      implStatus: entry.implStatus || '',
      implDate: entry.implDate || '',
      exclusionReason: entry.exclusionReason || '',
      owner: entry.owner || '',
      evidenceRef: entry.evidenceRef || '',
      lastReviewDate: entry.lastReviewDate || '',
      comments: Array.isArray(entry.comments) ? entry.comments : []
    };
  }

  /**
   * Normalise un tableau d'entrées SOA
   */
  function normalizeSoa(soaArray) {
    if (!Array.isArray(soaArray)) return [];
    return soaArray.map(entry => normalizeSoaEntry(entry));
  }

  /**
   * S'assure que toutes les entrées SOA dans appData sont normalisées
   */
  function ensureSoaNormalized() {
    if (Array.isArray(appData.soa)) {
      appData.soa = normalizeSoa(appData.soa);
    } else {
      appData.soa = [];
    }
  }

  // Export des fonctions de normalisation SOA
  window.normalizeSoaEntry = normalizeSoaEntry;
  window.normalizeSoa = normalizeSoa;
  window.ensureSoaNormalized = ensureSoaNormalized;

  // ============================================================
  // Normalisation pour TOUTES les autres sections de l'application
  // ============================================================

  /**
   * Normalise une entrée Action
   */
  function normalizeActionEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    const result = {
      id: entry.id || '',
      linkType: entry.linkType || 'control',
      linkId: entry.linkId || entry.controlId || '',
      controlId: entry.controlId || '',
      ncIds: Array.isArray(entry.ncIds) ? entry.ncIds : [],
      title: entry.title || '',
      description: entry.description || '',
      priority: (function(p) { var _pm = { 'normale': 'medium', 'haute': 'high', 'basse': 'low', 'critique': 'critical' }; return _pm[p] || p || 'medium'; })(entry.priority),
      progress: typeof entry.progress === 'number' ? entry.progress : (parseInt(entry.progress, 10) || 0),
      dueDate: entry.dueDate || '',
      assignedTo: entry.assignedTo || '',
      status: entry.status || 'planifie',
      comments: Array.isArray(entry.comments) ? entry.comments :
        (typeof entry.comments === 'string' && entry.comments ?
          [{ text: entry.comments, date: new Date().toISOString() }] : []),
      criticalAssetIds: Array.isArray(entry.criticalAssetIds) ? entry.criticalAssetIds : []
    };
    // Préserver les champs budget si présents (compatibilité NIS2 Plan actions)
    if (entry.budget2026 !== undefined) result.budget2026 = entry.budget2026;
    if (entry.budget2027 !== undefined) result.budget2027 = entry.budget2027;
    if (entry.budget2028 !== undefined) result.budget2028 = entry.budget2028;
    if (entry.budgetCategory) result.budgetCategory = entry.budgetCategory;
    if (entry.workType) result.workType = entry.workType;
    if (entry.owner) result.owner = entry.owner;
    if (entry.phase !== undefined) result.phase = entry.phase;
    return result;
  }

  /**
   * Normalise un tableau d'Actions
   */
  function normalizeActions(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeActionEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Risque
   */
  function normalizeRiskEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    const impact = parseInt(entry.impact, 10) || 1;
    const probability = parseInt(entry.probability, 10) || 1;
    const score = impact * probability;
    return {
      id: entry.id || '',
      title: entry.title || '',
      description: entry.description || '',
      impact: impact,
      probability: probability,
      score: score,
      level: entry.level || calculateRiskLevel(score),
      treatment: entry.treatment || '',
      owner: entry.owner || '', // M17 : propriétaire de risque (sinon perdu à la normalisation)
      comments: Array.isArray(entry.comments) ? entry.comments :
        (typeof entry.comments === 'string' && entry.comments ?
          [{ text: entry.comments, date: new Date().toISOString() }] : []),
      codirDate: entry.codirDate || '',
      criticalAssetIds: Array.isArray(entry.criticalAssetIds) ? entry.criticalAssetIds : []
    };
  }

  /**
   * Normalise un tableau de Risques
   */
  function normalizeRisks(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeRiskEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Menace
   */
  function normalizeThreatEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    return {
      id: entry.id || '',
      title: entry.title || '',
      description: entry.description || '',
      likelihood: entry.likelihood || 1,
      impact: entry.impact || 1,
      score: entry.score || 1,
      level: entry.level || '',
      category: entry.category || '',
      status: entry.status || 'reduction',
      context: entry.context || '',
      comments: Array.isArray(entry.comments) ? entry.comments :
        (typeof entry.comments === 'string' && entry.comments ?
          [{ text: entry.comments, date: new Date().toISOString() }] : []),
      criticalAssetIds: Array.isArray(entry.criticalAssetIds) ? entry.criticalAssetIds : [],
      createdAt: entry.createdAt || '',
      updatedAt: entry.updatedAt || ''
    };
  }

  /**
   * Normalise un tableau de Menaces
   */
  function normalizeThreats(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeThreatEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Non-conformité
   */
  function normalizeNcEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    return {
      id: entry.id || '',
      title: entry.title || '',
      description: entry.description || '',
      type: entry.type || 'Mineure',
      status: entry.status || 'detection',
      detectionDate: entry.detectionDate || '',
      assignedTo: entry.assignedTo || '',
      auditId: entry.auditId || '',
      rootCause: entry.rootCause || '',
      correctiveAction: entry.correctiveAction || '',
      closureDate: entry.closureDate || '',
      criticalAssetIds: Array.isArray(entry.criticalAssetIds) ? entry.criticalAssetIds : [],
      comments: Array.isArray(entry.comments) ? entry.comments :
        (typeof entry.comments === 'string' && entry.comments ?
          [{ text: entry.comments, date: new Date().toISOString() }] : [])
    };
  }

  /**
   * Normalise un tableau de Non-conformités
   */
  function normalizeNonconformities(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeNcEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Audit
   */
  function normalizeAuditEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    return {
      id: entry.id || '',
      title: entry.title || '',
      type: entry.type || 'interne',
      description: entry.description || '',
      scope: entry.scope || '',
      plannedDate: entry.plannedDate || '',
      executionDate: entry.executionDate || '',
      auditor: entry.auditor || '',
      status: entry.status || 'planifie',
      findings: entry.findings || '',
      recommendations: entry.recommendations || '',
      comments: Array.isArray(entry.comments) ? entry.comments :
        (typeof entry.comments === 'string' && entry.comments ?
          [{ text: entry.comments, date: new Date().toISOString() }] : [])
    };
  }

  /**
   * Normalise un tableau d'Audits
   */
  function normalizeAudits(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeAuditEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée KPI
   */
  function normalizeKpiEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    return {
      id: entry.id || '',
      title: entry.title || '',
      description: entry.description || '',
      target: entry.target || '',
      current: entry.current || '',
      unit: entry.unit || '',
      frequency: entry.frequency || 'monthly',
      lastUpdate: entry.lastUpdate || '',
      trend: entry.trend || 'stable',
      comments: typeof entry.comments === 'string' ? entry.comments : (Array.isArray(entry.comments) && entry.comments.length ? entry.comments.map(function(c) { return typeof c === 'object' ? (c.text || '') : String(c); }).join('; ') : ''),
      progress: typeof entry.progress === 'number' ? Math.max(0, Math.min(100, entry.progress)) : Math.max(0, Math.min(100, parseInt(entry.progress, 10) || 0)),
      history: Array.isArray(entry.history) ? entry.history : []
    };
  }

  /**
   * Normalise un tableau de KPIs
   */
  function normalizeKpis(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeKpiEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Revue de direction
   */
  function normalizeReviewEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    return {
      id: entry.id || '',
      date: entry.date || '',
      participants: entry.participants || '',
      inputs: entry.inputs || '',
      decisions: entry.decisions || '',
      comments: Array.isArray(entry.comments) ? entry.comments : (typeof entry.comments === 'string' && entry.comments ? [{ text: entry.comments, date: new Date().toISOString() }] : []),
      nextReviewDate: entry.nextReviewDate || ''
    };
  }

  /**
   * Normalise un tableau de Revues
   */
  function normalizeReviews(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeReviewEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Partie prenante
   */
  function normalizeStakeholderEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    return {
      id: entry.id || ('stakeholder_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5)),
      name: entry.name || '',
      role: entry.role || '',
      contact: entry.contact || '',
      notes: entry.notes || '',
      category: entry.category || '',
      influence: entry.influence || '',
      expectations: entry.expectations || '',
      comments: Array.isArray(entry.comments) ? entry.comments :
        (typeof entry.comments === 'string' && entry.comments ?
          [{ text: entry.comments, date: new Date().toISOString() }] : [])
    };
  }

  /**
   * Normalise un tableau de Parties prenantes
   */
  function normalizeStakeholders(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeStakeholderEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Document
   */
  function normalizeDocumentEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    return {
      id: entry.id || ('doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5)),
      type: entry.type || '',
      title: entry.title || '',
      reference: entry.reference || '',
      version: entry.version || '',
      status: entry.status || 'draft',
      owner: entry.owner || '',
      lastReview: entry.lastReview || '',
      nextReview: entry.nextReview || '',
      location: entry.location || '',
      notes: entry.notes || '',
      category: entry.category || entry.level || '',
      tool: entry.tool || '',
      link: entry.link || '',
      comments: Array.isArray(entry.comments) ? entry.comments :
        (typeof entry.comments === 'string' && entry.comments ?
          [{ text: entry.comments, date: new Date().toISOString() }] : [])
    };
  }

  /**
   * Normalise un tableau de Documents
   */
  function normalizeDocuments(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeDocumentEntry(e)).filter(Boolean);
  }

  /**
   * Normalise une entrée Control ISO 27002
   */
  function normalizeControlEntry(entry) {
    if (!entry || typeof entry !== 'object') return null;
    var normalized = {
      id: entry.id || '',
      reference: entry.reference || '',
      numero: entry.numero || '',
      title: entry.title || '',
      titleEn: entry.titleEn || '',
      measure: entry.measure || '',
      description: entry.description || '',
      descriptionEn: entry.descriptionEn || '',
      recommendations: Array.isArray(entry.recommendations) ? entry.recommendations : [],
      category: entry.category || 'Non classé',
      status: entry.status || 'non_implemente',
      cmm: entry.cmm || 'Initial',
      comments: Array.isArray(entry.comments) ? entry.comments : (typeof entry.comments === 'string' && entry.comments ? [{ text: entry.comments, date: new Date().toISOString() }] : []),
      evidence: entry.evidence || '',
      justificationControl: entry.justificationControl || '',
      subcategory: Array.isArray(entry.subcategory) ? entry.subcategory : [],
      owner: entry.owner || '',
      effectiveness: entry.effectiveness || '',
      implementationDate: entry.implementationDate || '',
      verificationDate: entry.verificationDate || '',
      documentLink: entry.documentLink || '',
      implStatus: entry.implStatus || '',
      lastEvaluation: entry.lastEvaluation || ''
    };
    // FIX-CONTROL-FIELD-LOSS (audit 2026-06-10) : préserver les champs non listés
    // ci-dessus (kpiRef « KPI associé », reviewFrequency « Fréquence de revue », ...),
    // saisis via editControl mais supprimés à chaque normalizeControls() au chargement.
    // Même pattern que FIX-CYFUN-LEVEL-LOSS dans setCyfunAssuranceLevel.
    Object.keys(entry).forEach(function(k) {
      if (!(k in normalized)) normalized[k] = entry[k];
    });
    return normalized;
  }

  /**
   * Normalise un tableau de Controls
   */
  function normalizeControls(list) {
    if (!Array.isArray(list)) return [];
    return list.map(e => normalizeControlEntry(e)).filter(Boolean);
  }

  /**
   * Fonction globale de normalisation de toutes les données importées
   */
  function normalizeAllImportedData(data) {
    return {
      controls: normalizeControls(data.controls),
      soa: normalizeSoa(data.soa),
      actions: normalizeActions(data.actions),
      risks: normalizeRisks(data.risks),
      threats: normalizeThreats(data.threats),
      nonconformities: normalizeNonconformities(data.nonconformities),
      audits: normalizeAudits(data.audits),
      kpis: normalizeKpis(data.kpis),
      reviews: normalizeReviews(data.reviews),
      stakeholders: normalizeStakeholders(data.stakeholders),
      documents: normalizeDocuments(data.documents),
      // Ces sections ont déjà leurs propres normalisations
      criticalAssets: data.criticalAssets,
      projectRisks: data.projectRisks,
      nis2Controls: data.nis2Controls,
      documentReview: data.documentReview,
      nis2Plan: data.nis2Plan,
      soclePillars: data.soclePillars,
      targetMaturity: data.targetMaturity,
      criticalAssetsMeta: data.criticalAssetsMeta,
      riskAnalyses: data.riskAnalyses,
      impactMatrices: data.impactMatrices,
      riskThresholds: data.riskThresholds,
      nextActionId: data.nextActionId,
      title: data.title,
      actionsHistory: data.actionsHistory
    };
  }

  // Export des fonctions de normalisation globales
  window.normalizeActions = normalizeActions;
  window.normalizeRisks = normalizeRisks;
  window.normalizeThreats = normalizeThreats;
  window.normalizeNonconformities = normalizeNonconformities;
  window.normalizeAudits = normalizeAudits;
  window.normalizeKpis = normalizeKpis;
  window.normalizeReviews = normalizeReviews;
  window.normalizeStakeholders = normalizeStakeholders;
  window.normalizeDocuments = normalizeDocuments;
  window.normalizeControls = normalizeControls;
  window.normalizeAllImportedData = normalizeAllImportedData;

  // ============================================================
  // Système générique de commentaires
  // ============================================================
  // Registre des sections supportant les commentaires.
  // Pour ajouter une section : 1) ajouter une entrée ici, 2) normaliser comments:[] dans la section, 3) ajouter un bouton UI
  const COMMENT_SECTIONS = {
    actions:        { dataKey: 'actions',         findBy: 'id',        reload: 'loadActionsTable' },
    risks:          { dataKey: 'risks',           findBy: 'id',        reload: 'loadRisksTable' },
    threats:        { dataKey: 'threats',          findBy: 'id',        reload: 'loadThreatsTable' },
    reviews:        { dataKey: 'reviews',          findBy: 'id',        reload: 'loadReviewsTable' },
    controls:       { dataKey: 'controls',         findBy: 'id',        reload: 'loadControlsTable' },
    nonconformities:{ dataKey: 'nonconformities',   findBy: 'id',        reload: 'loadNonConformitiesTable' },
    audits:         { dataKey: 'audits',           findBy: 'id',        reload: 'loadAuditsTable' },
    kpis:           { dataKey: 'kpis',             findBy: 'id',        reload: 'loadKpiTable' },
    stakeholders:   { dataKey: 'stakeholders',     findBy: 'id',        reload: 'loadStakeholdersTable' },
    documents:      { dataKey: 'documents',        findBy: 'id',        reload: 'loadDocumentsTable' },
    criticalAssets: { dataKey: 'criticalAssets',   findBy: 'id',        reload: 'loadCriticalAssetsTable' },
    soa:            { dataKey: 'soa',              findBy: 'controlId', reload: 'loadSoaTable' },
    docReview:      { dataKey: 'documentReview',   findBy: 'reference', reload: 'loadDocReviewTable' },
    nis2Controls:   { dataKey: 'nis2Controls',     findBy: 'id',        reload: 'loadNis2ControlsTable' },
    recyfControls:  { dataKey: 'recyfControls',    findBy: 'id',        reload: 'loadRecyfControlsTable' },
    projectRisks:   { dataKey: 'projectRisks',     findBy: 'id',        reload: 'loadProjectRisksTable' }
  };

  // --- Socle commun des commentaires (unification 2026-07-10) ---------------
  // Modèle : { text, date ISO, id?, author? } — id et author sont ADDITIFS :
  // les commentaires historiques sans ces champs restent valides partout.

  // Auteur courant (mode serveur multi-utilisateurs) ; '' en mode local.
  function currentCommentAuthor() {
    try {
      var u = window.CSI_AuthManager && window.CSI_AuthManager.getCurrentUser && window.CSI_AuthManager.getCurrentUser();
      if (!u) return '';
      return u.displayName || u.username || u.name || u.email || '';
    } catch (e) { return ''; }
  }

  // Normalisation idempotente d'un champ comments : legacy string -> [{text,date:''}],
  // tableau garanti, entrées invalides filtrées. Ne fabrique jamais de date.
  function normalizeComments(value) {
    if (Array.isArray(value)) {
      return value.filter(function(c) { return c && typeof c === 'object' && typeof c.text === 'string' && c.text; });
    }
    if (typeof value === 'string' && value.trim()) {
      return [{ text: value.trim(), date: '' }];
    }
    return [];
  }

  // Date d'un commentaire pour affichage — '' si absente/corrompue (jamais « Invalid Date »).
  function formatCommentDate(d) {
    if (!d) return '';
    var dt = new Date(d);
    return isNaN(dt.getTime()) ? '' : dt.toLocaleString();
  }

  // Ajout contrôlé d'un commentaire. Retourne true, 'empty' ou 'max'.
  function appendComment(item, text, maxComments) {
    var txt = (text || '').trim();
    if (!txt) return 'empty';
    item.comments = normalizeComments(item.comments);
    if (item.comments.length >= (maxComments || 10)) return 'max';
    var c = { id: generateId('comment'), text: txt, date: new Date().toISOString() };
    var author = currentCommentAuthor();
    if (author) c.author = author;
    item.comments.push(c);
    return true;
  }

  // Historique « [date — auteur] texte » pour les textarea lecture seule des modales d'édition.
  function formatCommentsReadonly(comments) {
    return normalizeComments(comments).map(function(c) {
      var meta = [formatCommentDate(c.date), c.author || ''].filter(Boolean).join(' — ');
      return (meta ? '[' + meta + '] ' : '') + c.text;
    }).join('\n');
  }

  window.normalizeComments = normalizeComments;
  window.appendComment = appendComment;
  window.formatCommentsReadonly = formatCommentsReadonly;

  /**
   * Gestion générique des commentaires pour toutes les sections.
   * @param {string} sectionKey - Clé dans COMMENT_SECTIONS
   * @param {string} itemId - Identifiant de l'élément (id, controlId, reference selon findBy)
   * @param {object} [options] - { titlePrefix, maxComments }
   */
  function manageComments(sectionKey, itemId, options) {
    var cfg = COMMENT_SECTIONS[sectionKey];
    if (!cfg) { void 0; return; }
    var opts = options || {};
    var maxComments = opts.maxComments || 10;
    var titlePrefix = opts.titlePrefix || t('comments.manage', 'Commentaires');

    if (!ensureEditMode("manage comments")) return;

    var dataArray = appData[cfg.dataKey];
    if (!Array.isArray(dataArray)) return;
    var item = dataArray.find(function(e) { return e[cfg.findBy] === itemId; });
    if (!item) return;
    item.comments = normalizeComments(item.comments);

    var commentsHtml = '<div class="csi-comments-list">';
    if (item.comments.length === 0) {
      commentsHtml += '<p class="csi-comments-empty">' + t('comments.none', 'Aucun commentaire.') + '</p>';
    } else {
      item.comments.forEach(function(c, i) {
        var meta = [formatCommentDate(c.date), c.author || ''].filter(Boolean).join(' — ');
        commentsHtml += '<div class="csi-comment-item">';
        commentsHtml += '<div class="csi-comment-item__body"><small class="csi-comment-item__date">' + escapeHtml(meta) + '</small>';
        commentsHtml += '<p class="csi-comment-item__text">' + escapeHtml(c.text) + '</p></div>';
        // data-cid (id du commentaire) prioritaire ; data-idx = repli pour les commentaires legacy sans id
        commentsHtml += '<button type="button" class="icon-btn icon-btn--danger csi-comment-delete" data-cid="' + escapeHtml(c.id || '') + '" data-idx="' + i + '" title="' + t('common.delete', 'Supprimer') + '"><i class="fas fa-trash"></i></button>';
        commentsHtml += '</div>';
      });
    }
    commentsHtml += '</div>';

    openModal(titlePrefix, [
      { name: '_commentsList', label: '', type: 'html', value: commentsHtml },
      { name: 'newComment', label: t('comments.new', 'Nouveau commentaire'), type: 'textarea' }
    ], function(data) {
      var res = appendComment(item, data.newComment, maxComments);
      if (res === 'max') {
        showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', maxComments), 'warning');
        return false;
      }
      if (res === true) {
        saveData();
        if (typeof window[cfg.reload] === 'function') window[cfg.reload]();
        showToast2(t('comments.added', 'Commentaire ajouté.'), 'success');
      }
    }, null, {
      saveLabel: t('comments.add', 'Ajouter le commentaire'),
      cancelLabel: t('comments.close', 'Fermer')
    });

    setTimeout(function() {
      var modalBody = document.getElementById('modalBody');
      if (!modalBody) return;
      modalBody.querySelectorAll('.csi-comment-delete').forEach(function(btn) {
        btn.addEventListener('click', function() {
          // Suppression par id de commentaire (robuste en co-édition) ; repli sur
          // l'index de position pour les commentaires legacy sans id.
          var cid = btn.getAttribute('data-cid');
          var idx = -1;
          if (cid) idx = item.comments.findIndex(function(c) { return c && c.id === cid; });
          if (idx < 0) idx = parseInt(btn.getAttribute('data-idx'), 10);
          if (idx >= 0 && idx < item.comments.length) {
            showConfirmModal(
              t('confirms.deleteTitle', 'Supprimer ?'),
              t('comments.deleteConfirm', 'Supprimer ce commentaire ?'),
              function() {
                item.comments.splice(idx, 1);
                saveData();
                closeModal();
                manageComments(sectionKey, itemId, options);
              },
              { type: 'danger' }
            );
          }
        });
      });
    }, 100);
  }
  window.manageComments = manageComments;
  window.COMMENT_SECTIONS = COMMENT_SECTIONS;

  // Options pour les sources de sélection SOA (ISO 27001:2022 clause 6.1.3)
  function getSoaSourceOptions() {
    return [
      { value: "", label: t('soaSources.select') },
      { value: "risk", label: t('soaSources.risk') },
      { value: "legal", label: t('soaSources.legal') },
      { value: "contractual", label: t('soaSources.contractual') },
      { value: "best_practice", label: t('soaSources.bestPractice') },
      { value: "regulatory", label: t('soaSources.regulatory') },
      { value: "business", label: t('soaSources.business') }
    ];
  }
  // Options pour l'état d'implémentation SOA
  function getSoaImplStatusOptions() {
    return [
      { value: "", label: t('soaImplStatuses.select') },
      { value: "not_started", label: t('soaImplStatuses.notStarted') },
      { value: "planned", label: t('soaImplStatuses.planned') },
      { value: "in_progress", label: t('soaImplStatuses.inProgress') },
      { value: "implemented", label: t('soaImplStatuses.implemented') },
      { value: "verified", label: t('soaImplStatuses.verified') }
    ];
  }
  // (audit global 2026-07-03) Statut d'implémentation EFFECTIF d'une entrée SoA. implStatus est
  // un champ SoA-spécifique optionnel (peut différer intentionnellement) ; s'il est VIDE (cas
  // courant : le RSSI renseigne l'implémentation dans l'onglet Contrôles, pas la SoA), on DÉRIVE
  // depuis control.status — sinon la Déclaration d'Applicabilité (livrable ISO 27001 §6.1.3)
  // affichait 0% implémenté alors que le SMSI l'était. L'override explicite reste prioritaire.
  function _soaEffectiveImplStatus(entry, control) {
    if (entry && entry.implStatus) return entry.implStatus;
    var cs = control && control.status;
    if (cs === 'verifie') return 'verified';
    if (cs === 'implemente') return 'implemented';
    if (cs === 'en_cours') return 'in_progress';
    return '';
  }
  // Export SoA option functions and dynamic getters (labels refresh after language change)
  window.getSoaSourceOptions = getSoaSourceOptions;
  window.getSoaImplStatusOptions = getSoaImplStatusOptions;
  Object.defineProperty(window, 'SOA_SOURCE_OPTIONS', { get: getSoaSourceOptions, configurable: true });
  Object.defineProperty(window, 'SOA_IMPL_STATUS_OPTIONS', { get: getSoaImplStatusOptions, configurable: true });

  // Fonction pour obtenir le domaine d'un contrôle
  function getControlDomain(reference) {
    if (!reference) return "Non classé";
    const prefix = reference.split('.')[0];
    return DOMAIN_BY_PREFIX[prefix] || "Non classé";
  }

  function loadSoaTable() {
    const tbody = document.getElementById("soaTableBody");
    if (!tbody) return;

    // Protection : s'assurer que le module SOA reste actif pendant le rechargement
    const soaModule = document.getElementById("soa");
    const wasActive = soaModule && soaModule.classList.contains("module--active");

    tbody.innerHTML = "";

    const _f = SectionToolbar.getFilters("soa");
    const _q = (SectionToolbar.getSearch("soa") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const applicabilityFilter = _f.applicability || "";
    const domainFilter = _f.domain || "";

    const _s = SectionToolbar.getSort("soa");
    const _sortKey = _s ? _s.key : "reference";
    const _sortDir = _s ? _s.dir : "asc";
    const sortedControls = [...appData.controls].sort((a, b) => {
      let diff = 0;
      switch (_sortKey) {
        case "applicability": {
          const entryA = appData.soa.find(e => e.controlId === a.id);
          const entryB = appData.soa.find(e => e.controlId === b.id);
          diff = SortUtils.text(entryA?.applicable ? "1" : "0", entryB?.applicable ? "1" : "0");
          break;
        }
        case "implementation": {
          const entryA = appData.soa.find(e => e.controlId === a.id);
          const entryB = appData.soa.find(e => e.controlId === b.id);
          diff = SortUtils.text(entryA?.implStatus || "", entryB?.implStatus || "");
          break;
        }
        default: diff = SortUtils.reference(a.reference, b.reference); break;
      }
      return _sortDir === "desc" ? -diff : diff;
    });

    // Stats pour les indicateurs de couverture
    let applicableCount = 0;
    let notApplicableCount = 0;
    let implementedCount = 0;
    const domainStats = {};

    sortedControls.forEach((control) => {
      let entry = appData.soa.find(e => e.controlId === control.id);
      if (!entry) {
        entry = {
          controlId: control.id,
          applicable: true,
          justification: "",
          linkedRiskIds: [],
          source: "",
          implStatus: "",
          implDate: "",
          exclusionReason: "",
          owner: "",
          evidenceRef: "",
          lastReviewDate: ""
        };
        appData.soa.push(entry);
      }
      // S'assurer que les nouveaux champs existent
      if (!entry.linkedRiskIds) entry.linkedRiskIds = [];
      if (!entry.source) entry.source = "";
      if (!entry.implStatus) entry.implStatus = "";
      if (!entry.implDate) entry.implDate = "";
      if (!entry.exclusionReason) entry.exclusionReason = "";
      if (!entry.owner) entry.owner = "";
      if (!entry.evidenceRef) entry.evidenceRef = "";
      if (!entry.lastReviewDate) entry.lastReviewDate = "";

      const domain = getControlDomain(control.reference);

      // Stats
      var _effImpl = _soaEffectiveImplStatus(entry, control); // (audit 2026-07-03) fallback control.status
      if (entry.applicable) {
        applicableCount++;
        if (_effImpl === "implemented" || _effImpl === "verified") {
          implementedCount++;
        }
      } else {
        notApplicableCount++;
      }

      // Stats par domaine
      if (!domainStats[domain]) {
        domainStats[domain] = { total: 0, applicable: 0, implemented: 0 };
      }
      domainStats[domain].total++;
      if (entry.applicable) {
        domainStats[domain].applicable++;
        if (_effImpl === "implemented" || _effImpl === "verified") {
          domainStats[domain].implemented++;
        }
      }

      // Filtres
      if (applicabilityFilter === "selected" && !entry.applicable) return;
      if (applicabilityFilter === "not_selected" && entry.applicable) return;
      if (domainFilter && domain !== domainFilter) return;
      if (_q && ((control.reference || "") + " " + (control.title || "")).toLowerCase().indexOf(_q) < 0) return;

      const riskBadgesHtml = renderSoaRiskBadges(entry.linkedRiskIds);

      const row = document.createElement("tr");
      row.className = entry.applicable ? "soa-row--applicable" : "soa-row--not-applicable";
      row.dataset.controlId = control.id;
      row.style.cursor = "pointer";

      // Ensure comments array exists
      if (!Array.isArray(entry.comments)) entry.comments = [];
      var soaCommentsBadge = entry.comments.length > 0 ? '<span class="csi-comment-count">' + entry.comments.length + '</span>' : '';

      // Labels lisibles pour applicabilité, source et implémentation
      var _applicableBadge = entry.applicable
        ? '<span class="badge badge--success">\u2713 ' + escapeHtml(t('soa.applicable', 'Applicable')) + '</span>'
        : '<span class="badge badge--muted">\u2717 ' + escapeHtml(t('soa.nonApplicable', 'Non applicable')) + '</span>';
      var _sourceOpt = getSoaSourceOptions().find(function(o) { return o.value === entry.source; });
      var _sourceText = _sourceOpt && _sourceOpt.value ? _sourceOpt.label : '';
      var _implOpt = getSoaImplStatusOptions().find(function(o) { return o.value === entry.implStatus; });
      var _implText = _implOpt && _implOpt.value ? _implOpt.label : '';
      var _justifPreview = (entry.justification || '').substring(0, 80);
      var _justifSuffix = (entry.justification || '').length > 80 ? '\u2026' : '';

      // Structure à 6 colonnes — affichage lecture seule (édition via modale au clic)
      var _cellsHtml = '<td class="soa-cell-control">'
        + '<div class="soa-control-header">'
        + '<strong class="soa-control-ref">' + escapeHtml(control.numero + ' ' + isoCtrlTitle(control)) + '</strong>'
        + ' <span class="badge badge--domain badge--' + domain.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '') + '">' + escapeHtml(getTranslatedDomain(domain)) + '</span>'
        + '</div>'
        + '</td>';
      _cellsHtml += '<td class="soa-cell-applicability">' + _applicableBadge
        + (_sourceText ? '<div class="soa-meta-line">' + escapeHtml(_sourceText) + '</div>' : '')
        + (!entry.applicable && entry.exclusionReason ? '<div class="soa-meta-line text-muted">' + escapeHtml((entry.exclusionReason || '').substring(0, 60)) + ((entry.exclusionReason || '').length > 60 ? '\u2026' : '') + '</div>' : '')
        + '</td>';
      _cellsHtml += '<td class="soa-cell-impl">'
        + (_implText ? '<span class="badge badge--outline">' + escapeHtml(_implText) + '</span>' : '<span class="text-muted">\u2014</span>')
        + (entry.owner ? '<div class="soa-meta-line">' + escapeHtml(entry.owner) + '</div>' : '')
        + (entry.implDate ? '<div class="soa-meta-line">' + escapeHtml(entry.implDate) + '</div>' : '')
        + '</td>';
      _cellsHtml += '<td class="soa-cell-risks"><div class="soa-risks">' + riskBadgesHtml + '</div></td>';
      _cellsHtml += '<td class="soa-cell-docs">'
        + (entry.evidenceRef ? escapeHtml(entry.evidenceRef) : '<span class="text-muted">\u2014</span>')
        + (entry.lastReviewDate ? '<div class="soa-meta-line">' + escapeHtml(entry.lastReviewDate) + '</div>' : '')
        + (soaCommentsBadge ? '<span class="soa-meta-line">' + soaCommentsBadge + '</span>' : '')
        + '</td>';
      _cellsHtml += '<td class="soa-cell-justification">'
        + (_justifPreview ? escapeHtml(_justifPreview) + _justifSuffix : '<span class="text-muted">\u2014</span>')
        + '</td>';
      row.innerHTML = _cellsHtml;

      // Clic sur la ligne → édition modale (même pattern que editAction/editRisk)
      row.classList.add("row-clickable");
      row.addEventListener("click", function() {
        editSoaControl(control.id);
      });

      tbody.appendChild(row);
    });

    // Mettre à jour les indicateurs de couverture
    updateSoaCoverageStats(applicableCount, notApplicableCount, implementedCount, domainStats);

    // Protection : restaurer l'état actif du module SOA si nécessaire
    if (wasActive && soaModule && !soaModule.classList.contains("module--active")) {
      void 0;
      document.querySelectorAll(".module").forEach(m => m.classList.remove("module--active"));
      soaModule.classList.add("module--active");
      const soaNav = document.querySelector('.nav__item[data-tab="soa"]');
      if (soaNav) {
        document.querySelectorAll(".nav__item").forEach(n => n.classList.remove("nav__item--active"));
        soaNav.classList.add("nav__item--active");
      }
    }
  }

  // Mettre à jour les statistiques de couverture SOA
  function updateSoaCoverageStats(applicable, notApplicable, implemented, domainStats) {
    safeSetText("soaApplicableCount", applicable);
    safeSetText("soaNotApplicableCount", notApplicable);

    const implPercent = applicable > 0 ? Math.round((implemented / applicable) * 100) : 0;
    safeSetText("soaImplementedCount", implPercent + "%");

    const implBar = document.getElementById("soaImplementedBar");
    if (implBar) {
      implBar.style.width = implPercent + "%";
      implBar.className = "soa-coverage__fill";
      if (implPercent >= 80) implBar.classList.add("soa-coverage__fill--good");
      else if (implPercent >= 50) implBar.classList.add("soa-coverage__fill--medium");
      else implBar.classList.add("soa-coverage__fill--low");
    }

    // Couverture par domaine
    const domainsContainer = document.getElementById("soaDomainsCoverage");
    if (domainsContainer) {
      domainsContainer.innerHTML = Object.entries(domainStats).map(([domain, stats]) => {
        const pct = stats.applicable > 0 ? Math.round((stats.implemented / stats.applicable) * 100) : 0;
        return `<div class="soa-domain-stat">
          <span class="soa-domain-stat__label">${escapeHtml(getTranslatedDomain(domain))}</span>
          <span class="soa-domain-stat__value">${pct}%</span>
        </div>`;
      }).join("");
    }
  }

  // Options pour le statut de conformité Doc Review
  function getDocReviewStatusOptions() {
    return [
      { value: "", label: t('docReviewStatuses.select') },
      { value: "conforme", label: t('docReviewStatuses.conforme'), icon: "check-circle", class: "success" },
      { value: "partiel", label: t('docReviewStatuses.partiel'), icon: "exclamation-circle", class: "warning" },
      { value: "non_conforme", label: t('docReviewStatuses.nonConforme'), icon: "times-circle", class: "danger" },
      { value: "na", label: t('docReviewStatuses.na'), icon: "minus-circle", class: "muted" }
    ];
  }
  // DOC_REVIEW_STATUS_OPTIONS computed dynamically (like SOA options) to support language switching

  // Retourne true si dateStr (AAAA-MM-JJ ou date parseable) est strictement antérieure à
  // aujourd'hui. Même critère que le calcul overdue du plan d'actions, factorisé pour
  // homogénéiser la mise en évidence des échéances dépassées (.cell-overdue).
  function _csiIsPastDate(dateStr) {
    if (!dateStr) return false;
    var s = String(dateStr);
    if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
      var now = new Date();
      var todayIso = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0");
      return s.slice(0, 10) < todayIso;
    }
    var d = new Date(s);
    if (Number.isNaN(d.getTime())) return false;
    d.setHours(0, 0, 0, 0);
    var t0 = new Date(); t0.setHours(0, 0, 0, 0);
    return d < t0;
  }

  function loadDocReviewTable() {
    const tbody = document.getElementById("docReviewTableBody");
    if (!tbody) return;
    tbody.innerHTML = "";

    const _f = SectionToolbar.getFilters("docReview");
    const statusFilter = _f.status || "";

    // Stats pour les indicateurs
    const _s = SectionToolbar.getSort("docReview");
    let conformeCount = 0;
    let partielCount = 0;
    let nonConformeCount = 0;
    let totalItems = 0;

    // Tri (la toolbar propose Clause / Statut / Dernière revue). L'ordre « clause » (défaut)
    // conserve le groupement par chapitre du référentiel ; trier par statut ou dernière revue
    // n'a de sens qu'à plat, on retire donc les en-têtes de chapitre dans ces cas.
    let _itemsToRender = DOCUMENT_REVIEW_ITEMS;
    if (_s && (_s.key === "status" || _s.key === "lastReview")) {
      const _dir = _s.dir === "desc" ? -1 : 1;
      const _statusRank = { conforme: 0, partiel: 1, non_conforme: 2, na: 3, "": 4 };
      const _entryOf = (it) => appData.documentReview.find((e) => e.reference === it.reference) || {};
      _itemsToRender = DOCUMENT_REVIEW_ITEMS
        .filter((it) => it.type !== "chapter")
        .slice()
        .sort((a, b) => {
          const ea = _entryOf(a), eb = _entryOf(b);
          if (_s.key === "status") {
            const ra = _statusRank[ea.status] != null ? _statusRank[ea.status] : 5;
            const rb = _statusRank[eb.status] != null ? _statusRank[eb.status] : 5;
            return (ra - rb) * _dir;
          }
          // lastReview : chaînes date comparables ; les valeurs vides toujours en dernier
          const va = ea.lastReview || "", vb = eb.lastReview || "";
          if (!va && !vb) return 0;
          if (!va) return 1;
          if (!vb) return -1;
          return (va < vb ? -1 : va > vb ? 1 : 0) * _dir;
        });
    }

    _itemsToRender.forEach((item, _drIdx) => {
      if (item.type === "chapter") {
        // Resolve chapter title from framework data (bilingual) or fallback to item.title
        var _chTitle = item.title;
        var _mgmtFwCh = _getActiveManagementFramework();
        if (_mgmtFwCh && _mgmtFwCh.clauses && _mgmtFwCh.clauses[_drIdx]) {
          var _chData = _mgmtFwCh.clauses[_drIdx];
          if (_chData.type === 'chapter' && _chData.chapterId) {
            var _isEnCh = (typeof currentLang !== 'undefined' && currentLang === 'en') || (document.documentElement.lang === 'en');
            var _chInfo = (_mgmtFwCh.chapters || []).find(function(c) { return c.id === _chData.chapterId; });
            if (_chInfo) _chTitle = (_isEnCh && _chInfo.titleEn) ? _chInfo.titleEn : _chInfo.title;
          }
        }
        if (!_chTitle) _chTitle = item.title;
        const row = document.createElement("tr");
        row.className = "table-section";
        // escapeHtml sanitizes _chTitle before insertion (existing pattern)
        row.innerHTML = '<td colspan="5" class="doc-review-chapter-header">' + escapeHtml(_chTitle) + '</td>';
        tbody.appendChild(row);
      } else {
        let entry = appData.documentReview.find(e => e.reference === item.reference);
        if (!entry) {
          entry = {
            reference: item.reference,
            justification: "",
            tool: "",
            status: "",
            responsible: "",
            lastReview: "",
            nextReview: "",
            documentLink: "",
            comments: []
          };
          appData.documentReview.push(entry);
        }
        // S'assurer que les nouveaux champs existent
        if (!entry.status) entry.status = "";
        if (!entry.responsible) entry.responsible = "";
        if (!entry.lastReview) entry.lastReview = "";
        if (!entry.nextReview) entry.nextReview = "";
        if (!entry.documentLink) entry.documentLink = "";
        if (!Array.isArray(entry.comments)) entry.comments = [];

        // Stats
        totalItems++;
        if (entry.status === "conforme") conformeCount++;
        else if (entry.status === "partiel") partielCount++;
        else if (entry.status === "non_conforme") nonConformeCount++;

        // Filtre par statut
        if (statusFilter && entry.status !== statusFilter) return;

        // Classe de ligne selon statut
        var rowClass = "";
        if (entry.status === "conforme") rowClass = "doc-review-row--conforme";
        else if (entry.status === "partiel") rowClass = "doc-review-row--partiel";
        else if (entry.status === "non_conforme") rowClass = "doc-review-row--non-conforme";
        else if (entry.status === "na") rowClass = "doc-review-row--na";

        // Extraire le numéro de clause (ex: "4.1" de "4.1 Compréhension de l'organisation...")
        var refParts = item.reference.split(' ');
        var clauseNum = refParts[0] || item.reference;
        // Look up clause data from active management framework (bilingual)
        var _mgmtFw = _getActiveManagementFramework();
        var _fwClause = _mgmtFw ? (_mgmtFw.clauses || []).find(function(c) { return c.reference === clauseNum; }) : null;
        // i18n: framework data takes priority, then reference text as fallback
        var _isEn = (typeof currentLang !== 'undefined' && currentLang === 'en') || (document.documentElement.lang === 'en');
        var clauseTitle = (_fwClause && _isEn && _fwClause.titleEn) ? _fwClause.titleEn : (_fwClause ? _fwClause.title : '') || refParts.slice(1).join(' ') || '';
        var clauseDesc = (_fwClause && _isEn && _fwClause.descriptionEn) ? _fwClause.descriptionEn : (_fwClause ? _fwClause.description : '') || item.description;

        var commentsBadge = entry.comments.length > 0 ? '<span class="csi-comment-count">' + entry.comments.length + '</span>' : '';

        // Labels lisibles pour statut
        var _statusOpt = getDocReviewStatusOptions().find(function(o) { return o.value === entry.status; });
        var _statusText = _statusOpt && _statusOpt.value ? _statusOpt.label : '';
        var _statusIcon = _statusOpt && _statusOpt.icon ? '<i class="fas fa-' + _statusOpt.icon + '"></i> ' : '';
        var _statusBadgeClass = _statusOpt && _statusOpt.class ? 'badge--' + _statusOpt.class : '';
        var _justifPreview = (entry.justification || '').substring(0, 80);
        var _justifSuffix = (entry.justification || '').length > 80 ? '\u2026' : '';
        var _toolPreview = (entry.tool || '').substring(0, 60);
        var _toolSuffix = (entry.tool || '').length > 60 ? '\u2026' : '';

        var row = document.createElement("tr");
        row.className = rowClass;
        row.dataset.ref = item.reference;
        row.style.cursor = "pointer";

        // Structure à 5 colonnes — affichage lecture seule (édition via modale au clic)
        var _drCells = '<td class="doc-review-cell-clause">'
          + '<span class="doc-review-clause-ref">' + escapeHtml(clauseNum) + '</span>'
          + (clauseTitle ? '<div class="doc-review-clause-title" style="font-weight:600;margin-bottom:2px">' + escapeHtml(clauseTitle) + '</div>' : '')
          + '<div class="doc-review-clause-desc">' + escapeHtml(clauseDesc) + '</div>'
          + '</td>';
        _drCells += '<td>'
          + (_statusText ? '<span class="badge ' + _statusBadgeClass + '">' + _statusIcon + escapeHtml(_statusText) + '</span>' : '<span class="text-muted">\u2014</span>')
          + (entry.responsible ? '<div class="soa-meta-line">' + escapeHtml(entry.responsible) + '</div>' : '')
          + '</td>';
        _drCells += '<td>'
          + (_justifPreview ? escapeHtml(_justifPreview) + _justifSuffix : '<span class="text-muted">\u2014</span>')
          + '</td>';
        _drCells += '<td>'
          + (_toolPreview ? escapeHtml(_toolPreview) + _toolSuffix : '<span class="text-muted">\u2014</span>')
          + (entry.documentLink && isSafeUrl(entry.documentLink) ? '<br><a href="' + escapeHtml(entry.documentLink) + '" target="_blank" rel="noopener noreferrer" class="doc-review-link" onclick="event.stopPropagation()"><i class="fas fa-external-link-alt"></i> ' + escapeHtml(t('common.open', 'Ouvrir')) + '</a>' : '')
          + '</td>';
        var _drNextOverdue = _csiIsPastDate(entry.nextReview);
        _drCells += (_drNextOverdue ? '<td class="cell-overdue" title="' + escapeHtml(t('docReview.reviewOverdue', 'Revue en retard')) + '">' : '<td>')
          + (entry.lastReview ? escapeHtml(entry.lastReview) : '<span class="text-muted">\u2014</span>')
          + (entry.nextReview ? '<div class="soa-meta-line">' + escapeHtml(t('docReview.nextReviewLabel', 'Prochaine')) + ': ' + escapeHtml(entry.nextReview) + '</div>' : '')
          + (commentsBadge ? ' ' + commentsBadge : '')
          + '</td>';
        row.innerHTML = _drCells;

        // Clic sur la ligne → édition modale (même pattern que editAction/editRisk)
        row.classList.add("row-clickable");
        row.addEventListener("click", function() {
          editDocReviewEntry(item.reference);
        });

        tbody.appendChild(row);
      }
    });

    // Mettre à jour les statistiques
    updateDocReviewStats(conformeCount, partielCount, nonConformeCount, totalItems);
  }

  // Mettre à jour les statistiques Doc Review
  function updateDocReviewStats(conforme, partiel, nonConforme, total) {
    safeSetText("docReviewConformeCount", conforme);
    safeSetText("docReviewPartielCount", partiel);
    safeSetText("docReviewNonConformeCount", nonConforme);

    const conformePercent = total > 0 ? Math.round((conforme / total) * 100) : 0;
    safeSetText("docReviewProgressValue", conformePercent + "%");

    const progressBar = document.getElementById("docReviewProgressBar");
    if (progressBar) {
      progressBar.style.width = conformePercent + "%";
      progressBar.className = "doc-review-stats__fill";
      if (conformePercent >= 80) progressBar.classList.add("doc-review-stats__fill--good");
      else if (conformePercent >= 50) progressBar.classList.add("doc-review-stats__fill--medium");
      else progressBar.classList.add("doc-review-stats__fill--low");
    }
  }

  // Ouvre la modale pour éditer une clause de la revue normative (même pattern que editAction/editRisk)
  function editDocReviewEntry(reference) {
    var item = DOCUMENT_REVIEW_ITEMS.find(function(i) { return i.reference === reference; });
    var entry = appData.documentReview.find(function(e) { return e.reference === reference; });
    if (!item || !entry) return;

    // Commentaires existants en lecture seule
    var commentsDisplay = formatCommentsReadonly(entry.comments);

    var refParts = reference.split(' ');
    var clauseNum = refParts[0] || reference;

    var fields = [
      { name: "status", label: t('docReview.status', 'Statut de conformité'), type: "select", value: entry.status, options: getDocReviewStatusOptions() },
      { name: "responsible", label: t('fields.owner', 'Responsable'), type: "text", value: entry.responsible, placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(entry.responsible), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "justification", label: t('fields.justification', 'Justification'), type: "textarea", value: entry.justification },
      { name: "tool", label: t('docReview.tool', 'Document / preuve'), type: "textarea", value: entry.tool },
      { name: "documentLink", label: t('docReview.documentLink', 'Lien document'), type: "text", value: entry.documentLink },
      { name: "lastReview", label: t('docReview.lastReview', 'Dernière revue'), type: "date", value: entry.lastReview },
      { name: "nextReview", label: t('docReview.nextReview', 'Prochaine revue'), type: "date", value: entry.nextReview }
    ];

    // Ajouter commentaires si existants
    if (commentsDisplay) {
      fields.push({ name: "existingComments", label: t('fields.existingComments', 'Commentaires existants'), type: "textarea", value: commentsDisplay, readOnly: true });
    }
    fields.push({ name: "newComment", label: t('fields.newComment', 'Nouveau commentaire'), type: "textarea" });

    openModal(
      t('modals.editDocReview', 'Modifier la clause') + ' \u2014 ' + clauseNum,
      fields,
      function(data) {
        if (!ensureEditMode("update the normative review")) return false;
        entry.status = data.status || '';
        entry.responsible = data.responsible || '';
        entry.justification = data.justification || '';
        entry.tool = data.tool || '';
        entry.documentLink = data.documentLink || '';
        entry.lastReview = data.lastReview || '';
        entry.nextReview = data.nextReview || '';
        if (appendComment(entry, data.newComment, 10) === 'max') {
          showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', 10), 'warning');
        }
        addAuditEntry('edit', 'docReview', clauseNum);
        saveData();
        loadDocReviewTable();
        updateDashboard();
      }
    );
  }

  window.loadDocReviewTable = loadDocReviewTable;
  window.editDocReviewEntry = editDocReviewEntry;

  // M4 (audit 2026-07-10) : rebranche le moteur de scoring CyFun (computeCyfunScoring —
  // seuils CCB 2.5/3/3.5 + minimum par catégorie) sur un encart verdict VISIBLE, tel que
  // le wizard le promet à l'onboarding. Sans cela overallPass était calculé mais jamais
  // affiché (fonction morte). Rendu 100 % DOM (aucune injection HTML).
  function renderCyfunVerdict() {
    var host = document.getElementById('cyfunSummary');
    if (!host) return;
    host.textContent = '';
    var nis2Enabled = window.CSI_FrameworkMapper && CSI_FrameworkMapper.isEnabled('nis2');
    if (!nis2Enabled || (appData.nis2Referential || 'cyfun2025') !== 'cyfun2025') return;
    var s = (typeof computeCyfunScoring === 'function') ? computeCyfunScoring() : null;
    if (!s) return;
    var pass = !!s.overallPass;
    var okColor = '#059669', koColor = '#dc2626', warnColor = '#d97706';
    var wrap = document.createElement('div');
    wrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin:6px 0 14px;padding:12px 16px;border:1px solid var(--color-border,#e5e7eb);border-radius:10px;background:var(--color-surface,#fff);';

    var verdictBox = document.createElement('div');
    verdictBox.style.cssText = 'display:flex;flex-direction:column;align-items:center;min-width:120px;';
    var badge = document.createElement('div');
    badge.style.cssText = 'font-size:1.05rem;font-weight:800;padding:4px 12px;border-radius:12px;color:#fff;background:' + (pass ? okColor : koColor) + ';';
    badge.textContent = pass ? t('cyfun.compliant', 'CONFORME') : t('cyfun.nonCompliant', 'NON CONFORME');
    var lvl = document.createElement('div');
    lvl.style.cssText = 'font-size:0.72rem;color:var(--color-text-secondary,#6b7280);margin-top:4px;text-align:center;';
    lvl.textContent = (s.levelLabel || s.level || '') + ' · ' + t('cyfun.threshold', 'Seuil de conformité') + ' ' + (s.thresholds.total || 0) + '/5';
    verdictBox.appendChild(badge); verdictBox.appendChild(lvl);
    wrap.appendChild(verdictBox);

    var metrics = document.createElement('div');
    metrics.style.cssText = 'display:flex;flex-wrap:wrap;gap:14px;font-size:0.82rem;';
    var addMetric = function(label, value, ok) {
      var m = document.createElement('div');
      m.style.cssText = 'display:flex;flex-direction:column;gap:2px;';
      var v = document.createElement('span');
      v.style.cssText = 'font-weight:700;color:' + (ok ? okColor : koColor) + ';';
      v.textContent = value;
      var l = document.createElement('span');
      l.style.cssText = 'color:var(--color-text-secondary,#6b7280);';
      l.textContent = label;
      m.appendChild(v); m.appendChild(l);
      metrics.appendChild(m);
    };
    addMetric(t('cyfun.avgScore', 'Score moyen'), s.avgTotal + '/5', s.passes.total);
    if (s.keyMeasureCount > 0) addMetric(t('cyfun.keyMeasuresScore', 'Score mesures clés'), s.avgKeyTotal + '/5 (≥ ' + (s.thresholds.keyMeasures || 0) + ')', s.passes.keyMeasures);
    if (s.thresholds.categoryMin) addMetric(t('cyfun.categoryMin', 'Minimum par catégorie'), '≥ ' + s.thresholds.categoryMin, s.passes.categories);
    addMetric(t('cyfun.coverage', 'Couverture'), s.scoredRequirements + ' / ' + s.totalRequirements, s.scoredRequirements > 0);
    wrap.appendChild(metrics);
    host.appendChild(wrap);

    if (s.unevaluatedCategories && s.unevaluatedCategories.length) {
      var warn = document.createElement('div');
      warn.style.cssText = 'margin-top:-8px;margin-bottom:12px;font-size:0.78rem;color:' + warnColor + ';';
      warn.textContent = t('cyfun.unevaluatedWarn', 'Mesure non évaluée = 0 dans le calcul du seuil (méthode CCB). Catégories non évaluées : {{n}}.').replace('{{n}}', s.unevaluatedCategories.length);
      host.appendChild(warn);
    }
  }

  function loadNis2ControlsTable() {
    const tbody = document.getElementById("nis2ControlsTableBody");
    if (!tbody) return;
    // FEAT-CYFUN-NL : réaligne les textes snapshotés sur le référentiel (langue courante).
    if (typeof refreshNis2ControlTexts === 'function') refreshNis2ControlTexts();
    var nis2Controls = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls('cyfun2025') : (appData.nis2Controls || []);
    nis2Controls.forEach(function(c) {
      if (!c) return;
      if (c.owner === undefined) c.owner = '';
      if (c.reviewer === undefined) c.reviewer = '';
      if (c.reviewFrequency === undefined) c.reviewFrequency = '';
      if (c.lastEvaluation === undefined) c.lastEvaluation = '';
      if (c.nextReviewDate === undefined) c.nextReviewDate = '';
      if (c.evidence === undefined) c.evidence = '';
    });
    const _f = SectionToolbar.getFilters("nis2-controls");
    const _q = (SectionToolbar.getSearch("nis2-controls") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte

    // Populate function filter dynamically
    var funcs = [...new Set(nis2Controls.map(c => c.functionId).filter(Boolean))];
    var funcOrder = { 'GV': 1, 'ID': 2, 'PR': 3, 'DE': 4, 'RS': 5, 'RC': 6 };
    funcs.sort(SortUtils.orderedMap(funcOrder, 99));
    SectionToolbar.updateFilterOptions('nis2-controls', 'function', funcs.map(function(fId) {
      var fn = (window.CYFUN_FUNCTIONS || []).find(function(f) { return f.id === fId; });
      return { value: fId, label: fn ? cyfunLabelText(fn) + ' (' + fId + ')' : fId };
    }));
    // Populate category filter dynamically
    var cats = [...new Set(nis2Controls.map(c => c.categoryId).filter(Boolean))];
    cats.sort(SortUtils.text);
    SectionToolbar.updateFilterOptions('nis2-controls', 'category', cats.map(function(catId) {
      return { value: catId, label: CATEGORY_LABELS[catId] || catId };
    }));
    SectionToolbar.updateFilterOptions('nis2-controls', 'owner', _frameworkOwnerFilterOptions(nis2Controls, 'controlsNIS2'));
    SectionToolbar.updateFilterOptions('nis2-controls', 'reviewDue', _frameworkReviewFilterOptions('controlsNIS2'));

    const fVal = _f.function || "";
    const cVal = _f.category || "";
    const sVal = _f.scoring || "";
    const lvlVal = _f.level || "";
    const keyVal = _f.keyMeasure || "";
    const ownerVal = _f.owner || "";
    const reviewVal = _f.reviewDue || "";

    const filtered = nis2Controls.filter(c =>
      (!fVal || c.functionId === fVal) &&
      (!cVal || c.categoryId === cVal) &&
      (!sVal || (sVal === "scored" ? (c.docMaturity > 0 || c.implMaturity > 0) : sVal === "unscored" ? (c.docMaturity === 0 && c.implMaturity === 0) : false)) &&
      (!lvlVal || c.level === lvlVal) &&
      (!keyVal || (keyVal === "key" ? c.isKeyMeasure : !c.isKeyMeasure)) &&
      _frameworkMatchesOwnerFilter(c, ownerVal) &&
      _frameworkMatchesReviewFilter(c, reviewVal) &&
      (!_q || ((c.ref || "") + " " + (c.title || "") + " " + (c.description || "") + " " + (c.owner || "")).toLowerCase().indexOf(_q) >= 0)
    );
    var _s = SectionToolbar.getSort("nis2-controls");
    var _sortKey = (_s && _s.key) || "reference";
    var _sortDir = (_s && _s.dir) || "asc";
    var sorted = filtered.slice().sort(function(a, b) {
      var diff;
      switch (_sortKey) {
        case "function": diff = SortUtils.text(a.functionId || "", b.functionId || ""); break;
        case "owner": diff = SortUtils.text(a.owner || "", b.owner || ""); break;
        case "nextReviewDate": diff = SortUtils.date(a.nextReviewDate || "", b.nextReviewDate || ""); break;
        case "docMaturity": diff = SortUtils.numeric(a.docMaturity || 0, b.docMaturity || 0); break;
        case "implMaturity": diff = SortUtils.numeric(a.implMaturity || 0, b.implMaturity || 0); break;
        default: diff = SortUtils.reference(a.ref || a.id || "", b.ref || b.id || "");
      }
      return _sortDir === "desc" ? -diff : diff;
    });

    // Level badge colors
    var levelColors = { basic: '#059669', important: '#d97706', essential: '#dc2626' };

    tbody.textContent = "";
    sorted.forEach(c => {
      var row = document.createElement("tr");
      if (c.isKeyMeasure) row.classList.add("nis2-control-row--key-measure");
      row.style.cursor = "pointer";

      // Col 1: Ref + icons + catégorie en sous-ligne (Lot 1b : colonne Catégorie fusionnée ici)
      var td1 = document.createElement("td");
      var code = document.createElement("code");
      code.style.fontSize = "0.8em";
      code.textContent = c.ref || '';
      td1.appendChild(code);
      if (c.isKeyMeasure) {
        var star = document.createElement("i");
        star.className = "fas fa-star";
        star.style.cssText = "color:var(--color-warning-dark, #d97706);margin-left:4px";
        star.title = "Mesure cl\u00e9";
        td1.appendChild(star);
      }
      if (c.isManagementLinked) {
        var bld = document.createElement("i");
        bld.className = "fas fa-building";
        bld.style.cssText = "color:var(--color-text-secondary, #6b7280);margin-left:4px";
        bld.title = "Contr\u00f4le li\u00e9 au management";
        td1.appendChild(bld);
      }
      // Catégorie (bilingue) en sous-ligne discrète sous la réf
      var fw = (window._csiFrameworks || {})['cyfun2025'];
      var catDef = fw && fw.meta ? fw.meta.categories.find(function(ct) { return ct.id === c.categoryId; }) : null;
      var catText = catDef ? cyfunLabelText(catDef) : (c.categoryLabel || c.categoryId || '');
      if (catText) {
        var catLine = document.createElement("div");
        catLine.className = "framework-cat-subline";
        catLine.textContent = catText;
        catLine.title = catText;
        td1.appendChild(catLine);
      }
      row.appendChild(td1);

      // Col 2: Function badge (code seul, libellé complet en title)
      var td2 = document.createElement("td");
      var fnDef = (window.CYFUN_FUNCTIONS || []).find(function(f) { return f.id === c.functionId; });
      var badge = document.createElement("span");
      var fnColor = fnDef ? fnDef.color : '#6b7280';
      badge.style.cssText = "display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:12px;font-size:0.8em;font-weight:600;white-space:nowrap;background:" + fnColor + "15;color:" + fnColor;
      if (fnDef && fnDef.icon) {
        var ico = document.createElement("i");
        ico.className = "fas " + fnDef.icon;
        ico.style.fontSize = "0.85em";
        badge.appendChild(ico);
      }
      var fnLabel = fnDef ? cyfunLabelText(fnDef) : '';
      badge.appendChild(document.createTextNode(c.functionId || ''));
      badge.title = fnLabel ? c.functionId + ' - ' + fnLabel : (c.functionId || '');
      td2.appendChild(badge);
      // Level indicator
      var lvlBadge = document.createElement("span");
      var lvlColor = levelColors[c.level] || '#6b7280';
      lvlBadge.style.cssText = "display:inline-block;margin-left:6px;padding:1px 6px;border-radius:8px;font-size:0.7em;font-weight:600;background:" + lvlColor + "18;color:" + lvlColor;
      lvlBadge.textContent = (c.level || '').toUpperCase().charAt(0);
      lvlBadge.title = c.level ? c.level.charAt(0).toUpperCase() + c.level.slice(1) : '';
      td2.appendChild(lvlBadge);
      row.appendChild(td2);

      // Col 3: Description
      var td4 = document.createElement("td");
      td4.className = "nis2-desc-cell";
      td4.title = c.description || '';
      td4.textContent = c.title || c.description || '';
      row.appendChild(td4);

      // Col 5-6: Responsable / Prochaine revue
      _frameworkAppendTextCell(row, c.owner || '', { nowrap: true, className: 'framework-owner-cell' });
      _frameworkAppendTextCell(row, c.nextReviewDate || '', { nowrap: true, className: 'framework-review-cell' });

      // Col 6-7: Doc / Impl (Lot 1b : colonne « Moy. » supprimée, couleur toujours basée sur la moyenne)
      var docVal = c.docMaturity || 0;
      var implVal = c.implMaturity || 0;
      var avg = docVal > 0 || implVal > 0 ? +((docVal + implVal) / 2).toFixed(1) : 0;
      var matColor = avg >= 3.5 ? '#059669' : avg >= 2.5 ? '#d97706' : avg > 0 ? '#dc2626' : '#9ca3af';

      [docVal, implVal].forEach(function(val) {
        var td = document.createElement("td");
        td.style.textAlign = "center";
        var sp = document.createElement("span");
        sp.style.color = matColor;
        sp.style.fontWeight = "600";
        sp.textContent = val > 0 ? String(val) : '—';
        td.appendChild(sp);
        row.appendChild(td);
      });

      row.classList.add("row-clickable");
      row.addEventListener("click", function() { editNis2Control(c.id); });
      tbody.appendChild(row);
    });

    // Update counts
    var countEl = document.getElementById("nis2ControlsCount");
    if (countEl) countEl.textContent = filtered.length + ' / ' + nis2Controls.length;

    // M4 : rafraîchir l'encart verdict de conformité CyFun (seuils CCB).
    if (typeof renderCyfunVerdict === 'function') renderCyfunVerdict();
  }

  function editNis2Control(id) {
    var _nis2CtrlsEdit = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls('cyfun2025') : (appData.nis2Controls || []);
    const control = _nis2CtrlsEdit.find(c => c.id === id);
    if (!control) return;
    // Build maturity options (1-5)
    const maturityOptions = [
      { value: "0", label: "\u2014 " + t('controlsISO.notEvaluated', 'Non évalué') + " \u2014" },
      { value: "1", label: "1 - " + t('controlsISO.initial', 'Initial') },
      { value: "2", label: "2 - " + t('controlsISO.repeatable', 'Répétable') },
      { value: "3", label: "3 - " + t('controlsISO.defined', 'Défini') },
      { value: "4", label: "4 - " + t('controlsISO.managed', 'Géré') },
      { value: "5", label: "5 - " + t('controlsISO.optimized', 'Optimisé') }
    ];
    const fields = [
      { name: "ref", label: t('controlsNIS2.ref', 'Réf.'), value: control.ref || '', readOnly: true },
      { name: "functionLabel", label: t('controlsNIS2.function', 'Fonction'), value: (function() {
        var fn = (window.CYFUN_FUNCTIONS || []).find(function(f) { return f.id === control.functionId; });
        return fn ? control.functionId + ' - ' + cyfunLabelText(fn) : control.functionLabel || control.functionId || '';
      })(), readOnly: true },
      { name: "categoryLabel", label: t('controlsNIS2.category', 'Catégorie'), value: (function() {
        var fw = (window._csiFrameworks || {})['cyfun2025'];
        var cat = fw && fw.meta ? fw.meta.categories.find(function(c2) { return c2.id === control.categoryId; }) : null;
        return cat ? control.categoryId + ' - ' + cyfunLabelText(cat) : control.categoryLabel || control.categoryId || '';
      })(), readOnly: true },
      { name: "description", label: t('fields.description'), type: "textarea", value: control.description, readOnly: true },
      { name: "owner", label: t('fields.controlOwner', 'Propriétaire du contrôle'), value: control.owner || "", placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(control.owner), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "reviewer", label: t('fields.reviewer', 'Relecteur'), value: control.reviewer || "", placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(control.reviewer), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "lastEvaluation", label: t('fields.lastEvaluation', 'Dernière évaluation'), type: "date", value: control.lastEvaluation || "" },
      { name: "nextReviewDate", label: t('fields.nextReviewDate', 'Prochaine revue'), type: "date", value: control.nextReviewDate || "" },
      { name: "reviewFrequency", label: t('fields.reviewFrequency', 'Fréquence de revue'), type: "select", value: control.reviewFrequency || "", options: CONTROL_REVIEW_FREQ_OPTIONS },
      { name: "docMaturity", label: t('cyfun.docMaturity', 'Maturité documentation') + " (1-5)", type: "select", value: String(control.docMaturity || 0), options: maturityOptions },
      { name: "implMaturity", label: t('cyfun.implMaturity', 'Maturité implémentation') + " (1-5)", type: "select", value: String(control.implMaturity || 0), options: maturityOptions },
      { name: "justification", label: t('controlsNIS2.justification', 'Justification'), type: "textarea", value: control.justification || "" },
      { name: "evidence", label: t('cyfun.evidence', 'Preuves / Artefacts'), type: "textarea", value: control.evidence || "", placeholder: t('cyfun.evidencePlaceholder', 'Documents, logs, captures...') },
      // Unification commentaires 2026-07-10 : même pattern que les actions (historique + ajout).
      // Le champ comments existait déjà dans le modèle CyFun (préservé au rebuild/import) mais sans UI.
      { name: "existingComments", label: t('fields.existingComments'), type: "textarea", value: formatCommentsReadonly(control.comments), readOnly: true },
      { name: "newComment", label: t('fields.newComment'), type: "textarea" }
    ];
    // Add badges info
    if (control.isKeyMeasure) {
      fields.splice(1, 0, { name: "_keyMeasure", label: t('cyfun.keyMeasure', 'Mesure clé'), value: "\u2b50 " + t('cyfun.keyMeasureInfo', 'Oui - Cette mesure est critique pour la conformité'), readOnly: true });
    }
    if (control.isManagementLinked) {
      fields.splice(control.isKeyMeasure ? 2 : 1, 0, { name: "_mgmtLinked", label: t('cyfun.managementLinked', 'Contrôle lié au management'), value: "\ud83c\udfe2 " + t('cyfun.mgmtLinkedInfo', 'Oui - Revue obligatoire à chaque audit'), readOnly: true });
    }
    openModal(t('modals.nis2ControlTracking', 'Suivi du contrôle NIS2'), fields, (data) => {
      control.docMaturity = parseInt(data.docMaturity, 10) || 0;
      control.implMaturity = parseInt(data.implMaturity, 10) || 0;
      control.owner = data.owner || '';
      control.reviewer = data.reviewer || '';
      control.lastEvaluation = data.lastEvaluation || '';
      control.nextReviewDate = data.nextReviewDate || '';
      control.reviewFrequency = data.reviewFrequency || '';
      control.justification = data.justification || '';
      control.evidence = data.evidence || '';
      if (appendComment(control, data.newComment, 10) === 'max') {
        showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', 10), 'warning');
      }
      // Update legacy compat fields
      var avg = (control.docMaturity + control.implMaturity) / 2;
      if (avg >= 4.5) control.cmm = 'Optimis\u00e9';
      else if (avg >= 3.5) control.cmm = 'G\u00e9r\u00e9';
      else if (avg >= 2.5) control.cmm = 'D\u00e9fini';
      else if (avg >= 1.5) control.cmm = 'R\u00e9p\u00e9table';
      else control.cmm = 'Initial';
      control.status = avg >= 3 ? 'implemente' : avg >= 1 ? 'en_cours' : 'non_implemente';
      saveData();
      loadNis2ControlsTable();
      recalcNis2Maturity();
      updateDashboard();
      renderNis2ProgramRadar();
    });
  }

  // ============================================================================
  // ReCyF (Référentiel Cyber France / ANSSI) — NIS2 « France »
  // Référentiel de CONFORMITÉ indépendant de CyFun (« Belgique »), qui reste
  // totalement inchangé. Modèle d'évaluation = statut de conformité par mesure
  // (moyens acceptables de conformité ANSSI), pas de maturité 1-5.
  //   appData.recyfControls   : mesures évaluées { conformity, justification }
  //   appData.recyfLevel      : 'importante' (EI, obj. 1-15) | 'essentielle' (EE, obj. 1-20)
  //   appData.nis2Referential : 'cyfun2025' (défaut) | 'recyf2025'
  // ============================================================================

  function _recyfUiLang() {
    return (window.CSI_i18n && CSI_i18n.getLanguage && CSI_i18n.getLanguage()) || 'fr';
  }
  function getRecyfControls() {
    return window.CSI_FrameworkMapper
      ? CSI_FrameworkMapper.getAppDataControls('recyf2025')
      : (appData.recyfControls || []);
  }
  function setRecyfControlsData(controls) {
    if (window.CSI_FrameworkMapper) CSI_FrameworkMapper.setAppDataControls('recyf2025', controls);
    else appData.recyfControls = controls;
  }
  function _recyfConformityOption(value) {
    return (window.RECYF_CONFORMITY_OPTIONS || []).find(function(o) { return o.value === value; }) || null;
  }
  function _recyfConformityLabel(value) {
    var o = _recyfConformityOption(value);
    if (!o) return value || '';
    return (_recyfUiLang() === 'en' && o.labelEn) ? o.labelEn : o.label;
  }

  function _frameworkTodayIso() {
    return new Date().toISOString().slice(0, 10);
  }

  function _frameworkAddDaysIso(days) {
    var d = new Date();
    d.setDate(d.getDate() + days);
    return d.toISOString().slice(0, 10);
  }

  function _frameworkMatchesReviewFilter(control, filterValue) {
    var next = control && control.nextReviewDate ? String(control.nextReviewDate).slice(0, 10) : '';
    if (!filterValue) return true;
    if (!next) return false;
    var today = _frameworkTodayIso();
    if (filterValue === 'overdue') return next < today;
    if (filterValue === 'next30') return next >= today && next <= _frameworkAddDaysIso(30);
    return true;
  }

  function _frameworkOwnerFilterOptions(controls, i18nRoot) {
    var seen = {};
    (appData.stakeholders || []).forEach(function(st) {
      if (st && st.name) seen[String(st.name)] = true;
    });
    (controls || []).forEach(function(control) {
      if (control && control.owner) seen[String(control.owner)] = true;
    });
    var options = Object.keys(seen).sort(SortUtils.text).map(function(name) {
      return { value: name, label: name };
    });
    options.unshift({ value: '__unassigned', label: t(i18nRoot + '.unassignedOwner', 'Sans responsable') });
    return options;
  }

  function _stakeholderNameSuggestions(currentValue) {
    var seen = {};
    var names = [];
    function add(value) {
      var name = typeof value === 'string' ? value.trim() : '';
      if (!name || seen[name]) return;
      seen[name] = true;
      names.push(name);
    }
    (appData.stakeholders || []).forEach(function(st) { if (st) add(st.name); });
    add(currentValue || '');
    return names.sort(SortUtils.text);
  }

  function _frameworkReviewFilterOptions(i18nRoot) {
    return [
      { value: 'overdue', label: t(i18nRoot + '.reviewOverdue', 'Revue échue') },
      { value: 'next30', label: t(i18nRoot + '.reviewNext30', 'Revue à 30 jours') }
    ];
  }

  function _frameworkMatchesOwnerFilter(control, ownerFilter) {
    if (!ownerFilter) return true;
    var owner = control && control.owner ? String(control.owner).trim() : '';
    if (ownerFilter === '__unassigned') return !owner;
    return owner === ownerFilter;
  }

  function _frameworkAppendTextCell(row, value, options) {
    var td = document.createElement('td');
    if (options && options.className) td.className = options.className;
    var text = value ? String(value) : '—';
    td.textContent = text;
    td.title = text === '—' ? '' : text;
    if (!value) td.style.color = 'var(--color-text-light, #9ca3af)';
    if (options && options.nowrap) td.style.whiteSpace = 'nowrap';
    row.appendChild(td);
    return td;
  }

  // Construit la liste des mesures ReCyF pour le degré courant (cumulatif EI ⊂ EE)
  var _RECYF_LEVEL_ORDER = { importante: 1, essentielle: 2 };
  function _recyfMaxOrder() { return _RECYF_LEVEL_ORDER[appData.recyfLevel || 'importante'] || 1; }

  // Construit TOUTES les mesures ReCyF (152). Le degré EI/EE ne filtre QUE l'affichage et
  // le score (cf. recyfControlsInScope) — jamais le stockage. Ainsi, changer de degré ne
  // perd jamais d'évaluation (ex. EE→EI→EE conserve les mesures EE). L'id est dérivé de la
  // réf (stable et unique), pas de l'index (évite tout doublon d'id lors d'un changement).
  function loadRecyfControls() {
    var fw = (window._csiFrameworks || {})['recyf2025'];
    if (!fw || !fw.requirements) {
      void 0;
      return [];
    }
    if (typeof window.rebuildRecyfThemes === 'function') window.rebuildRecyfThemes(appData.recyfLevel || 'importante');
    return fw.requirements.map(function(req) {
      return {
        id: 'RECYF-' + String(req.ref).replace(/[^A-Za-z0-9]+/g, '_'),
        ref: req.ref,
        objectiveId: req.objectiveId,
        objectiveLabel: req.objectiveLabel,
        theme: req.theme,
        themeId: req.themeId,
        target: req.target,
        targets: req.targets || [],
        level: req.level,
        content: req.content || '',
        conformity: 'non_evalue',
        justification: '',
        owner: '',
        reviewer: '',
        reviewFrequency: '',
        lastEvaluation: '',
        nextReviewDate: '',
        evidence: '',
        comments: [] // Unification 2026-07-10 : alignement sur CyFun ({text,date,id?,author?})
      };
    });
  }

  // Mesures dans le périmètre du degré courant (EI = importante uniquement ; EE = tout)
  function recyfControlsInScope() {
    var maxO = _recyfMaxOrder();
    return getRecyfControls().filter(function(c) { return (_RECYF_LEVEL_ORDER[c.level] || 99) <= maxO; });
  }

  // Génère/migre recyfControls vers l'ensemble COMPLET (152), en préservant les évaluations par réf.
  // Migre aussi les anciennes données filtrées (76) vers l'ensemble complet sans perte.
  function ensureRecyfControls() {
    var fw = (window._csiFrameworks || {})['recyf2025'];
    var full = (fw && fw.requirements) ? fw.requirements.length : 152;
    var c = getRecyfControls();
    if (!Array.isArray(c) || c.length < full) {
      var byRef = {};
      (c || []).forEach(function(x) { if (x && x.ref) byRef[x.ref] = x; });
      var rebuilt = loadRecyfControls().map(function(x) {
        var p = byRef[x.ref];
        if (p) {
          x.conformity = p.conformity || 'non_evalue';
          x.justification = p.justification || '';
          x.owner = p.owner || '';
          x.reviewer = p.reviewer || '';
          x.reviewFrequency = p.reviewFrequency || '';
          x.lastEvaluation = p.lastEvaluation || '';
          x.nextReviewDate = p.nextReviewDate || '';
          x.evidence = p.evidence || '';
          x.comments = normalizeComments(p.comments);
        }
        return x;
      });
      setRecyfControlsData(rebuilt);
    } else {
      c.forEach(function(x) {
        if (!x) return;
        if (x.owner === undefined) x.owner = '';
        if (x.reviewer === undefined) x.reviewer = '';
        if (x.reviewFrequency === undefined) x.reviewFrequency = '';
        if (x.lastEvaluation === undefined) x.lastEvaluation = '';
        if (x.nextReviewDate === undefined) x.nextReviewDate = '';
        if (x.evidence === undefined) x.evidence = '';
        if (!Array.isArray(x.comments)) x.comments = normalizeComments(x.comments);
      });
    }
  }

  // Change le degré (EI/EE) : n'affecte QUE l'affichage et le score, jamais les évaluations stockées.
  function setRecyfDegree(level) {
    if (!level || ['importante', 'essentielle'].indexOf(level) < 0) level = 'importante';
    appData.recyfLevel = level;
    if (typeof window.rebuildRecyfThemes === 'function') window.rebuildRecyfThemes(level);
    ensureRecyfControls();
    if (typeof saveData === 'function') saveData();
  }

  // Statistiques de conformité ReCyF
  function computeRecyfConformity() {
    var controls = recyfControlsInScope();
    var opts = window.RECYF_CONFORMITY_OPTIONS || [];
    var weightByValue = {};
    opts.forEach(function(o) { weightByValue[o.value] = o.weight; });
    var counts = {};
    opts.forEach(function(o) { counts[o.value] = 0; });
    var evaluated = 0, weightSum = 0, na = 0;
    controls.forEach(function(c) {
      var v = c.conformity || 'non_evalue';
      if (counts[v] === undefined) counts[v] = 0;
      counts[v]++;
      if (v === 'non_applicable') { na++; return; }
      var w = weightByValue[v];
      if (w === null || w === undefined) return; // non évalué
      evaluated++;
      weightSum += w;
    });
    var percent = evaluated > 0 ? Math.round((weightSum / evaluated) * 100) : 0;
    // M3 : taux « coverage-aware » (dénominateur = mesures applicables, les non évaluées
    // comptent pour 0). Utilisé par le KPI global du dashboard ; `percent` (évalué-based)
    // reste affiché dans le module ReCyF avec sa couverture explicite.
    var _applicable = controls.length - na;
    var percentApplicable = _applicable > 0 ? Math.round((weightSum / _applicable) * 100) : 0;
    return {
      total: controls.length,
      counts: counts,
      evaluated: evaluated,
      na: na,
      applicable: _applicable,
      percent: percent,
      percentApplicable: percentApplicable
    };
  }

  // Taux de conformité par objectif de sécurité (pour le radar/araignée du dashboard).
  // Renvoie un tableau ordonné des objectifs DANS LE PÉRIMÈTRE du degré courant.
  function computeRecyfConformityByObjective() {
    var controls = recyfControlsInScope();
    var opts = window.RECYF_CONFORMITY_OPTIONS || [];
    var w = {}; opts.forEach(function(o) { w[o.value] = o.weight; });
    var byObj = {};
    controls.forEach(function(c) {
      var o = c.objectiveId;
      if (!byObj[o]) byObj[o] = { objectiveId: o, objectiveLabel: c.objectiveLabel || '', wsum: 0, evaluated: 0, total: 0, na: 0 };
      byObj[o].total++;
      var v = c.conformity || 'non_evalue';
      if (v === 'non_applicable') { byObj[o].na++; return; }
      var ww = w[v];
      if (ww === null || ww === undefined) return; // non évalué
      byObj[o].evaluated++;
      byObj[o].wsum += ww;
    });
    var fw = (window._csiFrameworks || {})['recyf2025'];
    var order = (fw && fw.meta && fw.meta.objectives) ? fw.meta.objectives.map(function(o) { return o.id; }) : Object.keys(byObj);
    return order.filter(function(id) { return byObj[id]; }).map(function(id) {
      var b = byObj[id];
      // percent = conformité des mesures ÉVALUÉES (pour affichage détaillé du module).
      b.percent = b.evaluated > 0 ? Math.round((b.wsum / b.evaluated) * 100) : 0;
      // M-RC3 (audit fond 2026-07-11) : percentApplicable = coverage-aware (dénominateur =
      // mesures applicables, non évaluées comptées 0). Utilisé par le RADAR du dashboard pour
      // rester COHÉRENT avec le KPI global (lui aussi coverage-aware depuis M3) — sinon 100 %
      // au radar vs 13 % à la jauge sur donnée identique.
      var _applicable = b.total - b.na;
      b.percentApplicable = _applicable > 0 ? Math.round((b.wsum / _applicable) * 100) : 0;
      return b;
    });
  }

  function updateRecyfCount(shown, total) {
    var el = document.getElementById('recyfControlsCount');
    if (!el) return;
    if (typeof shown !== 'number') { total = recyfControlsInScope().length; shown = total; }
    el.textContent = shown + ' / ' + total;
  }

  function _recyfForceSync() {
    // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
    if (window.CSI_SyncManager && window.CSI_SyncManager.isServerMode &&
        window.CSI_SyncManager.isServerMode() && window.CSI_SyncManager.forceServerSync) {
      setTimeout(function() {
        window.CSI_SyncManager.forceServerSync(['recyfControls', 'recyfLevel']).catch(function(e) {
          void 0;
        });
      }, 500);
    }
  }

  // Synthèse de conformité (score global + degré + répartition par statut)
  function renderRecyfSummary() {
    var host = document.getElementById('recyfSummary');
    if (!host) return;
    ensureRecyfControls();
    var stats = computeRecyfConformity();
    var opts = window.RECYF_CONFORMITY_OPTIONS || [];
    var degree = appData.recyfLevel || 'importante';
    var degInfo = (window.RECYF_LEVELS || []).find(function(l) { return l.id === degree; }) || { short: 'EI', label: 'Entité Importante' };

    host.textContent = '';
    var wrap = document.createElement('div');
    wrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin:6px 0 14px;padding:12px 16px;border:1px solid var(--color-border,#e5e7eb);border-radius:10px;background:var(--color-surface,#fff);';

    // Score
    var scoreBox = document.createElement('div');
    scoreBox.style.cssText = 'display:flex;flex-direction:column;align-items:center;min-width:96px;';
    var pct = document.createElement('div');
    var pColor = stats.percent >= 80 ? '#059669' : stats.percent >= 50 ? '#d97706' : '#dc2626';
    pct.style.cssText = 'font-size:1.8rem;font-weight:800;line-height:1;color:' + pColor + ';';
    pct.textContent = stats.percent + '%';
    var pLbl = document.createElement('div');
    pLbl.style.cssText = 'font-size:0.72rem;color:var(--color-text-secondary,#6b7280);margin-top:4px;text-align:center;';
    pLbl.textContent = t('recyfControls.conformityRate', 'Conformité (mesures évaluées)');
    scoreBox.appendChild(pct);
    scoreBox.appendChild(pLbl);
    wrap.appendChild(scoreBox);

    // Degré + progression
    var meta = document.createElement('div');
    meta.style.cssText = 'display:flex;flex-direction:column;gap:4px;';
    var degBadge = document.createElement('span');
    degBadge.style.cssText = 'align-self:flex-start;padding:2px 10px;border-radius:12px;font-size:0.78rem;font-weight:700;background:#0055A415;color:#0055A4;';
    degBadge.textContent = (degInfo.short || '') + ' — ' + (degInfo.label || '');
    var prog = document.createElement('span');
    prog.style.cssText = 'font-size:0.8rem;color:var(--color-text-secondary,#6b7280);';
    prog.textContent = t('recyfControls.evaluatedCount', 'Évaluées') + ' : ' + stats.evaluated + ' / ' + stats.applicable +
      '  ·  ' + t('recyfControls.naCount', 'N/A') + ' : ' + stats.na +
      '  ·  ' + t('recyfControls.totalCount', 'Total') + ' : ' + stats.total;
    meta.appendChild(degBadge);
    meta.appendChild(prog);
    wrap.appendChild(meta);

    // Chips par statut
    var chips = document.createElement('div');
    chips.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin-left:auto;';
    opts.forEach(function(o) {
      var n = stats.counts[o.value] || 0;
      if (o.value === 'non_evalue' && n === 0) return;
      var chip = document.createElement('span');
      chip.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:14px;font-size:0.76rem;font-weight:600;background:' + (o.color || '#6b7280') + '18;color:' + (o.color || '#374151') + ';';
      var dot = document.createElement('span');
      dot.style.cssText = 'width:8px;height:8px;border-radius:50%;background:' + (o.color || '#6b7280') + ';';
      chip.appendChild(dot);
      chip.appendChild(document.createTextNode(((_recyfUiLang() === 'en' && o.labelEn) ? o.labelEn : o.label) + ' : ' + n));
      chips.appendChild(chip);
    });
    wrap.appendChild(chips);
    host.appendChild(wrap);
  }

  // Tableau des mesures ReCyF
  function loadRecyfControlsTable() {
    var tbody = document.getElementById('recyfControlsTableBody');
    if (!tbody) return;
    ensureRecyfControls();
    var controls = recyfControlsInScope();
    var opts = window.RECYF_CONFORMITY_OPTIONS || [];
    var colorByValue = {};
    opts.forEach(function(o) { colorByValue[o.value] = o.color; });

    var _f = (typeof SectionToolbar !== 'undefined' && SectionToolbar.getFilters) ? SectionToolbar.getFilters('recyf-controls') : {};
    var _q = ((typeof SectionToolbar !== 'undefined' && SectionToolbar.getSearch) ? SectionToolbar.getSearch('recyf-controls') : '').trim().toLowerCase(); // Toolbar v2 : recherche plein texte

    // Options de filtres dynamiques
    if (typeof SectionToolbar !== 'undefined' && SectionToolbar.updateFilterOptions) {
      var fw = (window._csiFrameworks || {})['recyf2025'];
      var objSeen = {}, objOpts = [];
      controls.forEach(function(c) {
        if (!objSeen[c.objectiveId]) { objSeen[c.objectiveId] = true; objOpts.push({ value: c.objectiveId, label: 'Obj. ' + c.objectiveId + ' — ' + (c.objectiveLabel || ''), _n: parseInt(c.objectiveId, 10) }); }
      });
      objOpts.sort(function(a, b) { return a._n - b._n; });
      SectionToolbar.updateFilterOptions('recyf-controls', 'objective', objOpts);
      var thSeen = {}, thOpts = [];
      controls.forEach(function(c) {
        if (!thSeen[c.themeId]) { thSeen[c.themeId] = true; thOpts.push({ value: c.themeId, label: c.theme }); }
      });
      SectionToolbar.updateFilterOptions('recyf-controls', 'theme', thOpts);
      SectionToolbar.updateFilterOptions('recyf-controls', 'owner', _frameworkOwnerFilterOptions(controls, 'recyfControls'));
      SectionToolbar.updateFilterOptions('recyf-controls', 'reviewDue', _frameworkReviewFilterOptions('recyfControls'));
    }

    var objVal = _f.objective || '';
    var thVal = _f.theme || '';
    var tgVal = _f.target || '';
    var cfVal = _f.conformity || '';
    var ownerVal = _f.owner || '';
    var reviewVal = _f.reviewDue || '';

    var filtered = controls.filter(function(c) {
      return (!objVal || c.objectiveId === objVal) &&
             (!thVal || c.themeId === thVal) &&
             (!tgVal || (tgVal === 'EE_only' ? c.level === 'essentielle' : (c.targets || []).indexOf(tgVal) >= 0)) &&
             (!cfVal || (c.conformity || 'non_evalue') === cfVal) &&
             _frameworkMatchesOwnerFilter(c, ownerVal) &&
             _frameworkMatchesReviewFilter(c, reviewVal) &&
             (!_q || ((c.ref || '') + ' ' + (c.content || '') + ' ' + (c.owner || '')).toLowerCase().indexOf(_q) >= 0);
    });

    tbody.textContent = '';
    var _s = (typeof SectionToolbar !== 'undefined' && SectionToolbar.getSort) ? SectionToolbar.getSort('recyf-controls') : null;
    var _sortKey = (_s && _s.key) || 'reference';
    var _sortDir = (_s && _s.dir) || 'asc';
    var sorted = filtered.slice().sort(function(a, b) {
      var diff;
      switch (_sortKey) {
        case 'objective': diff = SortUtils.numeric(parseInt(a.objectiveId, 10) || 0, parseInt(b.objectiveId, 10) || 0); break;
        case 'owner': diff = SortUtils.text(a.owner || '', b.owner || ''); break;
        case 'nextReviewDate': diff = SortUtils.date(a.nextReviewDate || '', b.nextReviewDate || ''); break;
        default: diff = SortUtils.reference(a.ref || a.id || '', b.ref || b.id || '');
      }
      return _sortDir === 'desc' ? -diff : diff;
    });

    sorted.forEach(function(c) {
      var row = document.createElement('tr');
      row.style.cursor = 'pointer';

      // Col 1 : Réf
      var td1 = document.createElement('td');
      var code = document.createElement('code');
      code.style.fontSize = '0.8em';
      code.textContent = c.ref || '';
      td1.appendChild(code);
      row.appendChild(td1);

      // Col 2 : Objectif
      var td2 = document.createElement('td');
      var objBadge = document.createElement('span');
      objBadge.style.cssText = 'display:inline-block;padding:2px 8px;border-radius:12px;font-size:0.78em;font-weight:600;background:#0055A415;color:#0055A4;white-space:nowrap;';
      objBadge.textContent = 'Obj. ' + c.objectiveId;
      objBadge.title = c.objectiveLabel || '';
      td2.appendChild(objBadge);
      row.appendChild(td2);

      // Lot 1b : colonne « Thématique » retirée de la liste (visible dans la modale editRecyfControl)

      // Col 3 : Mesure (contenu)
      var td4 = document.createElement('td');
      td4.className = 'nis2-desc-cell';
      td4.title = c.content || '';
      td4.textContent = c.content || '';
      row.appendChild(td4);

      // Col 4-5 : Responsable / Prochaine revue
      _frameworkAppendTextCell(row, c.owner || '', { nowrap: true, className: 'framework-owner-cell' });
      _frameworkAppendTextCell(row, c.nextReviewDate || '', { nowrap: true, className: 'framework-review-cell' });

      // Col 6 : Cible EI/EE
      var td5 = document.createElement('td');
      var isEEonly = (c.level === 'essentielle');
      var tg = document.createElement('span');
      tg.style.cssText = 'display:inline-block;padding:1px 6px;border-radius:8px;font-size:0.72em;font-weight:700;background:' + (isEEonly ? '#dc262618' : '#05966918') + ';color:' + (isEEonly ? '#dc2626' : '#059669') + ';';
      tg.textContent = isEEonly ? 'EE' : 'EI/EE';
      tg.title = isEEonly ? t('recyfControls.eeOnly', 'Entité Essentielle uniquement') : t('recyfControls.eiEe', 'Entités Importante et Essentielle');
      td5.appendChild(tg);
      row.appendChild(td5);

      // Col 7 : Conformité (select inline)
      var td6 = document.createElement('td');
      var sel = document.createElement('select');
      sel.className = 'form-control';
      var curConf = c.conformity || 'non_evalue';
      sel.style.cssText = 'min-width:150px;font-size:0.82em;font-weight:600;color:' + (colorByValue[curConf] || '#374151') + ';';
      opts.forEach(function(o) {
        var op = document.createElement('option');
        op.value = o.value;
        op.textContent = (_recyfUiLang() === 'en' && o.labelEn) ? o.labelEn : o.label;
        if (curConf === o.value) op.selected = true;
        sel.appendChild(op);
      });
      sel.addEventListener('click', function(e) { e.stopPropagation(); });
      sel.addEventListener('change', function(e) {
        e.stopPropagation();
        c.conformity = sel.value;
        sel.style.color = colorByValue[sel.value] || '#374151';
        if (typeof saveData === 'function') saveData();
        renderRecyfSummary();
        updateRecyfCount(filtered.length, controls.length);
        if (typeof updateDashboard === 'function') updateDashboard();
        _recyfForceSync();
      });
      td6.appendChild(sel);
      // Lot 1b : colonne « Justification » supprimée (clic-ligne ouvre la même modale) ;
      // indicateur discret si la justification est absente.
      if (!(c.justification && c.justification.trim())) {
        var jmiss = document.createElement('i');
        jmiss.className = 'far fa-comment';
        jmiss.style.cssText = 'color:#9ca3af;margin-left:6px;font-size:0.85em;';
        jmiss.title = t('recyfControls.justificationMissing', 'Justification manquante');
        td6.appendChild(jmiss);
      }
      row.appendChild(td6);

      row.classList.add("row-clickable");
      row.addEventListener('click', function() { editRecyfControl(c.id); });
      tbody.appendChild(row);
    });

    updateRecyfCount(filtered.length, controls.length);
    renderRecyfSummary();
  }

  // Modale de détail / évaluation d'une mesure ReCyF (réutilise openModal générique)
  function editRecyfControl(id) {
    var control = getRecyfControls().find(function(c) { return c.id === id; });
    if (!control) return;
    var conformityOptions = (window.RECYF_CONFORMITY_OPTIONS || []).map(function(o) {
      return { value: o.value, label: (_recyfUiLang() === 'en' && o.labelEn) ? o.labelEn : o.label };
    });
    var targetLabel = (control.level === 'essentielle')
      ? t('recyfControls.eeOnly', 'Entité Essentielle uniquement') + ' (EE)'
      : t('recyfControls.eiEe', 'Entités Importante et Essentielle') + ' (EI/EE)';
    var fields = [
      { name: 'ref', label: t('recyfControls.ref', 'Réf.'), value: control.ref || '', readOnly: true },
      { name: 'objective', label: t('recyfControls.objective', 'Objectif'), value: 'Obj. ' + control.objectiveId + ' — ' + (control.objectiveLabel || ''), readOnly: true },
      { name: 'theme', label: t('recyfControls.theme', 'Thématique'), value: control.theme || '', readOnly: true },
      { name: 'target', label: t('recyfControls.target', 'Cible'), value: targetLabel, readOnly: true },
      { name: 'content', label: t('recyfControls.measure', 'Mesure'), type: 'textarea', value: control.content || '', readOnly: true },
      { name: 'owner', label: t('recyfControls.owner', 'Responsable'), value: control.owner || '', placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(control.owner), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: 'reviewer', label: t('recyfControls.reviewer', 'Relecteur'), value: control.reviewer || '', placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(control.reviewer), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: 'lastEvaluation', label: t('recyfControls.lastEvaluation', 'Dernière évaluation'), type: 'date', value: control.lastEvaluation || '' },
      { name: 'nextReviewDate', label: t('recyfControls.nextReviewDate', 'Prochaine revue'), type: 'date', value: control.nextReviewDate || '' },
      { name: 'reviewFrequency', label: t('recyfControls.reviewFrequency', 'Fréquence de revue'), type: 'select', value: control.reviewFrequency || '', options: CONTROL_REVIEW_FREQ_OPTIONS },
      { name: 'conformity', label: t('recyfControls.conformity', 'Conformité'), type: 'select', value: control.conformity || 'non_evalue', options: conformityOptions },
      { name: 'justification', label: t('recyfControls.justification', 'Justification'), type: 'textarea', value: control.justification || '', placeholder: t('recyfControls.justificationPlaceholder', 'Justification, mesure alternative mise en œuvre, plan d\'action…') },
      { name: 'evidence', label: t('recyfControls.evidence', 'Preuves / Artefacts'), type: 'textarea', value: control.evidence || '', placeholder: t('recyfControls.evidencePlaceholder', 'Documents, logs, captures…') },
      // Unification commentaires 2026-07-10 : parité avec CyFun et les actions.
      { name: 'existingComments', label: t('fields.existingComments'), type: 'textarea', value: formatCommentsReadonly(control.comments), readOnly: true },
      { name: 'newComment', label: t('fields.newComment'), type: 'textarea' }
    ];
    openModal(t('recyfControls.modalTitle', 'Mesure ReCyF'), fields, function(data) {
      control.owner = data.owner || '';
      control.reviewer = data.reviewer || '';
      control.lastEvaluation = data.lastEvaluation || '';
      control.nextReviewDate = data.nextReviewDate || '';
      control.reviewFrequency = data.reviewFrequency || '';
      control.conformity = data.conformity || 'non_evalue';
      control.justification = data.justification || '';
      control.evidence = data.evidence || '';
      if (appendComment(control, data.newComment, 10) === 'max') {
        showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', 10), 'warning');
      }
      if (typeof saveData === 'function') saveData();
      loadRecyfControlsTable();
      if (typeof updateDashboard === 'function') updateDashboard();
      _recyfForceSync();
    });
  }

  // Applique le référentiel NIS2 actif : montre/masque les onglets BE (CyFun) vs FR (ReCyF)
  // Modèle NIS2 « mutuellement exclusif + toujours un référentiel actif » : aligne la liste
  // des frameworks activés (appData.frameworkMapper.enabledFrameworks) sur le référentiel NIS2
  // choisi (appData.nis2Referential). L'alias 'nis2' reste TOUJOURS activé (il pilote la carte
  // générique du dashboard data-framework="nis2", référentiel-aware) ; le référentiel actif
  // (cyfun2025 OU recyf2025) est ajouté, l'inactif est retiré. Fonction pure (données
  // uniquement) — aucune écriture DOM, aucun saveData : l'appelant s'en charge. Idempotente.
  function reconcileNis2Frameworks() {
    if (typeof appData === 'undefined' || !appData.frameworkMapper) return;
    var list = appData.frameworkMapper.enabledFrameworks;
    if (!Array.isArray(list)) return;
    var ref = appData.nis2Referential;
    if (ref !== 'recyf2025' && ref !== 'cyfun2025') { ref = 'cyfun2025'; appData.nis2Referential = ref; }
    function add(id) { if (list.indexOf(id) < 0) list.push(id); }
    function rm(id) { var i = list.indexOf(id); if (i >= 0) list.splice(i, 1); }
    add('nis2'); // NIS2 toujours actif (carte dashboard générique + compat. checks indexOf('nis2'))
    if (ref === 'recyf2025') { add('recyf2025'); rm('cyfun2025'); }
    else { add('cyfun2025'); rm('recyf2025'); }
  }
  window.reconcileNis2Frameworks = reconcileNis2Frameworks;

  // Verrou du référentiel NIS2 : une fois le référentiel choisi/confirmé (wizard B11 ou
  // modale B13), on n'autorise plus la bascule CyFun<->ReCyF via les Réglages — seule une
  // réinitialisation le rouvre. Objectif : éviter qu'un clic sur le sélecteur ne masque
  // brutalement un encodage pré-existant (l'utilisateur croit avoir tout perdu). Le NIVEAU
  // (cyfunLevel / recyfLevel) reste, lui, modifiable.
  //   - _nis2ReferentialLocked === true  -> verrouillé
  //   - _nis2ReferentialLocked === false -> déverrouillé (après un Reset, ou install neuve)
  //   - undefined (installs antérieures à cette version) -> verrou DÉRIVÉ de _cyfunLevelChosen,
  //     qui vaut true dès qu'un référentiel a été confirmé une fois. Protège rétroactivement
  //     les données déjà encodées sans migration explicite.
  function isNis2ReferentialLocked() {
    if (typeof appData === 'undefined') return false;
    if (appData._nis2ReferentialLocked === true) return true;
    if (appData._nis2ReferentialLocked === false) return false;
    return appData._cyfunLevelChosen === true;
  }
  window.isNis2ReferentialLocked = isNis2ReferentialLocked;

  function applyNis2Referential(navigate) {
    if (typeof reconcileNis2Frameworks === 'function') reconcileNis2Frameworks();
    // === CSI-ME === masquer les sections interdites à une sous-entité (nav +
    // bandeau d'espace). No-op hors mode / compte principal. Se rejoue à chaque
    // reconfiguration de la nav (même point que le masquage par référentiel).
    if (window.CSI_EntityUI) window.CSI_EntityUI.applySubEntityView();
    var ref = (typeof appData !== 'undefined' && appData.nis2Referential) || 'cyfun2025';
    // « Toujours un référentiel actif » : la catégorie et le panneau NIS2 restent visibles
    // quel que soit le référentiel (Belgique ou France). Garantit qu'aucun appel direct à
    // applyNis2Referential ne laisse la catégorie masquée.
    var nis2Cat = document.querySelector('.nav-ribbon__cat[data-category="nis2"]');
    var nis2Panel = document.querySelector('.nav-ribbon__panel[data-panel="nis2"]');
    if (nis2Cat) nis2Cat.style.display = '';
    if (nis2Panel) nis2Panel.style.display = '';
    var hiddenActive = null;
    document.querySelectorAll('.nav__item[data-nis2-ref]').forEach(function(tab) {
      var refs = String(tab.getAttribute('data-nis2-ref') || '').split(/\s+/).filter(Boolean);
      var match = refs.indexOf(ref) >= 0;
      tab.style.display = match ? '' : 'none';
      if (!match && tab.classList.contains('nav__item--active')) hiddenActive = tab;
    });
    // Si l'onglet actif vient d'être masqué, basculer vers l'onglet principal du référentiel actif
    if (navigate && hiddenActive) {
      var primary = (ref === 'recyf2025') ? 'recyf-controls' : 'nis2-controls';
      var target = document.querySelector('.nav__item[data-tab="' + primary + '"]');
      if (target) target.click();
    }
    if (typeof updateDashboard === 'function') updateDashboard();
  }
  // Export CSV/Excel dédié (évite le dump des <select> par l'export générique)
  function exportRecyfControlsCsv(label) {
    ensureRecyfControls();
    var controls = recyfControlsInScope();
    var headers = [
      t('recyfControls.ref', 'Réf.'),
      t('recyfControls.objective', 'Objectif'),
      t('recyfControls.theme', 'Thématique'),
      t('recyfControls.measure', 'Mesure'),
      t('recyfControls.owner', 'Responsable'),
      t('recyfControls.reviewer', 'Relecteur'),
      t('recyfControls.reviewFrequency', 'Fréquence de revue'),
      t('recyfControls.lastEvaluation', 'Dernière évaluation'),
      t('recyfControls.nextReviewDate', 'Prochaine revue'),
      t('recyfControls.target', 'Cible'),
      t('recyfControls.conformity', 'Conformité'),
      t('recyfControls.justification', 'Justification'),
      t('recyfControls.evidence', 'Preuves / Artefacts')
    ];
    var rows = controls.map(function(c) {
      return [
        c.ref || '',
        'Obj. ' + c.objectiveId + ' — ' + (c.objectiveLabel || ''),
        c.theme || '',
        c.content || '',
        c.owner || '',
        c.reviewer || '',
        c.reviewFrequency || '',
        c.lastEvaluation || '',
        c.nextReviewDate || '',
        (c.level === 'essentielle') ? 'EE' : 'EI/EE',
        _recyfConformityLabel(c.conformity || 'non_evalue'),
        c.justification || '',
        c.evidence || ''
      ];
    });
    downloadCsvFile(label || 'Mesures ReCyF', headers, rows);
  }

  window.applyNis2Referential = applyNis2Referential;
  window.loadRecyfControlsTable = loadRecyfControlsTable;
  window.setRecyfDegree = setRecyfDegree;
  window.editRecyfControl = editRecyfControl;
  window.computeRecyfConformity = computeRecyfConformity;
  window.computeRecyfConformityByObjective = computeRecyfConformityByObjective;
  window.exportRecyfControlsCsv = exportRecyfControlsCsv;

  // Options pour la fréquence de revue des contrôles
  function getControlReviewFreqOptions() { return [
    { value: "", label: "\u2014 " + t('common.select', 'Sélectionner') + " \u2014" },
    { value: "monthly", label: t('reviewFreq.monthly', 'Mensuelle') },
    { value: "quarterly", label: t('reviewFreq.quarterly', 'Trimestrielle') },
    { value: "biannual", label: t('reviewFreq.biannual', 'Semestrielle') },
    { value: "annual", label: t('reviewFreq.annual', 'Annuelle') },
    { value: "on_change", label: t('reviewFreq.onChange', 'Sur changement') }
  ]; }
  var CONTROL_REVIEW_FREQ_OPTIONS = getControlReviewFreqOptions();

  // Options pour l'efficacité mesurée
  const CONTROL_EFFECTIVENESS_OPTIONS = [
    { value: "", label: "— " + t('effectiveness.notEvaluated', 'Non évaluée') + " —" },
    { value: "effective", label: "✓ " + t('effectiveness.effective', 'Efficace') },
    { value: "partially", label: "◐ " + t('effectiveness.partiallyEffective', 'Partiellement efficace') },
    { value: "ineffective", label: "✗ " + t('effectiveness.ineffective', 'Non efficace') },
    { value: "not_tested", label: "○ " + t('effectiveness.notTested', 'Non testé') }
  ];

  function editControl(controlId) {
    var _isoCtrlsEdit = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    const control = _isoCtrlsEdit.find((c) => c.id === controlId);
    if (!control) return;

    // Vérifier le statut SoA
    const isNonApplicable = isControlNonApplicable(control.id);
    const soaJustification = getSoaJustification(control.id);

    // S'assurer que les nouveaux champs existent
    if (!control.owner) control.owner = "";
    if (!control.lastEvaluation) control.lastEvaluation = "";
    if (!control.reviewFrequency) control.reviewFrequency = "";
    if (!control.effectiveness) control.effectiveness = "";
    if (!control.evidence) control.evidence = "";
    if (!control.kpiRef) control.kpiRef = "";

    // Construire les champs de base
    const fields = [
      { name: "numero", label: t('fields.numbering', 'Numérotation'), value: control.numero, readOnly: true },
      { name: "reference", label: t('fields.reference', 'Référence'), value: control.reference, readOnly: true },
      { name: "title", label: t('fields.title'), value: isoCtrlTitle(control), readOnly: true },
      { name: "measure", label: t('fields.measure', 'Mesure'), type: 'textarea', value: isoCtrlDesc(control), readOnly: true },
      { name: "category", label: t('fields.category', 'Catégorie'), type: "select", value: control.category, options: DOMAIN_OPTIONS },
      { name: "status", label: t('fields.status'), type: "select", value: control.status, options: CONTROL_STATUS_OPTIONS },
      { name: "cmm", label: t('fields.cmmLevel', 'Niveau de maturité CMM'), type: "select", value: control.cmm, options: CMM_LEVEL_OPTIONS },
      { name: "owner", label: t('fields.controlOwner', 'Propriétaire du contrôle'), value: control.owner || "", placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(control.owner), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "lastEvaluation", label: t('fields.lastEvaluation', 'Dernière évaluation'), type: "date", value: control.lastEvaluation || "" },
      { name: "reviewFrequency", label: t('fields.reviewFrequency', 'Fréquence de revue'), type: "select", value: control.reviewFrequency, options: CONTROL_REVIEW_FREQ_OPTIONS },
      { name: "effectiveness", label: t('fields.effectiveness', 'Efficacité mesurée'), type: "select", value: control.effectiveness, options: CONTROL_EFFECTIVENESS_OPTIONS },
      { name: "evidence", label: t('fields.evidence', 'Preuves / Artefacts'), type: "textarea", value: control.evidence || "", placeholder: t('fields.evidencePlaceholder', 'Documents, logs, captures...') },
      { name: "kpiRef", label: t('fields.kpiRef', 'KPI associé'), value: control.kpiRef || "", placeholder: t('fields.kpiPlaceholder', 'Référence du KPI mesurant l\'efficacité') },
      { name: "justificationControl", label: t('fields.comments', 'Commentaires'), type: "textarea", value: control.justificationControl || "" }
    ];

    openModal(t('modals.controlTracking'), fields, (data) => {
      control.category = data.category;
      control.status = data.status;
      control.cmm = data.cmm || control.cmm;
      control.owner = data.owner || "";
      control.lastEvaluation = data.lastEvaluation || "";
      control.reviewFrequency = data.reviewFrequency || "";
      control.effectiveness = data.effectiveness || "";
      control.evidence = data.evidence || "";
      control.kpiRef = data.kpiRef || "";
      control.justificationControl = data.justificationControl || control.justificationControl;
            addAuditEntry('edit', 'controls', control.title);
      saveData();
      loadControlsTable();
      loadSoaTable();
      updateDashboard();
    }, null, {
      // Callback pour ajouter du contenu personnalisé après ouverture de la modale
      afterOpen: () => {
        if (isNonApplicable) {
          const modalBody = document.getElementById("modalBody");
          if (modalBody) {
            const alertDiv = document.createElement("div");
            alertDiv.className = "soa-alert soa-alert--warning";
            alertDiv.innerHTML = `
              <div class="soa-alert__icon"><i class="fas fa-exclamation-triangle"></i></div>
              <div class="soa-alert__content">
                <strong>${escapeHtml(t('soa.controlNotApplicable', 'Ce contrôle est déclaré non applicable'))}</strong>
                ${soaJustification ? `<p class="soa-alert__justification">${escapeHtml(t('soa.justificationLabel', 'Justification'))} : ${escapeHtml(soaJustification)}</p>` : ""}
                <a href="#" class="soa-alert__link" id="goToSoaLink">
                  <i class="fas fa-external-link-alt"></i> ${escapeHtml(t('soa.seeInDeclaration', 'Voir dans la Déclaration d\'applicabilité'))}
                </a>
              </div>
            `;
            modalBody.insertBefore(alertDiv, modalBody.firstChild);

            // Gestionnaire pour le lien
            document.getElementById("goToSoaLink")?.addEventListener("click", (e) => {
              e.preventDefault();
              closeModal();
              navigateToSoaControl(control.id);
            });
          }
        }
      }
    });
  }
  function viewControlDetails(controlId) {
    var _isoCtrlsView = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
    const control = _isoCtrlsView.find((c) => c.id === controlId);
    if (!control) return;
    const actionList = appData.actions
      .filter((a) => a.linkType === "control" && a.linkId === control.id)
      .map((a) => a.id)
      .join(", ");
    const recommendations = Array.isArray(control.recommendations) ? control.recommendations.join("\n- ") : "";
    openModal(t('modals.controlDetails', 'Détails du contrôle'), [
      { name: 'reference', label: t('fields.reference', 'Référence'), type: 'text', value: control.reference || '', readOnly: true },
      { name: 'title', label: t('fields.title', 'Titre'), type: 'text', value: isoCtrlTitle(control), readOnly: true },
      { name: 'measure', label: t('fields.measure', 'Mesure'), type: 'textarea', value: isoCtrlDesc(control), readOnly: true },
      { name: 'recommendations', label: t('fields.recommendations', 'Recommandations'), type: 'textarea', value: recommendations ? '- ' + recommendations : '', readOnly: true },
      { name: 'category', label: t('fields.category', 'Catégorie'), type: 'text', value: control.category || '', readOnly: true },
      { name: 'status', label: t('fields.status', 'Statut'), type: 'text', value: control.status || '', readOnly: true },
      { name: 'cmm', label: t('fields.cmm', 'Niveau CMM'), type: 'text', value: control.cmm || '', readOnly: true },
      { name: 'actions', label: t('fields.linkedActions', 'Actions liées'), type: 'text', value: actionList || t('labels.none', 'Aucune'), readOnly: true }
    ], null, null, { hideCancel: true, saveLabel: t('common.close', 'Fermer') });
  }
  function addControl() {
    openModal(t('modals.addControl'), [
      { name: "numero", label: t('fields.numbering', 'Numérotation') },
      { name: "reference", label: t('sort.reference', 'Réf.'), required: true },
      { name: "title", label: t('fields.title'), required: true },
      { name: "category", label: t('fields.category', 'Catégorie'), type: "select", value: "Organisationnel", options: DOMAIN_OPTIONS },
      { name: "status", label: t('fields.status'), type: "select", value: "non_implemente", options: CONTROL_STATUS_OPTIONS },
      { name: "cmm", label: t('controlsISO.cmmLevel', 'Niveau de maturité CMM'), type: "select", value: "Initial", options: CMM_LEVEL_OPTIONS }
    ], (data) => {
      if (!data.reference || !data.title) return false;
      var _isoCtrlsAdd = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
      if (_isoCtrlsAdd.some((c) => c.id === data.reference || c.reference === data.reference)) {
        showToast2(t('toasts.duplicateControlRef', "Un contrôle avec cette référence existe déjà."), "error");
        return false;
      }
      appData.controls.push({
        id: data.reference,
        numero: data.numero || "",
        reference: data.reference,
        title: data.title,
        description: "",
        category: data.category,
        status: data.status || "non_implemente",
        cmm: data.cmm || "Initial",
        comments: "",
        evidence: "",
        justificationControl: ""
      });
      appData.soa.push({ controlId: data.reference, applicable: true, justification: "" });
            addAuditEntry('add', 'controls', data.title);
      saveData();
      loadControlsTable();
      loadSoaTable();
      updateDashboard();
    });
  }
  function deleteControl(controlId) {
    showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.deleteControl', 'Supprimer ce contrôle ?'), function() {
      var _isoCtrlsDel = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls(_getActiveControlsFrameworkId() || 'iso27002') : (appData.controls || []);
      var _ctrl = _isoCtrlsDel.find(function(c) { return c.id === controlId; });
      appData.controls = (appData.controls || []).filter((c) => c.id !== controlId);
      appData.soa = (appData.soa || []).filter((e) => e.controlId !== controlId);
      appData.actions = (appData.actions || []).filter((a) => !(a.linkType === "control" && a.linkId === controlId));
            addAuditEntry('delete', 'controls', _ctrl ? _ctrl.title : controlId);
      saveData();
      _csiPersistAffectedTabs(['controls', 'soa', 'actions']); // FIX-CASCADE-PERSIST : persiste les sections croisées nettoyées
      loadControlsTable();
      loadSoaTable();
      loadActionsTable(); // PASS2-301: Refresh actions table after cascade delete
      updateDashboard();
    }, { type: 'danger' });
  }
  window.loadControlsTable = loadControlsTable;
  window.loadNis2ControlsTable = loadNis2ControlsTable;
  window.loadSoaTable = loadSoaTable;
  window.editNis2Control = editNis2Control;
  window.editControl = editControl;
  window.viewControlDetails = viewControlDetails;
  window.addControl = addControl;
  window.deleteControl = deleteControl;
  // Fonctions SoA helper pour indicateur non-applicable et risques liés
  window.isControlNonApplicable = isControlNonApplicable;
  window.getSoaJustification = getSoaJustification;
  window.navigateToSoaControl = navigateToSoaControl;
  window.editSoaControl = editSoaControl;
  window.renderSoaRiskBadges = renderSoaRiskBadges;

  // modules/actions.js
  function getActionPriorityOptions() {
    return [
      { value: "critical", label: t('actionPriorities.critical') },
      { value: "high", label: t('actionPriorities.high') },
      { value: "medium", label: t('actionPriorities.medium') },
      { value: "low", label: t('actionPriorities.low') }
    ];
  }
  var ACTION_PRIORITY_OPTIONS = getActionPriorityOptions();
  function getActionStatusOptions() {
    return [
      { value: "planifie", label: t('actionStatuses.planifie') },
      { value: "en_cours", label: t('actionStatuses.enCours') },
      { value: "termine", label: t('actionStatuses.termine') },
      { value: "annule", label: t('actionStatuses.annule') }
    ];
  }
  var ACTION_STATUS_OPTIONS = getActionStatusOptions();
  function loadActionsTable() {
    const tbody = document.getElementById("actionsTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('actions')) return;
    tbody.innerHTML = "";
    const _s = SectionToolbar.getSort("actions");
    const sort = _s ? _s.key : "priority";
    const sortDir = _s ? _s.dir : "asc";
    updateActionsFilterOptions();
    const _f = SectionToolbar.getFilters("actions");
    const _q = (SectionToolbar.getSearch("actions") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const filterType = _f.filterType || "";
    const filterValue = _f.filterValue || "";
    const filterStatus = _f.status || ""; // (audit global 2026-07-03) filtre par statut d'action
    const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
    const getActionIdNumber = (value) => {
      const text = value ? String(value) : "";
      const match = text.match(/(\d+)(?!.*\d)/);
      return match ? parseInt(match[1], 10) : NaN;
    };
    const getActionLinkSortKey = (action) => {
      const type = action.linkType || (action.controlId ? "control" : "");
      const ids = getActionLinkIds(action);
      const byId = (items, id) => items.find((item) => item.id === id);
      if (type === "risk") {
        const labels = ids
          .map((id) => {
            const r = byId(appData.risks, id);
            return r ? r.title : id;
          })
          .filter(Boolean);
        const idKey = ids.slice().sort().join(",");
        return `risk:${idKey}:${labels.join(" | ")}`;
      }
      if (type === "audit") {
        const audit = byId(appData.audits, ids[0]);
        const label = audit ? audit.title : ids[0];
        const key = ids[0] ? `audit:${ids[0]}` : "audit:missing";
        return `${key}:${label || ""}`;
      }
      if (type === "nc") {
        const nc = byId(appData.nonconformities, ids[0]);
        const auditId = nc && nc.auditId ? nc.auditId : "";
        if (auditId) {
          const audit = byId(appData.audits, auditId);
          const label = audit ? audit.title : auditId;
          return `audit:${auditId}:${label || ""}`;
        }
        const label = nc ? nc.title : ids[0];
        return `nc:${ids[0] || ""}:${label || ""}`;
      }
      if (type === "control") {
        const ctrl = byId(appData.controls, ids[0]);
        const label = ctrl ? ctrl.reference : ids[0];
        return `control:${ids[0] || ""}:${label || ""}`;
      }
      if (type === "threat") {
        const th = byId(appData.threats, ids[0]);
        const label = th ? th.title : ids[0];
        return `threat:${ids[0] || ""}:${label || ""}`;
      }
      if (type === "continuous") {
        const label = "Am\xE9lioration continue";
        return `continuous:${ids[0] || ""}:${label}`;
      }
      return `other:${type || ""}`;
    };
    let sourceActions = filterType
      ? (appData.actions || []).filter((action) => matchesActionFilter(action, filterType, filterValue))
      : (appData.actions || []);
    if (filterStatus) sourceActions = sourceActions.filter((action) => (action.status || 'planifie') === filterStatus);
    if (_q) sourceActions = sourceActions.filter((action) => ((action.title || "") + " " + (action.id || "") + " " + (action.assignedTo || "")).toLowerCase().indexOf(_q) >= 0);
    const sortedActions = [...sourceActions].sort((a, b) => {
      switch (sort) {
        case "id": {
          const aNum = getActionIdNumber(a.id);
          const bNum = getActionIdNumber(b.id);
          if (Number.isFinite(aNum) && Number.isFinite(bNum)) return aNum - bNum;
          return SortUtils.reference(a.id, b.id);
        }
        case "link": {
          const keyA = getActionLinkSortKey(a);
          const keyB = getActionLinkSortKey(b);
          const cmp = SortUtils.reference(keyA, keyB);
          if (cmp !== 0) return cmp;
          const fallbackA = getActionIdNumber(a.id);
          const fallbackB = getActionIdNumber(b.id);
          if (Number.isFinite(fallbackA) && Number.isFinite(fallbackB)) return fallbackA - fallbackB;
          return SortUtils.reference(a.id, b.id);
        }
        case "dueDate":
          return SortUtils.date(a.dueDate, b.dueDate);
        case "status": {
          var _so = { planifie: 0, en_cours: 1, termine: 2, annule: 3 };
          return (_so[a.status] ?? 0) - (_so[b.status] ?? 0);
        }
        case "progress":
          return SortUtils.numeric(a.progress, b.progress);
        default:
          return SortUtils.orderedMap(priorityOrder)(a.priority, b.priority);
      }
    });
    if (sortDir === "desc") sortedActions.reverse();
    if (sortedActions.length === 0) {
      var _er = document.createElement('tr'); var _et = document.createElement('td');
      _et.setAttribute('colspan', '8'); _et.className = 'table__empty-state';
      var _ei = document.createElement('i'); _ei.className = 'fas fa-tasks';
      _ei.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      _et.appendChild(_ei); _et.appendChild(document.createTextNode(t('emptyStates.noActions', 'Aucune action en cours')));
      _er.appendChild(_et); tbody.appendChild(_er); return;
    }
    sortedActions.forEach((action) => {
      const row = document.createElement("tr");
      let linkLabels = [];
      let linkClass = "";
      const type = action.linkType || (action.controlId ? "control" : "");
      const ids = getActionLinkIds(action);
      switch (type) {
        case "control": {
          const id = ids[0];
          if (id) {
            const resolved = resolveControlById(id);
            linkLabels = [resolved ? resolved.label : id];
            linkClass = "link-badge--control";
          }
          break;
        }
        case "risk": {
          linkLabels = ids
            .map((id) => {
              const r = (appData.risks || []).find((rr) => rr.id === id);
              return r ? r.title : id;
            })
            .filter(Boolean);
          if (linkLabels.length) linkClass = "link-badge--risk";
          break;
        }
        case "nc": {
          const id = ids[0];
          if (id) {
            const nc = appData.nonconformities.find((n) => n.id === id);
            linkLabels = [nc ? nc.title : id];
            linkClass = "link-badge--nc";
          }
          break;
        }
        case "audit": {
          const id = ids[0];
          if (id) {
            const au = appData.audits.find((a) => a.id === id);
            linkLabels = [au ? au.title : id];
            linkClass = "link-badge--audit";
          }
          break;
        }
        case "threat": {
          const id = ids[0];
          if (id) {
            const th = appData.threats.find((t) => t.id === id);
            linkLabels = [th ? th.title : id];
            linkClass = "link-badge--threat";
          }
          break;
        }
        case "continuous": {
          linkLabels = ids.length ? ids.map(() => "Am\xE9lioration continue") : [];
          if (linkLabels.length) linkClass = "link-badge--continuous";
          break;
        }
      }
      const linkHtml = linkLabels.length
        ? linkLabels.map((label) => `<span class="link-badge ${linkClass}">${escapeHtml(label)}</span>`).join(" ")
        : "—";
      const priorityClasses = { critical: "priority--critical", high: "priority--high", medium: "priority--medium", low: "priority--low" };
      const priorityLabels = { critical: t('actionPriorities.critical'), high: t('actionPriorities.high'), medium: t('actionPriorities.medium'), low: t('actionPriorities.low') };
      // Échéance dépassée (comparaison de dates locales, sans heure) — actions terminées/annulées exclues
      let actionOverdue = false;
      if (action.dueDate && action.status !== "termine" && action.status !== "annule" && action.status !== "done") {
        const dueStr = String(action.dueDate);
        if (/^\d{4}-\d{2}-\d{2}/.test(dueStr)) {
          const now = new Date();
          const todayIso = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0");
          actionOverdue = dueStr.slice(0, 10) < todayIso;
        } else {
          const due = new Date(dueStr);
          if (!Number.isNaN(due.getTime())) {
            due.setHours(0, 0, 0, 0);
            const today0 = new Date();
            today0.setHours(0, 0, 0, 0);
            actionOverdue = due < today0;
          }
        }
      }
      row.innerHTML = `
            <td>${escapeHtml(action.id)}</td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(action.title)}</div></td>
            <td><div class="cell-clamp cell-clamp--2">${linkHtml}</div></td>
            <td><span class="status-badge ${priorityClasses[action.priority]}">${priorityLabels[action.priority]}</span></td>
            <td><span class="status-badge status-action status-action--${escapeHtml(action.status || 'planifie')}">${escapeHtml(getStatusLabel(action.status || 'planifie'))}</span></td>
            <td><div class="progress-bar" role="progressbar" aria-valuenow="${action.progress}" aria-valuemin="0" aria-valuemax="100" aria-label="${escapeHtml(action.title)} ${action.progress}%"><div class="progress-bar__fill" style="width: ${action.progress}%"></div></div><span>${action.progress}%</span></td>
            <td${actionOverdue ? ` class="cell-overdue" title="${escapeHtml(t('actions.overdue', 'En retard'))}"` : ""}>${action.dueDate ? new Date(action.dueDate).toLocaleDateString() : "—"}</td>
            <td>${action.assignedTo ? escapeHtml(action.assignedTo) : "—"}</td>
        `;
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger") || e.target.closest(".critical-asset-link")) return;
        editAction(action.id);
      });
      attachCriticalAssetLinkHandlers(row);
      tbody.appendChild(row);
    });
  }

  /**
   * Build grouped control options from all enabled frameworks.
   * Returns { flat: [{value, label, searchText}], groups: [{id, label, color, subgroups}] }
   * Reusable anywhere a control selector is needed.
   */
  function getControlLinkOptions() {
    var FM = window.CSI_FrameworkMapper;
    var result = { flat: [], groups: [] };

    // --- Controls framework (iso27002 or open-security-controls) ---
    var isoControls = appData.controls || [];
    var _ctrlFwId = _getActiveControlsFrameworkId() || 'iso27002';
    var isoFw = FM ? FM.getFramework(_ctrlFwId) : null;
    if (!isoFw) { var _ctrlFwObj = _getActiveControlsFramework(); if (_ctrlFwObj) isoFw = _ctrlFwObj; }
    var isoMeta = isoFw ? isoFw.meta : null;
    var isoCategories = isoMeta ? isoMeta.categories : [
      { id: 'org', label: 'Organisationnel', prefix: '5' },
      { id: 'hum', label: 'Humain', prefix: '6' },
      { id: 'phy', label: 'Physique', prefix: '7' },
      { id: 'tech', label: 'Technologique', prefix: '8' }
    ];
    var isoGroup = {
      id: _ctrlFwId,
      label: isoMeta ? (isoMeta.shortName || isoMeta.name) : 'ISO 27002',
      color: isoMeta ? isoMeta.color : '#2563eb',
      subgroups: []
    };
    isoCategories.forEach(function(cat) {
      var catControls = isoControls.filter(function(c) {
        if (c.category === cat.label) return true;
        if (c.numero && c.numero.startsWith(cat.prefix + '.')) return true;
        return false;
      });
      if (!catControls.length) return;
      var catDisplayLabel = isoCatLabel(cat);
      var subgroup = {
        id: cat.id,
        label: catDisplayLabel + ' (' + cat.prefix + '.x)',
        options: catControls.map(function(c) {
          var lbl = c.reference || (c.numero + ' ' + isoCtrlTitle(c));
          return {
            value: c.id,
            label: lbl,
            searchText: ((c.numero || '') + ' ' + isoCtrlTitle(c) + ' ' + (c.reference || '') + ' ' + catDisplayLabel).toLowerCase()
          };
        })
      };
      isoGroup.subgroups.push(subgroup);
      result.flat = result.flat.concat(subgroup.options);
    });
    // Controls not matching any category
    var categorized = new Set();
    isoGroup.subgroups.forEach(function(sg) { sg.options.forEach(function(o) { categorized.add(o.value); }); });
    var uncatISO = isoControls.filter(function(c) { return !categorized.has(c.id); });
    if (uncatISO.length) {
      var uncatSub = {
        id: 'other',
        label: t('common.other', 'Autres'),
        options: uncatISO.map(function(c) {
          var lbl = c.reference || (c.numero + ' ' + isoCtrlTitle(c));
          return { value: c.id, label: lbl, searchText: ((c.numero || '') + ' ' + isoCtrlTitle(c) + ' ' + (c.reference || '')).toLowerCase() };
        })
      };
      isoGroup.subgroups.push(uncatSub);
      result.flat = result.flat.concat(uncatSub.options);
    }
    if (isoGroup.subgroups.length) result.groups.push(isoGroup);

    // --- NIS2 / CyFun 2025 (if enabled) ---
    if (FM && FM.isEnabled('nis2')) {
      var nis2Controls = appData.nis2Controls || [];
      var nis2Fw = FM.getFramework('cyfun2025');
      if (nis2Fw && nis2Controls.length > 0) {
        var functions = nis2Fw.meta.functions || [];
        var nis2Group = {
          id: 'cyfun2025',
          label: nis2Fw.meta.name || 'CyFun 2025',
          color: nis2Fw.meta.color || '#059669',
          subgroups: []
        };
        functions.forEach(function(func) {
          var funcControls = nis2Controls.filter(function(c) { return c.functionId === func.id; });
          if (!funcControls.length) return;
          var funcLabel = cyfunLabelText(func) + ' (' + func.id + ')';
          var subgroup = {
            id: func.id,
            label: funcLabel,
            options: funcControls.map(function(c) {
              var lbl = c.ref + ' \u2014 ' + (c.title || c.categorie || c.description || '');
              return {
                value: c.id,
                label: lbl,
                searchText: (c.ref + ' ' + (c.title || '') + ' ' + (c.categorie || '') + ' ' + (c.description || '') + ' ' + funcLabel).toLowerCase()
              };
            })
          };
          nis2Group.subgroups.push(subgroup);
          result.flat = result.flat.concat(subgroup.options);
        });
        if (nis2Group.subgroups.length) result.groups.push(nis2Group);
      }
    }

    // --- Future frameworks: automatic inclusion ---
    if (FM) {
      var known = { iso27002: true, cyfun2025: true, nis2: true };
      FM.listEnabledFrameworks().forEach(function(fw) {
        if (known[fw.id]) return;
        var fwDef = FM.getFramework(fw.id);
        var fwControls = FM.getAppDataControls(fw.id);
        if (!fwDef || !fwControls || !fwControls.length) return;
        var fwGroup = { id: fw.id, label: fw.name, color: fw.color || '#6b7280', subgroups: [] };
        var categories = (fwDef.meta.categories || []);
        if (categories.length) {
          categories.forEach(function(cat) {
            var catCtrls = fwControls.filter(function(c) { return c.categoryId === cat.id || c.category === cat.id; });
            if (!catCtrls.length) return;
            fwGroup.subgroups.push({
              id: cat.id,
              label: cat.label || cat.id,
              options: catCtrls.map(function(c) {
                var lbl = (c.ref || c.numero || c.id) + ' \u2014 ' + isoCtrlTitle(c);
                return { value: c.id, label: lbl, searchText: (lbl + ' ' + (cat.label || '')).toLowerCase() };
              })
            });
          });
        } else {
          fwGroup.subgroups.push({
            id: 'all',
            label: fw.name,
            options: fwControls.map(function(c) {
              var lbl = (c.ref || c.numero || c.id) + ' \u2014 ' + isoCtrlTitle(c);
              return { value: c.id, label: lbl, searchText: lbl.toLowerCase() };
            })
          });
        }
        fwGroup.subgroups.forEach(function(sg) { result.flat = result.flat.concat(sg.options); });
        if (fwGroup.subgroups.length) result.groups.push(fwGroup);
      });
    }

    return result;
  }

  /**
   * Resolve a control ID across all framework appData arrays.
   * Returns { control, framework, label } or null.
   */
  function resolveControlById(id) {
    if (!id) return null;
    var ctrl = (appData.controls || []).find(function(c) { return c.id === id; });
    if (ctrl) return { control: ctrl, framework: _getActiveControlsFrameworkId() || 'iso27002', label: ctrl.reference || (ctrl.numero + ' ' + isoCtrlTitle(ctrl)) };
    var nis2 = (appData.nis2Controls || []).find(function(c) { return c.id === id; });
    if (nis2) return { control: nis2, framework: 'cyfun2025', label: nis2.ref + ' \u2014 ' + (nis2.categorie || nis2.description || nis2.titre || '') };
    return null;
  }

  /**
   * Factory: create a searchable-select widget (standalone, reusable).
   * @param {string} name - field name (used for IDs)
   * @param {Array} groups - [{id, label, color, subgroups: [{id, label, options: [{value, label, searchText}]}]}]
   * @param {string} value - initial selected value
   * @param {string} placeholder - trigger placeholder
   * @param {boolean} locked - if true, disable interaction
   * @returns {{ container: HTMLElement, hiddenInput: HTMLElement, destroy: Function }}
   */
  function buildSearchableSelectWidget(name, groups, value, placeholder, locked) {
    var ssContainer = document.createElement("div");
    ssContainer.className = "searchable-select";
    ssContainer.id = "modal-ss-wrap-" + name;

    var hiddenInput = document.createElement("input");
    hiddenInput.type = "hidden";
    hiddenInput.id = "modal-input-" + name;
    hiddenInput.value = value || "";

    var ssTrigger = document.createElement("input");
    ssTrigger.type = "text";
    ssTrigger.className = "form-control searchable-select__trigger";
    ssTrigger.readOnly = true;
    ssTrigger.placeholder = placeholder || t('common.search', 'Rechercher...') + "\u2026";
    ssTrigger.id = "modal-ss-trigger-" + name;

    var ssDropdown = document.createElement("div");
    ssDropdown.className = "searchable-select__dropdown";
    var ssSearch = document.createElement("input");
    ssSearch.type = "text";
    ssSearch.className = "searchable-select__search";
    ssSearch.placeholder = t('common.filter', 'Filtrer') + "\u2026";
    ssSearch.autocomplete = "off";
    var ssList = document.createElement("div");
    ssList.className = "searchable-select__list";
    var ssEmpty = document.createElement("div");
    ssEmpty.className = "searchable-select__empty";
    ssEmpty.textContent = t('search.noResults', 'Aucun résultat trouvé');
    ssEmpty.style.display = "none";

    var ssAllOptions = [];
    (groups || []).forEach(function(group) {
      var groupEl = document.createElement("div");
      groupEl.className = "searchable-select__group";
      groupEl.dataset.groupId = group.id;
      var groupHeader = document.createElement("div");
      groupHeader.className = "searchable-select__group-header";
      if (group.color) {
        var dot = document.createElement("span");
        dot.className = "searchable-select__fw-dot";
        dot.style.backgroundColor = group.color;
        groupHeader.appendChild(dot);
      }
      groupHeader.appendChild(document.createTextNode(group.label));
      groupEl.appendChild(groupHeader);

      (group.subgroups || []).forEach(function(subgroup) {
        var subgroupEl = document.createElement("div");
        subgroupEl.className = "searchable-select__subgroup";
        subgroupEl.dataset.subgroupId = subgroup.id;
        var subHeader = document.createElement("div");
        subHeader.className = "searchable-select__subgroup-header";
        subHeader.textContent = subgroup.label;
        subgroupEl.appendChild(subHeader);
        (subgroup.options || []).forEach(function(opt) {
          var optEl = document.createElement("div");
          optEl.className = "searchable-select__option";
          optEl.dataset.value = opt.value;
          optEl.textContent = opt.label;
          optEl.title = opt.label;
          optEl.dataset.searchText = opt.searchText || opt.label.toLowerCase();
          subgroupEl.appendChild(optEl);
          ssAllOptions.push({ el: optEl, subgroupEl: subgroupEl, groupEl: groupEl, searchText: optEl.dataset.searchText, value: opt.value, label: opt.label });
        });
        groupEl.appendChild(subgroupEl);
      });
      ssList.appendChild(groupEl);
    });
    ssList.appendChild(ssEmpty);
    ssDropdown.appendChild(ssSearch);
    ssDropdown.appendChild(ssList);
    ssContainer.appendChild(ssTrigger);
    ssContainer.appendChild(ssDropdown);
    ssContainer.appendChild(hiddenInput);

    // Set initial display
    var initOpt = ssAllOptions.find(function(o) { return o.value === (value || ""); });
    if (initOpt) {
      ssTrigger.value = initOpt.label;
      initOpt.el.classList.add("searchable-select__option--selected");
    }

    // Normalize for accent-insensitive search
    var ssNormalize = function(str) {
      return str.normalize ? str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase() : str.toLowerCase();
    };

    // Filter
    var ssFilter = function() {
      var query = ssNormalize(ssSearch.value);
      var anyVisible = false;
      var visSub = {}, visGrp = {};
      ssAllOptions.forEach(function(item) {
        var match = !query || ssNormalize(item.searchText).indexOf(query) >= 0;
        item.el.style.display = match ? "" : "none";
        if (match) { anyVisible = true; visSub[item.subgroupEl.dataset.subgroupId] = true; visGrp[item.groupEl.dataset.groupId] = true; }
      });
      ssList.querySelectorAll(".searchable-select__subgroup").forEach(function(el) { el.style.display = visSub[el.dataset.subgroupId] ? "" : "none"; });
      ssList.querySelectorAll(".searchable-select__group").forEach(function(el) { el.style.display = visGrp[el.dataset.groupId] ? "" : "none"; });
      ssEmpty.style.display = anyVisible ? "none" : "";
    };

    // Open/Close
    var ssIsOpen = false;
    var ssOpen = function() {
      if (ssIsOpen) return;
      ssIsOpen = true;
      ssContainer.classList.add("searchable-select--open");
      var mb = ssContainer.closest(".modal__body");
      if (mb) mb.style.overflow = "visible";
      ssSearch.value = "";
      ssFilter();
      requestAnimationFrame(function() { ssSearch.focus(); });
      var sel = ssList.querySelector(".searchable-select__option--selected");
      if (sel) sel.scrollIntoView({ block: "nearest" });
    };
    var ssClose = function() {
      if (!ssIsOpen) return;
      ssIsOpen = false;
      ssContainer.classList.remove("searchable-select--open");
      var mb = ssContainer.closest(".modal__body");
      if (mb) mb.style.overflow = "";
      ssFocusedIdx = -1;
      ssAllOptions.forEach(function(o) { o.el.classList.remove("searchable-select__option--focused"); });
    };

    // Select
    var ssSelect = function(val, lbl) {
      hiddenInput.value = val;
      ssTrigger.value = lbl;
      ssAllOptions.forEach(function(o) { o.el.classList.remove("searchable-select__option--selected"); });
      var m = ssAllOptions.find(function(o) { return o.value === val; });
      if (m) m.el.classList.add("searchable-select__option--selected");
      ssClose();
    };

    // Keyboard
    var ssFocusedIdx = -1;
    var ssGetVisible = function() { return ssAllOptions.filter(function(o) { return o.el.style.display !== "none"; }); };
    var ssSetFocus = function(idx) {
      var vis = ssGetVisible();
      ssAllOptions.forEach(function(o) { o.el.classList.remove("searchable-select__option--focused"); });
      if (idx < 0 || idx >= vis.length) { ssFocusedIdx = -1; return; }
      ssFocusedIdx = idx;
      vis[idx].el.classList.add("searchable-select__option--focused");
      vis[idx].el.scrollIntoView({ block: "nearest" });
    };

    // Events
    ssTrigger.addEventListener("click", function(e) { e.stopPropagation(); ssIsOpen ? ssClose() : ssOpen(); });
    ssTrigger.addEventListener("keydown", function(e) {
      if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault(); ssOpen(); }
    });
    ssSearch.addEventListener("input", function() { ssFilter(); ssFocusedIdx = -1; });
    ssSearch.addEventListener("keydown", function(e) {
      var vis = ssGetVisible();
      if (e.key === "ArrowDown") { e.preventDefault(); ssSetFocus(Math.min(ssFocusedIdx + 1, vis.length - 1)); }
      else if (e.key === "ArrowUp") { e.preventDefault(); ssSetFocus(Math.max(ssFocusedIdx - 1, 0)); }
      else if (e.key === "Enter") { e.preventDefault(); if (ssFocusedIdx >= 0 && ssFocusedIdx < vis.length) ssSelect(vis[ssFocusedIdx].value, vis[ssFocusedIdx].label); }
      else if (e.key === "Escape") { e.preventDefault(); ssClose(); ssTrigger.focus(); }
    });
    ssList.addEventListener("click", function(e) {
      var optEl = e.target.closest(".searchable-select__option");
      if (optEl) ssSelect(optEl.dataset.value, optEl.textContent);
    });

    // Click outside handler
    var ssOutsideHandler = function(e) {
      if (!ssContainer.contains(e.target)) ssClose();
      if (!document.body.contains(ssContainer)) document.removeEventListener("click", ssOutsideHandler);
    };
    requestAnimationFrame(function() { document.addEventListener("click", ssOutsideHandler); });

    // Lock
    if (locked) {
      ssTrigger.disabled = true;
      ssContainer.style.pointerEvents = "none";
      ssContainer.style.opacity = "0.6";
    }

    return {
      container: ssContainer,
      hiddenInput: hiddenInput,
      destroy: function() { document.removeEventListener("click", ssOutsideHandler); ssClose(); }
    };
  }

  /**
   * Shared handler: bind linkType select to linkId field with dynamic swap.
   * Handles switching between searchable-select (for "control") and native select.
   * @param {string} initialLinkType - current linkType value
   * @param {*} initialSelected - current linkId value (string or array)
   */
  function bindLinkTypeToLinkId(initialLinkType, initialSelected) {
    var typeSelect = document.getElementById("modal-input-linkType");
    if (!typeSelect) return;
    var modalBody = document.getElementById("modalBody");
    if (!modalBody) return;
    var _currentSsWidget = null;

    function findFieldElements() {
      var inp = document.getElementById("modal-input-linkId");
      if (!inp) return null;
      var wrap = inp.closest(".searchable-select");
      var fieldEl = wrap || inp;
      return { fieldEl: fieldEl, inp: inp };
    }

    function removeCurrentField() {
      if (_currentSsWidget) { _currentSsWidget.destroy(); _currentSsWidget = null; }
      var found = findFieldElements();
      if (found) found.fieldEl.remove();
    }

    function getInsertionPoint() {
      // Find the label for linkId by checking all labels
      var labels = modalBody.querySelectorAll("label");
      for (var i = 0; i < labels.length; i++) {
        if (labels[i].htmlFor === "modal-input-linkId") return labels[i];
      }
      return null;
    }

    function rebuild(selected) {
      var type = typeSelect.value;
      var label = getInsertionPoint();
      removeCurrentField();
      if (!label) return;

      if (type === "control") {
        var controlOpts = getControlLinkOptions();
        var selVal = Array.isArray(selected) ? selected[0] : (selected || "");
        // Validate value exists
        if (selVal && !controlOpts.flat.some(function(o) { return o.value === selVal; })) {
          selVal = controlOpts.flat.length ? controlOpts.flat[0].value : "";
        }
        _currentSsWidget = buildSearchableSelectWidget("linkId", controlOpts.groups, selVal, t('common.searchControl', 'Rechercher un contrôle') + "\u2026", false);
        label.after(_currentSsWidget.container);
      } else {
        var options = getLinkOptions(type);
        var sel = document.createElement("select");
        sel.id = "modal-input-linkId";
        sel.className = "form-control";
        sel.dataset.readonlyLock = "true";
        var selectedArr = type === "risk" ? (Array.isArray(selected) ? selected : [selected].filter(Boolean)) : [Array.isArray(selected) ? selected[0] : selected].filter(Boolean);
        if (type === "risk") {
          sel.multiple = true;
          sel.size = Math.min(6, Math.max(3, options.length));
        }
        options.forEach(function(opt) {
          var o = document.createElement("option");
          o.value = opt.value;
          o.textContent = opt.label;
          if (selectedArr.includes(opt.value)) o.selected = true;
          sel.appendChild(o);
        });
        if (!sel.multiple && options.length && !selectedArr.length) {
          sel.value = options[0].value;
        }
        label.after(sel);
      }
    }

    // Initial build
    var initSel = Array.isArray(initialSelected) ? initialSelected : [initialSelected].filter(Boolean);
    rebuild(initialLinkType === "risk" ? initSel : initSel);

    // Re-build on linkType change
    typeSelect.addEventListener("change", function() { rebuild(); });
  }

  function getLinkOptions(type) {
    switch (type) {
      case "control":
        return getControlLinkOptions().flat;
      case "risk":
        return (appData.risks || []).map(r => ({ value: r.id, label: r.title }));
      case "nc":
        return (appData.nonconformities || []).map(nc => ({ value: nc.id, label: nc.title }));
      case "audit":
        return appData.audits.map(a => ({ value: a.id, label: a.title }));
      case "threat":
        return appData.threats.map(t => ({ value: t.id, label: t.title }));
      case "continuous":
        return [
          { value: "continuous_improvement", label: "Am\xE9lioration continue" }
        ];
      default:
        return [];
    }
  }
  function getActionFilterOptions(type) {
    switch (type) {
      case "risk":
        return (appData.risks || []).map(r => ({ value: r.id, label: r.title }));
      case "nc":
        return (appData.nonconformities || []).map(nc => ({ value: nc.id, label: nc.title }));
      case "audit":
        return appData.audits.map(a => ({ value: a.id, label: a.title }));
      case "threat":
        return appData.threats.map(t => ({ value: t.id, label: t.title }));
      case "owner": {
        const names = new Set();
        const addName = (value) => {
          const cleaned = typeof value === "string" ? value.trim() : "";
          if (cleaned) names.add(cleaned);
        };
        (appData.actions || []).forEach((action) => addName(action.assignedTo));
        (appData.stakeholders || []).forEach((st) => addName(st.name));
        const options = Array.from(names)
          .sort(SortUtils.text)
          .map((name) => ({ value: name, label: name }));
        if ((appData.actions || []).some((action) => !action.assignedTo)) {
          options.unshift({ value: "unassigned", label: "Non assigné" });
        }
        return options;
      }
      case "continuous":
        return [
          { value: "continuous_improvement", label: "Am\xE9lioration continue" }
        ];
      default:
        return [];
    }
  }
  function updateActionsFilterOptions() {
    const _f = SectionToolbar.getFilters("actions");
    const type = _f.filterType || "";
    const options = type ? getActionFilterOptions(type) : [];
    SectionToolbar.updateFilterOptions("actions", "filterValue", options);
  }
  function matchesActionFilter(action, filterType, filterValue) {
    if (!action || !filterType || filterType === "all") return true;
    // Type-only filtering: filterValue is "all" → match all actions of this type
    if (!filterValue || filterValue === "all") {
      if (filterType === "owner") return true;
      if (filterType === "audit") {
        if (action.linkType === "audit") return true;
        const ncIds = [];
        if (action.linkType === "nc") ncIds.push(...getActionLinkIds(action));
        if (Array.isArray(action.ncIds)) ncIds.push(...action.ncIds);
        return ncIds.some((ncId) => {
          const nc = appData.nonconformities.find((n) => n.id === ncId);
          return nc && nc.auditId;
        });
      }
      return action.linkType === filterType;
    }
    // Specific-value filtering
    if (filterType === "risk") {
      return action.linkType === "risk" && getActionLinkIds(action).includes(filterValue);
    }
    if (filterType === "nc") {
      if (action.linkType === "nc" && getActionLinkIds(action).includes(filterValue)) return true;
      return (action.ncIds || []).includes(filterValue);
    }
    if (filterType === "audit") {
      if (action.linkType === "audit" && getActionLinkIds(action).includes(filterValue)) return true;
      const ncIds = [];
      if (action.linkType === "nc") ncIds.push(...getActionLinkIds(action));
      if (Array.isArray(action.ncIds)) ncIds.push(...action.ncIds);
      if (!ncIds.length) return false;
      return ncIds.some((ncId) => {
        const nc = appData.nonconformities.find((n) => n.id === ncId);
        return nc && nc.auditId === filterValue;
      });
    }
    if (filterType === "threat") {
      return action.linkType === "threat" && getActionLinkIds(action).includes(filterValue);
    }
    if (filterType === "owner") {
      const assignedTo = (action.assignedTo || "").trim();
      if (filterValue === "unassigned") return !assignedTo;
      return assignedTo === filterValue;
    }
    if (filterType === "continuous") {
      return action.linkType === "continuous" && getActionLinkIds(action).includes(filterValue);
    }
    return true;
  }
  function normalizeActionLinkValue(type, value) {
    if (type === "risk") {
      if (Array.isArray(value)) return value.filter(Boolean);
      if (value) return [value];
      return [];
    }
    if (Array.isArray(value)) return value[0] || "";
    return value || "";
  }
  function getActionLinkIds(action) {
    if (!action) return [];
    const type = action.linkType || (action.controlId ? "control" : "");
    if (type === "risk") return normalizeActionLinkValue("risk", action.linkId);
    const id = action.linkId || action.controlId;
    return id ? [id] : [];
  }
  function actionLinksToRisk(action, riskId) {
    if (!action || action.linkType !== "risk") return false;
    return getActionLinkIds(action).includes(riskId);
  }
  function normalizeActionProgressStatus(rawProgress, rawStatus) {
    const parsed = parseInt(rawProgress, 10);
    const progress = Number.isFinite(parsed) ? Math.max(0, Math.min(100, parsed)) : 0;
    let status = rawStatus || "planifie";
    if (progress >= 100 && status !== "annule") status = "termine";
    if (status === "termine" && progress < 100) return { progress: 100, status };
    return { progress, status };
  }
  function bindActionStatusProgressSync() {
    const progressInput = document.getElementById("modal-input-progress");
    const statusSelect = document.getElementById("modal-input-status");
    if (!progressInput || !statusSelect) return;
    const getProgress = () => {
      const raw = parseInt(progressInput.value, 10);
      if (!Number.isFinite(raw)) return null;
      const value = Math.max(0, Math.min(100, raw));
      if (String(value) !== String(progressInput.value)) progressInput.value = String(value);
      return value;
    };
    const syncFromProgress = () => {
      const value = getProgress();
      if (value === null) return;
      if (value >= 100 && statusSelect.value !== "termine" && statusSelect.value !== "annule") {
        statusSelect.value = "termine";
      }
    };
    const syncFromStatus = () => {
      if (statusSelect.value === "termine") {
        progressInput.value = "100";
      }
    };
    progressInput.addEventListener("input", syncFromProgress);
    progressInput.addEventListener("change", syncFromProgress);
    statusSelect.addEventListener("change", syncFromStatus);
    syncFromStatus();
    syncFromProgress();
  }
  function editAction(actionId) {
    const action = (appData.actions || []).find((a) => a.id === actionId);
    if (!action) return;
    const linkType = action.linkType || "control";
    const currentLinkId = action.linkId || action.controlId || "";
    const normalizedValue = normalizeActionLinkValue(linkType, currentLinkId);
    const criticalAssetIds = Array.isArray(action.criticalAssetIds) ? action.criticalAssetIds : [];
    const criticalAssetOptions = getCriticalAssetOptions(criticalAssetIds);
    // linkId rendered as empty select placeholder — bindLinkTypeToLinkId replaces it immediately
    openModal(t('modals.editAction'), [
      { name: "title", label: t('actions.colTitle', 'Titre'), value: action.title },
      { name: "linkType", label: t('actions.linkType', 'Type de lien'), type: "select", value: linkType, options: [
          { value: "control", label: t('actionLinkTypes.control', 'Contrôle') },
          { value: "risk", label: t('actionLinkTypes.risk', 'Risque') },
          { value: "nc", label: t('actionLinkTypes.nc', 'Non-conformité') },
          { value: "audit", label: t('actionLinkTypes.audit', 'Audit') },
          { value: "threat", label: t('actionLinkTypes.threat', 'Menace') },
          { value: "continuous", label: t('actionLinkTypes.continuous', 'Amélioration continue') }
        ] },
      { name: "linkId", label: t('actions.linkedElement', 'Élément lié'), type: "select", value: "", options: [] },
      { name: "criticalAssetIds", label: t('fields.criticalAssets'), type: "multiselect", value: criticalAssetIds, options: criticalAssetOptions },
      {
        name: "assignedTo",
        label: t('fields.owner'),
        type: "select",
        value: action.assignedTo || "",
        // FIX-ACTION-OWNER-RESET (audit 2026-06-10) : option vide + valeur courante
        // conservée si absente de la liste des intervenants. Sans elles, le navigateur
        // sélectionnait le PREMIER intervenant pour une action non assignée (ou dont le
        // responsable a été supprimé/renommé) et la sauvegarde réassignait en silence.
        options: (() => {
          const opts = [{ value: '', label: '— ' + t('common.select', 'Sélectionner') + ' —' }]
            .concat(appData.stakeholders.map((s) => ({ value: s.name, label: s.name })));
          const cur = action.assignedTo || "";
          if (cur && !appData.stakeholders.some((s) => s.name === cur)) {
            opts.push({ value: cur, label: cur });
          }
          return opts;
        })()
      },
      { name: "priority", label: t('actions.priority', 'Priorité'), type: "select", value: action.priority, options: ACTION_PRIORITY_OPTIONS },
      { name: "dueDate", label: t('actions.dueDate', 'Échéance'), type: "date", value: action.dueDate },
      { name: "status", label: t('fields.status'), type: "select", value: action.status, options: ACTION_STATUS_OPTIONS },
      { name: "progress", label: t('actions.progress', 'Avancement') + " (%)", value: action.progress, type: "number" },
      { name: "existingComments", label: t('fields.existingComments'), type: "textarea", value: formatCommentsReadonly(action.comments), readOnly: true },
      { name: "newComment", label: t('fields.newComment'), type: "textarea" }
    ], (data) => {
      const resolvedLinkType = data.linkType || "control";
      const resolvedLinkValue = normalizeActionLinkValue(resolvedLinkType, data.linkId);
      const normalized = normalizeActionProgressStatus(data.progress, data.status);
      action.title = data.title;
      action.linkType = resolvedLinkType;
      action.linkId = resolvedLinkType === "risk" ? resolvedLinkValue : (resolvedLinkValue || "");
      action.controlId = resolvedLinkType === "control" ? (resolvedLinkValue || "") : "";
      action.progress = normalized.progress;
      action.priority = data.priority;
      action.assignedTo = data.assignedTo;
      action.dueDate = data.dueDate;
      action.status = normalized.status;
      action.criticalAssetIds = Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : [];
      // Unification 2026-07-10 : même logique que manageComments (trim + plafond 10 + id/auteur)
      if (appendComment(action, data.newComment, 10) === 'max') {
        showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', 10), 'warning');
      }
            addAuditEntry('edit', 'actions', action.title);
      saveData();
      loadActionsTable();
      updateDashboard();
      loadNonConformitiesTable();
      updateActionsChart();
    }, () => deleteAction(actionId));
    // Shared handler: dynamic swap between searchable-select (control) and native select (other types)
    bindLinkTypeToLinkId(linkType, normalizedValue);
    bindActionStatusProgressSync();
  }
  function deleteAction(actionId) {
    showConfirmModal(t('confirms.deleteAction'), t('confirms.deleteAction'), function() {
      var _act = (appData.actions || []).find(function(a) { return a.id === actionId; });
      appData.actions = (appData.actions || []).filter((a) => a.id !== actionId);
            addAuditEntry('delete', 'actions', _act ? _act.title : actionId);
      saveData();
      loadActionsTable();
      updateDashboard();
      loadNonConformitiesTable();
      updateActionsChart();
    }, { type: 'danger' });
  }
  function addAction(defaultNcIds = []) {
    if (!Array.isArray(defaultNcIds)) defaultNcIds = [];
    const criticalAssetOptions = getCriticalAssetOptions();
    openModal(t('modals.addAction'), [
      { name: "title", label: t('actions.colTitle', 'Titre') },
      { name: "linkType", label: t('actions.linkType', 'Type de lien'), type: "select", value: "control", options: [
          { value: "control", label: t('actionLinkTypes.control', 'Contrôle') },
          { value: "risk", label: t('actionLinkTypes.risk', 'Risque') },
          { value: "nc", label: t('actionLinkTypes.nc', 'Non-conformité') },
          { value: "audit", label: t('actionLinkTypes.audit', 'Audit') },
          { value: "threat", label: t('actionLinkTypes.threat', 'Menace') },
          { value: "continuous", label: t('actionLinkTypes.continuous', 'Amélioration continue') }
        ] },
      { name: "linkId", label: t('actions.linkedElement', 'Élément lié'), type: "select", value: "", options: [] },
      { name: "criticalAssetIds", label: t('fields.criticalAssets'), type: "multiselect", value: [], options: criticalAssetOptions },
      {
        name: "assignedTo",
        label: t('fields.owner'),
        type: "select",
        options: [{ value: '', label: '\u2014 ' + t('common.select', 'Sélectionner') + ' \u2014' }].concat(appData.stakeholders.map((s) => ({ value: s.name, label: s.name })))
      },
      { name: "priority", label: t('actions.priority', 'Priorité'), type: "select", value: "medium", options: ACTION_PRIORITY_OPTIONS },
      { name: "dueDate", label: t('actions.dueDate', 'Échéance'), type: "date" },
      { name: "status", label: t('fields.status'), type: "select", value: "planifie", options: ACTION_STATUS_OPTIONS },
      { name: "progress", label: t('actions.progress', 'Avancement') + " (%)", type: "number", value: "0" },
      { name: "comment", label: t('fields.comment'), type: "textarea" }
    ], (data) => {
      const resolvedLinkType = data.linkType || "control";
      const resolvedLinkValue = normalizeActionLinkValue(resolvedLinkType, data.linkId);
      const normalized = normalizeActionProgressStatus(data.progress, data.status);
      if (!data.title) return false;
      // FIX-ACTIONID-SYNC (audit 2026-06-10) : si le compteur est désynchronisé
      // (ancien état serveur, reset partiel), avancer jusqu'au premier id libre au
      // lieu de réutiliser l'id d'une action existante.
      let id = "action-" + appData.nextActionId;
      appData.nextActionId++;
      while ((appData.actions || []).some(a => a.id === id)) {
        id = "action-" + appData.nextActionId;
        appData.nextActionId++;
      }
      const ncIds = Array.isArray(defaultNcIds) ? defaultNcIds.slice() : [];
      const _newAction = {
        id,
        linkType: resolvedLinkType,
        linkId: resolvedLinkType === "risk" ? resolvedLinkValue : (resolvedLinkValue || ""),
        controlId: resolvedLinkType === "control" ? (resolvedLinkValue || "") : "",
        ncIds,
        title: data.title,
        description: "",
        priority: data.priority || "medium",
        progress: normalized.progress,
        dueDate: data.dueDate || "",
        assignedTo: data.assignedTo || "",
        status: normalized.status,
        criticalAssetIds: Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : [],
        comments: []
      };
      appendComment(_newAction, data.comment, 10); // trim + id/auteur, no-op si vide
      appData.actions.push(_newAction);
            addAuditEntry('add', 'actions', data.title);
      saveData();
      loadActionsTable();
      updateDashboard();
      loadNonConformitiesTable();
      updateActionsChart();
    });
    // Shared handler: dynamic swap between searchable-select (control) and native select (other types)
    bindLinkTypeToLinkId("control", "");
    bindActionStatusProgressSync();
  }

  function manageActionComments(actionId) {
    manageComments('actions', actionId, { titlePrefix: t('actions.manageComments', 'Commentaires de l\'action') });
  }
  window.loadActionsTable = loadActionsTable;
  window.editAction = editAction;
  window.deleteAction = deleteAction;
  window.manageActionComments = manageActionComments;
  window.addAction = addAction;

  // modules/nis2-program.js
  function getNis2StatusMeta() {
    return {
      todo: { label: t('nis2Statuses.todo'), className: "status-badge status-badge--non_implemente" },
      in_progress: { label: t('nis2Statuses.inProgress'), className: "status-badge status-badge--en_cours" },
      suspended: { label: t('nis2Statuses.suspended'), className: "status-badge status-badge--en_cours" },
      done: { label: t('nis2Statuses.done'), className: "status-badge status-badge--verifie" }
    };
  }
  // FIX-I18N-NIS2-ENUMS (audit 2026-07-10) : `let` (et non `const`) pour pouvoir être
  // réévalués dans le handler 'languageChange' — sinon les badges statut/urgence/type du
  // plan NIS2 gardaient la langue de démarrage jusqu'au rechargement complet.
  let NIS2_STATUS_META = getNis2StatusMeta();
  function getNis2UrgencyLabels() {
    return { high: t('nis2Urgency.high'), medium: t('nis2Urgency.medium'), standard: t('nis2Urgency.standard') };
  }
  let NIS2_URGENCY_LABELS = getNis2UrgencyLabels();
  function getNis2ActionTypeOptions() {
    return [
      { value: "service", label: t('nis2ActionTypes.service') },
      { value: "projet", label: t('nis2ActionTypes.projet') },
      { value: "initiative", label: t('nis2ActionTypes.initiative') },
      { value: "programme", label: t('nis2ActionTypes.programme') }
    ];
  }
  let NIS2_ACTION_TYPE_OPTIONS = getNis2ActionTypeOptions();
  function getNis2ActionTypeLabels() {
    return NIS2_ACTION_TYPE_OPTIONS.reduce((acc, opt) => {
      acc[opt.value] = opt.label;
      return acc;
    }, {});
  }
  let NIS2_ACTION_TYPE_LABELS = getNis2ActionTypeLabels();
  const NIS2_DOMAIN_ICONS = {
    [NORMALIZE("Gouvernance & responsabilité")]: "fa-user-shield",
    [NORMALIZE("Gestion des risques (SMSI)")]: "fa-scale-balanced",
    [NORMALIZE("Gestion des incidents & notification")]: "fa-bell",
    [NORMALIZE("Gestion des incidents et notification")]: "fa-bell",
    [NORMALIZE("Continuité d’activité & gestion de crise (BCP/DRP)")]: "fa-life-ring",
    [NORMALIZE("Sécurité de la chaîne d’approvisionnement")]: "fa-truck-fast",
    [NORMALIZE("Sécurité dans l’acquisition, le développement & la maintenance")]: "fa-code-branch",
    [NORMALIZE("Traçabilité et auditabilité")]: "fa-magnifying-glass-chart",
    [NORMALIZE("Gestion des vulnérabilités")]: "fa-bug-slash",
    [NORMALIZE("Sensibilisation & formation")]: "fa-chalkboard-user",
    [NORMALIZE("Cryptographie")]: "fa-lock",
    [NORMALIZE("Sécurité physique")]: "fa-building-lock",
    [NORMALIZE("Gestion des accès au SI")]: "fa-key"
  };
  let nis2PlanViewMode = "detail";
  let nis2PlanShowOpenOnly = false;
  const getSortedNis2Phases = () => {
    const phases = Array.isArray(appData?.nis2Plan?.phases) ? [...appData.nis2Plan.phases] : [...DEFAULT_NIS2_PHASES];
    return phases.sort((a, b) => SortUtils.numeric(a.order, b.order));
  };
  const getNis2PhaseLabel = (phaseId) => {
    if (!phaseId) return t('nis2Phases.outOfPhase');
    const found = getSortedNis2Phases().find((p) => p.id === phaseId);
    return found ? found.label : `Phase ${phaseId}`;
  };
  const ensureNis2Phases = () => {
    if (!appData.nis2Plan) appData.nis2Plan = {};
    if (!Array.isArray(appData.nis2Plan.phases) || appData.nis2Plan.phases.length === 0) {
      appData.nis2Plan.phases = [...DEFAULT_NIS2_PHASES];
    }
    const map = new Map();
    appData.nis2Plan.phases.forEach((p) => {
      const id = p.id || generateId("phase");
      if (!map.has(id)) map.set(id, { id, label: p.label || id, order: typeof p.order === "number" ? p.order : 99, fallback: !!p.fallback });
    });
    const hasFallback = [...map.values()].some((p) => p.fallback);
    if (!hasFallback) {
      map.set("phase-unplanned", { id: "phase-unplanned", label: t('nis2Phases.outOfPhase', 'Hors phase'), order: 99, fallback: true });
    }
    appData.nis2Plan.phases = [...map.values()].sort((a, b) => SortUtils.numeric(a.order, b.order, 0));
    return appData.nis2Plan.phases;
  };
  function normalizePhaseId(value) {
    const phases = ensureNis2Phases();
    const fallback = phases.find((p) => p.fallback) || phases[phases.length - 1];
    const str = value === undefined || value === null ? "" : value.toString();
    if (phases.some((p) => p.id === str)) return str;
    const asNum = parseInt(str, 10);
    if (!isNaN(asNum)) {
      const numericMatch = phases.find((p) => parseInt(p.order, 10) === asNum || p.id === `phase-${asNum}`);
      if (numericMatch) return numericMatch.id;
    }
    return fallback?.id || "phase-unplanned";
  }
  function parseBudgetValue(value) {
    if (typeof value === "number") return value;
    if (!value || typeof value !== "string") return NaN;
    // Tenter d'abord une interprétation numérique PROPRE (gère la notation scientifique
    // « 1e9 », les décimales…) avant le nettoyage agressif des caractères, qui sinon
    // transformerait « 1e9 » en « 19 ».
    const cleaned = value.replace(/\s/g, "").replace(",", ".");
    const direct = Number(cleaned);
    if (Number.isFinite(direct)) return direct;
    const sanitized = cleaned.replace(/[^0-9.\-]/g, "");
    const parsed = parseFloat(sanitized);
    return isNaN(parsed) ? NaN : parsed;
  }
  const formatCurrency = (val) => {
    if (!Number.isFinite(val)) return "—";
    try {
      // Montants budgétaires : toujours en clair, PAS de notation compacte — préserver la
      // précision (1 234 567 € et non « 1,2 M€ »), indispensable pour un suivi de budget.
      return new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 0 }).format(val) + " €";
    } catch (e) {
      const locale = window.CSI_i18n ? { fr: 'fr-FR', en: 'en-GB' }[window.CSI_i18n.getLanguage()] || 'fr-FR' : 'fr-FR';
      return val.toLocaleString(locale) + " €";
    }
  };
  const formatBudgetValue = (val) => {
    const num = parseBudgetValue(val);
    return Number.isFinite(num) ? formatCurrency(num) : "—";
  };
  const getActionBudgetTotal = (action = {}) => {
    const yearlyKeys = ["budget2026", "budget2027", "budget2028"];
    let total = 0;
    let hasValue = false;
    yearlyKeys.forEach((k) => {
      const num = parseBudgetValue(action[k]);
      if (Number.isFinite(num)) {
        total += num;
        hasValue = true;
      }
    });
    if (!hasValue) {
      const legacy = parseBudgetValue(action.budget);
      if (Number.isFinite(legacy)) return legacy;
      return NaN;
    }
    return total;
  };
  function ensureNis2PlanToolbar() {
    const container = document.getElementById("nis2PlanContainer");
    if (!container) return;
    const infoBox = container.previousElementSibling?.classList?.contains("info-box") ? container.previousElementSibling : null;
    const anchor = infoBox || container;
    let toolbar = document.getElementById("nis2PlanToolbar");
    if (!toolbar) {
      toolbar = document.createElement("div");
      toolbar.id = "nis2PlanToolbar";
      // Toolbar v2 : langage visuel commun — carte .section-toolbar, une rangée,
      // chips existants conservés (comportement inchangé).
      toolbar.className = "nis2-plan-toolbar section-toolbar";
      toolbar.innerHTML = `
        <div class="section-toolbar__row">
          <div class="nis2-plan-toolbar__group">
            <button class="chip-btn" type="button" data-view-mode="detail">Vue détaillée</button>
            <button class="chip-btn" type="button" data-view-mode="executive">Vue exécutive</button>
          </div>
          <div class="nis2-plan-toolbar__group section-toolbar__end">
            <button class="chip-btn" type="button" id="nis2PlanFocusBtn" data-focus="open">Mettre en avant les actions ouvertes</button>
          </div>
        </div>
      `;
      anchor.insertAdjacentElement("afterend", toolbar);
    }
    if (!toolbar.dataset.bound) {
      toolbar.dataset.bound = "true";
      toolbar.addEventListener("click", (event) => {
        const viewBtn = event.target.closest("[data-view-mode]");
        if (viewBtn) {
          nis2PlanViewMode = viewBtn.dataset.viewMode || "detail";
          ensureNis2PlanToolbar();
          return renderNis2Plan();
        }
        const focusBtn = event.target.closest("#nis2PlanFocusBtn");
        if (focusBtn) {
          nis2PlanShowOpenOnly = !nis2PlanShowOpenOnly;
          ensureNis2PlanToolbar();
          return renderNis2Plan();
        }
      });
    }
    toolbar.querySelectorAll("[data-view-mode]").forEach((btn) => {
      const isActive = btn.dataset.viewMode === nis2PlanViewMode;
      btn.classList.toggle("is-active", isActive);
      btn.setAttribute("aria-pressed", isActive ? "true" : "false");
    });
    const focusBtn = toolbar.querySelector("#nis2PlanFocusBtn");
    if (focusBtn) {
      focusBtn.classList.toggle("is-active", !!nis2PlanShowOpenOnly);
      focusBtn.setAttribute("aria-pressed", nis2PlanShowOpenOnly ? "true" : "false");
    }
  }
  function refreshNis2PhaseFilter() {
    const select = document.getElementById("nis2PhaseFilter");
    if (!select) return;
    const current = select.value || "all";
    const phases = getSortedNis2Phases();
    select.innerHTML = `<option value="all">Toutes les phases</option>` + phases.map((p) => `<option value="${p.id}">${escapeHtml(p.label)}</option>`).join("");
    const exists = phases.some((p) => p.id === current);
    select.value = exists ? current : "all";
  }
  function renderNis2PhasesPanel() {
    if (isReadOnlyMode && typeof isReadOnlyMode === "function" && isReadOnlyMode()) return;
    ensureNis2Phases();
    const container = document.getElementById("nis2PlanContainer");
    if (!container) return;
    const anchor = document.getElementById("nis2PlanToolbar") || container;
    let panel = document.getElementById("nis2PhasesPanel");
    if (!panel) {
      panel = document.createElement("div");
      panel.id = "nis2PhasesPanel";
      panel.className = "nis2-phases-panel";
      anchor.insertAdjacentElement("afterend", panel);
    }
    const phases = getSortedNis2Phases();
    const chips = phases.map((p) => `
      <div class="phase-chip" data-phase-id="${escapeHtml(p.id)}">
        <span class="phase-chip__label">${escapeHtml(p.label)}</span>
        <span class="phase-chip__order">#${p.order || 0}</span>
        <div class="phase-chip__actions">
          <button class="icon-btn" type="button" data-phase-action="edit" data-phase-id="${escapeHtml(p.id)}" title="${t('nis2Plan.editPhase', 'Modifier la phase')}"><i class="fas fa-pen"></i></button>
          ${p.fallback ? "" : `<button class="icon-btn icon-btn--danger" type="button" data-phase-action="delete" data-phase-id="${escapeHtml(p.id)}" title="${t('nis2Plan.deletePhase', 'Supprimer la phase')}"><i class="fas fa-trash"></i></button>`}
        </div>
      </div>
    `).join("");
    panel.innerHTML = `
      <div class="nis2-phases-panel__header">
        <div class="nis2-phases-panel__title"><i class="fas fa-timeline"></i> ${t('nis2Plan.planPhases', 'Phases du plan')}</div>
        <button class="btn btn--outline btn--sm" type="button" data-phase-action="add"><i class="fas fa-plus"></i> ${t('nis2Plan.addPhase', 'Ajouter une phase')}</button>
      </div>
      <div class="nis2-phases-panel__list">${chips}</div>
    `;
    if (!panel.dataset.bound) {
      panel.dataset.bound = "true";
      panel.addEventListener("click", (event) => {
        const addBtn = event.target.closest("[data-phase-action=\"add\"]");
        if (addBtn) return openNis2PhaseModal();
        const editBtn = event.target.closest("[data-phase-action=\"edit\"]");
        if (editBtn) return openNis2PhaseModal(editBtn.dataset.phaseId);
        const delBtn = event.target.closest("[data-phase-action=\"delete\"]");
        if (delBtn) return deleteNis2Phase(delBtn.dataset.phaseId);
      });
    }
  }
  function renderSocleGrid(containerId, limit = null) {
    const container = document.getElementById(containerId);
    if (!container) return;
    const pillars = normalizeSoclePillars(appData.soclePillars);
    appData.soclePillars = pillars;
    const items = typeof limit === "number" ? pillars.slice(0, limit) : pillars;
    container.innerHTML = "";
    items.forEach((pillar, idx) => {
      const pillarIndex = idx;
      const card = document.createElement("div");
      card.className = `socle-card socle-card--pillar${pillarIndex + 1}`;
      const listItems = (pillar.items || []).map((item) => `<li>${escapeHtml(item)}</li>`).join("");
      card.innerHTML = `
        <div class="socle-card__header">
          <div class="socle-card__icon"><i class="fas ${escapeHtml(pillar.icon || "")}"></i></div>
          <div class="socle-card__meta">
            <p class="socle-card__eyebrow">Pilier ${pillarIndex + 1}</p>
            <h3 class="socle-card__title">${escapeHtml(pillar.title)}</h3>
            <p class="socle-card__subtitle">${escapeHtml(pillar.subtitle)}</p>
          </div>
          <button class="icon-btn socle-card__edit" type="button" data-readonly-lock="true" data-pillar-index="${pillarIndex}" title="${t('nis2Socle.editPillar', 'Modifier le pilier')}">
            <i class="fas fa-pen"></i>
          </button>
        </div>
        <div class="socle-card__body">
          <h4>Éléments clefs</h4>
          <ul>${listItems}</ul>
          <div class="socle-card__footer"><i class="fas fa-user-tie"></i> ${escapeHtml(pillar.owner)}</div>
        </div>
      `;
      container.appendChild(card);
    });
    container.querySelectorAll(".socle-card__edit").forEach((btn) => {
      btn.addEventListener("click", () => {
        const index = parseInt(btn.dataset.pillarIndex, 10);
        if (Number.isInteger(index)) openSoclePillarEditor(index);
      });
    });
  }
  function renderSocleGrids() {
    renderSocleGrid("nis2SocleInline", 3);
    renderSocleGrid("nis2SocleFull");
    refreshReadOnlyLocks();
  }
  function loadNis2Program() {
    ensureNis2Plan();
    renderBudgetTab();
  }

  // ===================== Budget Cybersécurité (onglet générique) =====================
  // Onglet de pilotage budgétaire décorrélé de NIS2 : lignes budgétaires autonomes
  // (intitulé, catégorie, responsable, statut, montant par année configurable).
  function makeBudgetLineId() {
    return 'bl-' + Date.now().toString(36) + '-' + Math.floor(Math.random() * 1e6).toString(36);
  }
  function getBudgetYears() {
    const p = appData && appData.nis2Plan;
    let yrs = (p && Array.isArray(p.budgetYears)) ? p.budgetYears.slice() : null;
    if (!yrs || !yrs.length) yrs = defaultBudgetYears();
    return yrs;
  }
  function getBudgetLines() {
    const p = appData && appData.nis2Plan;
    return (p && Array.isArray(p.budgetLines)) ? p.budgetLines : [];
  }
  function getBudgetCategoriesList() {
    const p = appData && appData.nis2Plan;
    const cats = (p && Array.isArray(p.budgetCategories) && p.budgetCategories.length)
      ? p.budgetCategories
      : [
          { value: "infra", label: t('budgetCategories.infra', 'Infrastructure') },
          { value: "cyber", label: t('budgetCategories.cyber', 'Cybersécurité') },
          { value: "divers", label: t('budgetCategories.divers', 'Divers') }
        ];
    return cats.map(c => ({ value: c.value || c.label || '', label: c.label || c.value || '' }));
  }
  function budgetCategoryLabel(value) {
    const cat = getBudgetCategoriesList().find(c => c.value === value);
    return cat ? cat.label : (value || t('budgetCategories.divers', 'Divers'));
  }
  function budgetStatusMeta(status) {
    const s = normalizeBudgetStatus(status);
    if (s === 'engage') return { value: 'engage', label: t('budgetStatus.engage', 'Engagé'), className: 'budget-status--engage' };
    if (s === 'realise') return { value: 'realise', label: t('budgetStatus.realise', 'Réalisé'), className: 'budget-status--realise' };
    return { value: 'planifie', label: t('budgetStatus.planifie', 'Planifié'), className: 'budget-status--planifie' };
  }
  function getBudgetLineYearAmount(line, year) {
    if (!line || !line.amounts) return 0;
    const n = parseBudgetValue(line.amounts[year]);
    // (audit global 2026-07-03) borner sur l'invariant de saisie (>= 0) — un montant négatif
    // injecté (import/donnée corrompue) faussait sinon les totaux budgétaires au rendu.
    return (Number.isFinite(n) && n >= 0) ? n : 0;
  }
  function getBudgetLineTotal(line) {
    return getBudgetYears().reduce((sum, yr) => sum + getBudgetLineYearAmount(line, yr), 0);
  }

  function renderBudgetTab() {
    ensureNis2Plan();
    populateBudgetFilters();
    renderBudgetKpis();
    renderBudgetByYearChart();
    renderBudgetByCategoryChart();
    renderBudgetLinesTable();
    if (typeof refreshReadOnlyLocks === 'function') refreshReadOnlyLocks();
  }

  function getBudgetFilterState() {
    const catSel = document.getElementById('budgetCategoryFilter');
    const statusSel = document.getElementById('budgetStatusFilter');
    return {
      category: (catSel && catSel.value) || 'all',
      status: (statusSel && statusSel.value) || 'all'
    };
  }
  function getFilteredBudgetLines() {
    const { category, status } = getBudgetFilterState();
    return getBudgetLines().filter(line => {
      if (category !== 'all' && (line.category || 'divers') !== category) return false;
      if (status !== 'all' && normalizeBudgetStatus(line.status) !== status) return false;
      return true;
    });
  }
  function populateBudgetFilters() {
    const catSel = document.getElementById('budgetCategoryFilter');
    if (catSel) {
      const current = catSel.value || 'all';
      const cats = getBudgetCategoriesList();
      const opts = ['<option value="all">' + escapeHtml(t('nis2Program.allCategories', 'Toutes les catégories')) + '</option>']
        .concat(cats.map(c => '<option value="' + escapeHtml(c.value) + '">' + escapeHtml(c.label) + '</option>'));
      catSel.innerHTML = opts.join('');
      catSel.value = Array.prototype.some.call(catSel.options, o => o.value === current) ? current : 'all';
    }
    const tb = document.getElementById('budgetToolbar');
    if (tb && !tb.dataset.bound) {
      tb.dataset.bound = 'true';
      ['budgetCategoryFilter', 'budgetStatusFilter'].forEach(id => {
        const el = document.getElementById(id);
        if (el) el.addEventListener('change', () => renderBudgetLinesTable());
      });
    }
  }

  function renderBudgetKpis() {
    const grid = document.getElementById('budgetKpiGrid');
    if (!grid) return;
    const years = getBudgetYears();
    const lines = getBudgetLines();
    const grandTotal = lines.reduce((sum, l) => sum + getBudgetLineTotal(l), 0);
    const yearTotals = years.map(yr => lines.reduce((sum, l) => sum + getBudgetLineYearAmount(l, yr), 0));
    const statusTotals = { planifie: 0, engage: 0, realise: 0 };
    lines.forEach(l => { statusTotals[normalizeBudgetStatus(l.status)] += getBudgetLineTotal(l); });
    const yearColors = ['kpi-card--success', 'kpi-card--warning', 'kpi-card--info', 'kpi-card--purple', 'kpi-card--cyan', 'kpi-card--orange'];
    const lineWord = lines.length > 1 ? t('nis2Program.linesPlural', 'lignes') : t('nis2Program.lineSingular', 'ligne');
    let html = `
      <div class="kpi-card kpi-card--primary budget-kpi-card">
        <div class="kpi-card__icon"><i class="fas fa-sack-dollar"></i></div>
        <div class="kpi-card__content">
          <p class="kpi-card__eyebrow">${escapeHtml(t('nis2Program.totalBudget', 'Budget total'))}</p>
          <div class="kpi-card__value">${formatCurrency(grandTotal)}</div>
          <div class="kpi-card__detail">${lines.length} ${escapeHtml(lineWord)}</div>
        </div>
      </div>`;
    years.forEach((yr, idx) => {
      const pct = grandTotal ? Math.round(yearTotals[idx] / grandTotal * 100) : 0;
      html += `
      <div class="kpi-card ${yearColors[idx % yearColors.length]} budget-kpi-card">
        <div class="kpi-card__icon"><i class="fas fa-calendar-alt"></i></div>
        <div class="kpi-card__content">
          <p class="kpi-card__eyebrow">${escapeHtml(yr)}</p>
          <div class="kpi-card__value">${formatCurrency(yearTotals[idx])}</div>
          <div class="kpi-card__detail">${pct}%</div>
        </div>
      </div>`;
    });
    const chips = ['planifie', 'engage', 'realise'].map(budgetStatusMeta);
    html += `
      <div class="budget-status-chips">
        ${chips.map(m => `<span class="budget-status-chip ${m.className}"><span class="budget-status-chip__dot"></span>${escapeHtml(m.label)} <strong>${formatCurrency(statusTotals[m.value])}</strong></span>`).join('')}
      </div>`;
    grid.innerHTML = html;
  }

  function renderBudgetByYearChart() {
    const canvas = document.getElementById('budgetByYearChart');
    if (!canvas || typeof Chart === 'undefined') return;
    if (charts.budgetByYear) charts.budgetByYear.destroy();
    const themeVars = getThemeVars();
    const years = getBudgetYears();
    const lines = getBudgetLines();
    const statuses = ['planifie', 'engage', 'realise'];
    const statusColors = { planifie: themeVars.secondary, engage: themeVars.warning, realise: themeVars.success };
    const datasets = statuses.map(s => ({
      label: budgetStatusMeta(s).label,
      data: years.map(yr => lines.filter(l => normalizeBudgetStatus(l.status) === s).reduce((sum, l) => sum + getBudgetLineYearAmount(l, yr), 0)),
      backgroundColor: withAlpha(statusColors[s], 0.85),
      borderColor: withAlpha(statusColors[s], 1),
      borderWidth: 1,
      borderRadius: 4
    }));
    charts.budgetByYear = new Chart(canvas, {
      type: 'bar',
      data: { labels: years, datasets },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          x: { stacked: true, grid: { display: false }, ticks: { color: themeVars.textSecondary } },
          y: { stacked: true, beginAtZero: true, ticks: { color: themeVars.textSecondary, callback: (v) => formatCurrency(v) }, grid: { color: withAlpha(themeVars.borderColor, 0.6) } }
        },
        plugins: {
          legend: { position: 'bottom', labels: { usePointStyle: true, padding: 14, color: themeVars.textColor } },
          tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${formatCurrency(ctx.raw || 0)}` } }
        }
      }
    });
  }

  function renderBudgetByCategoryChart() {
    const canvas = document.getElementById('budgetByCategoryChart');
    const legendEl = document.getElementById('budgetByCategoryLegend');
    if (!canvas || typeof Chart === 'undefined') return;
    if (charts.budgetByCategory) { charts.budgetByCategory.destroy(); charts.budgetByCategory = null; }
    const themeVars = getThemeVars();
    const totalsByCat = {};
    getBudgetCategoriesList().forEach(c => { totalsByCat[c.value] = 0; });
    getBudgetLines().forEach(l => {
      const cat = l.category || 'divers';
      if (!(cat in totalsByCat)) totalsByCat[cat] = 0;
      totalsByCat[cat] += getBudgetLineTotal(l);
    });
    const entries = Object.keys(totalsByCat).filter(k => totalsByCat[k] > 0);
    const data = entries.map(k => totalsByCat[k]);
    const total = data.reduce((a, b) => a + b, 0);
    if (!data.length) {
      const ctx2 = canvas.getContext('2d');
      if (ctx2) ctx2.clearRect(0, 0, canvas.width, canvas.height);
      if (legendEl) legendEl.innerHTML = `<p class="budget-empty-hint">${escapeHtml(t('nis2Program.noBudgetData', 'Aucune donnée budgétaire à afficher'))}</p>`;
      return;
    }
    const palette = [themeVars.primary, themeVars.secondary, themeVars.warning, themeVars.success, themeVars.info, themeVars.danger];
    const colors = entries.map((_, i) => withAlpha(palette[i % palette.length], 0.85));
    const borderColors = entries.map((_, i) => withAlpha(palette[i % palette.length], 1));
    const labels = entries.map(budgetCategoryLabel);
    charts.budgetByCategory = new Chart(canvas, {
      type: 'doughnut',
      data: { labels, datasets: [{ data, backgroundColor: colors, borderColor: borderColors, borderWidth: 2, hoverOffset: 10 }] },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: '58%',
        plugins: {
          legend: { display: false },
          tooltip: { callbacks: { label: (ctx) => { const v = ctx.raw || 0; const pct = total ? Math.round(v / total * 100) : 0; return `${ctx.label}: ${formatCurrency(v)} (${pct}%)`; } } }
        }
      }
    });
    if (legendEl) {
      legendEl.innerHTML = entries.map((k, i) => `<span class="budget-legend__item"><span class="budget-legend__dot" style="background:${borderColors[i]}"></span>${escapeHtml(budgetCategoryLabel(k))} <strong>${formatCurrency(data[i])}</strong></span>`).join('');
    }
  }

  function renderBudgetLinesTable() {
    const container = document.getElementById('budgetLinesContainer');
    if (!container) return;
    const years = getBudgetYears();
    const allLines = getBudgetLines();
    const lines = getFilteredBudgetLines();
    const readonly = isReadOnlyMode && typeof isReadOnlyMode === 'function' ? isReadOnlyMode() : false;
    if (!allLines.length) {
      container.innerHTML = `<div class="budget-empty">
        <i class="fas fa-coins"></i>
        <p class="budget-empty__title">${escapeHtml(t('nis2Program.budgetEmptyTitle', 'Aucune ligne budgétaire'))}</p>
        <p class="budget-empty__hint">${escapeHtml(t('nis2Program.budgetEmptyHint', 'Ajoutez votre première ligne de budget cybersécurité avec le bouton « Nouvelle ligne ».'))}</p>
      </div>`;
      return;
    }
    const cats = getBudgetCategoriesList();
    const order = {};
    cats.forEach((c, i) => { order[c.value] = i; });
    const groups = {};
    lines.forEach(l => { const c = l.category || 'divers'; (groups[c] = groups[c] || []).push(l); });
    const groupKeys = Object.keys(groups).sort((a, b) => (order[a] != null ? order[a] : 999) - (order[b] != null ? order[b] : 999));
    const colspanLead = 3;
    const yearHeaders = years.map(yr => `<th class="text-right">${escapeHtml(yr)}</th>`).join('');
    const grandYearTotals = years.map(() => 0);
    let grandTotal = 0;
    let rowsHtml = '';
    groupKeys.forEach(catKey => {
      const groupLines = groups[catKey];
      const groupYearTotals = years.map(yr => groupLines.reduce((s, l) => s + getBudgetLineYearAmount(l, yr), 0));
      const groupTotal = groupLines.reduce((s, l) => s + getBudgetLineTotal(l), 0);
      groupYearTotals.forEach((v, i) => { grandYearTotals[i] += v; });
      grandTotal += groupTotal;
      rowsHtml += `<tr class="budget-group-row"><td colspan="${colspanLead}">${escapeHtml(budgetCategoryLabel(catKey))}</td>${groupYearTotals.map(v => `<td class="text-right">${formatCurrency(v)}</td>`).join('')}<td class="text-right">${formatCurrency(groupTotal)}</td>${readonly ? '' : '<td></td>'}</tr>`;
      groupLines.forEach(line => {
        const meta = budgetStatusMeta(line.status);
        // Lot 1b : ligne cliquable (ouvre la modale d'édition) sauf en lecture seule.
        rowsHtml += `<tr${readonly ? '' : ` class="budget-line-row" data-budget-row-id="${escapeHtml(line.id)}" style="cursor:pointer"`}>
          <td><div class="budget-line-label">${escapeHtml(line.label || t('nis2Program.untitledLine', 'Sans intitulé'))}</div></td>
          <td>${line.owner ? escapeHtml(line.owner) : '—'}</td>
          <td><span class="budget-status ${meta.className}">${escapeHtml(meta.label)}</span></td>
          ${years.map(yr => `<td class="text-right">${formatCurrency(getBudgetLineYearAmount(line, yr))}</td>`).join('')}
          <td class="text-right"><strong>${formatCurrency(getBudgetLineTotal(line))}</strong></td>
          ${readonly ? '' : `<td class="text-right budget-line-actions">
            <button class="icon-btn icon-btn--danger" type="button" data-budget-action="delete" data-line-id="${escapeHtml(line.id)}" title="${escapeHtml(t('common.delete', 'Supprimer'))}"><i class="fas fa-trash"></i></button>
          </td>`}
        </tr>`;
      });
    });
    const filtered = lines.length !== allLines.length;
    if (!rowsHtml && filtered) {
      // Des lignes existent mais aucune ne passe les filtres : message explicite.
      const totalCols = colspanLead + years.length + 1 + (readonly ? 0 : 1);
      rowsHtml = `<tr class="budget-no-match"><td colspan="${totalCols}">${escapeHtml(t('nis2Program.noFilterMatch', 'Aucune ligne ne correspond aux filtres sélectionnés.'))}</td></tr>`;
    }
    container.innerHTML = `
      <table class="table budget-line-table">
        <thead>
          <tr>
            <th>${escapeHtml(t('nis2Program.lineLabel', 'Intitulé'))}</th>
            <th>${escapeHtml(t('nis2Program.lineOwner', 'Responsable'))}</th>
            <th>${escapeHtml(t('nis2Program.lineStatus', 'Statut'))}</th>
            ${yearHeaders}
            <th class="text-right">${escapeHtml(t('nis2Program.lineTotal', 'Total'))}</th>
            ${readonly ? '' : `<th class="text-right">${escapeHtml(t('common.actions', 'Actions'))}</th>`}
          </tr>
        </thead>
        <tbody>${rowsHtml}</tbody>
        <tfoot>
          <tr class="budget-total-row">
            <td colspan="${colspanLead}">${escapeHtml(filtered ? t('nis2Program.filteredTotal', 'Total (filtré)') : t('nis2Program.grandTotal', 'Total général'))}</td>
            ${grandYearTotals.map(v => `<td class="text-right">${formatCurrency(v)}</td>`).join('')}
            <td class="text-right"><strong>${formatCurrency(grandTotal)}</strong></td>
            ${readonly ? '' : '<td></td>'}
          </tr>
        </tfoot>
      </table>`;
    if (!container.dataset.bound) {
      container.dataset.bound = 'true';
      container.addEventListener('click', (event) => {
        const btn = event.target.closest('[data-budget-action]');
        if (btn) {
          const id = btn.getAttribute('data-line-id');
          const act = btn.getAttribute('data-budget-action');
          if (act === 'delete') deleteBudgetLine(id);
          return;
        }
        // Lot 1b : clic-ligne = édition (l'attribut n'est posé qu'en mode édition).
        const rowEl = event.target.closest('tr[data-budget-row-id]');
        if (rowEl) openBudgetLineModal(rowEl.getAttribute('data-budget-row-id'));
      });
    }
  }

  function openBudgetLineModal(lineId) {
    if (!ensureEditMode(t('editMode.editBudgetLine', 'éditer une ligne budgétaire'))) return;
    ensureNis2Plan();
    const lines = getBudgetLines();
    const existing = lineId ? lines.find(l => l.id === lineId) : null;
    const years = getBudgetYears();
    const cats = getBudgetCategoriesList();
    const fields = [
      { name: 'label', label: t('nis2Program.lineLabel', 'Intitulé'), value: existing ? existing.label : '' },
      { name: 'category', label: t('nis2Program.lineCategory', 'Catégorie'), type: 'select', value: existing ? (existing.category || 'divers') : (cats[0] ? cats[0].value : 'divers'), options: cats },
      { name: 'owner', label: t('nis2Program.lineOwner', 'Responsable'), value: existing ? (existing.owner || '') : '', placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(existing ? existing.owner : ''), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: 'status', label: t('nis2Program.lineStatus', 'Statut'), type: 'select', value: existing ? normalizeBudgetStatus(existing.status) : 'planifie', options: [
        { value: 'planifie', label: t('budgetStatus.planifie', 'Planifié') },
        { value: 'engage', label: t('budgetStatus.engage', 'Engagé') },
        { value: 'realise', label: t('budgetStatus.realise', 'Réalisé') }
      ] }
    ];
    years.forEach(yr => {
      fields.push({ name: 'amount_' + yr, label: t('nis2Program.amountForYear', 'Montant') + ' ' + yr + ' (€)', type: 'number', min: 0, value: existing ? getBudgetLineYearAmount(existing, yr) : 0 });
    });
    const title = existing ? t('modals.editBudgetLine', 'Modifier la ligne budgétaire') : t('modals.addBudgetLine', 'Nouvelle ligne budgétaire');
    openModal(title, fields, (data) => {
      const label = (data.label || '').trim();
      if (!label) { showToast(t('toasts.budgetLineLabelRequired', 'Un intitulé est requis'), 'warning'); return false; }
      const amounts = {};
      years.forEach(yr => {
        const n = parseBudgetValue(data['amount_' + yr]);
        // Montants bornés à >= 0 (un budget négatif n'a pas de sens et casserait le donut).
        amounts[yr] = (Number.isFinite(n) && n > 0) ? n : 0;
      });
      const clean = {
        id: existing ? existing.id : makeBudgetLineId(),
        label,
        category: data.category || 'divers',
        owner: (data.owner || '').trim(),
        status: normalizeBudgetStatus(data.status),
        amounts
      };
      if (existing) {
        const idx = lines.findIndex(l => l.id === existing.id);
        if (idx >= 0) lines[idx] = clean; else lines.push(clean);
      } else {
        lines.push(clean);
      }
      appData.nis2Plan.budgetLines = lines;
      saveData();
      renderBudgetTab();
      showToast(t('toasts.budgetLineSaved', 'Ligne budgétaire enregistrée'), 'success');
      return true;
    }, existing ? () => deleteBudgetLine(existing.id) : null);
  }

  function deleteBudgetLine(lineId) {
    if (!ensureEditMode(t('editMode.deleteBudgetLine', 'supprimer une ligne budgétaire'))) return;
    ensureNis2Plan();
    const lines = getBudgetLines();
    const idx = lines.findIndex(l => l.id === lineId);
    if (idx < 0) return;
    showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.deleteBudgetLine', 'Supprimer cette ligne budgétaire ?'), function() {
      lines.splice(idx, 1);
      appData.nis2Plan.budgetLines = lines;
      saveData();
      renderBudgetTab();
      showToast(t('toasts.budgetLineDeleted', 'Ligne budgétaire supprimée'), 'success');
    }, { type: 'danger' });
  }

  function remapBudgetLineYears(oldYears, newYears) {
    const positional = oldYears.length === newYears.length;
    getBudgetLines().forEach(line => {
      const old = line.amounts || {};
      const next = {};
      newYears.forEach((yr, i) => {
        let raw;
        if (positional) {
          // Même nombre d'années = renommage / décalage de fenêtre : mapping PUREMENT
          // positionnel depuis l'instantané `old`. Tester d'abord hasOwnProperty(yr)
          // corromprait un décalage (2026/27/28 → 2027/28/29) : le libellé « 2027 »
          // chevauchant lirait l'ancienne valeur de 2027 au lieu de celle de 2026.
          raw = old[oldYears[i]];
        } else if (Object.prototype.hasOwnProperty.call(old, yr)) {
          // Longueurs différentes = ajout / retrait : conservation par libellé d'année.
          raw = old[yr];
        }
        const v = parseBudgetValue(raw);
        next[yr] = Number.isFinite(v) ? v : 0;
      });
      line.amounts = next;
    });
  }

  function openBudgetYearsModal() {
    if (!ensureEditMode(t('editMode.manageBudgetYears', 'gérer les années budgétaires'))) return;
    ensureNis2Plan();
    const years = getBudgetYears();
    openModal(t('modals.manageYears', 'Gérer les années budgétaires'), [
      { name: 'years', label: t('fields.budgetYearsList', 'Une année par ligne (min. 1)'), type: 'textarea', value: years.join('\n'), help: t('fields.budgetYearsHelp', 'Ajoutez, retirez ou renommez des années (une par ligne). Les montants restent associés au libellé de chaque année ; un renommage à même position conserve la valeur.') }
    ], (data) => {
      let newYears = (data.years || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
      newYears = newYears.filter((y, i) => newYears.indexOf(y) === i);
      if (!newYears.length) { showToast(t('toasts.budgetYearMin', 'Au moins une année requise'), 'warning'); return false; }
      const oldYears = getBudgetYears();
      remapBudgetLineYears(oldYears, newYears);
      appData.nis2Plan.budgetYears = newYears;
      saveData();
      renderBudgetTab();
      showToast(t('toasts.budgetYearsUpdated', 'Années budgétaires mises à jour'), 'success');
      return true;
    });
  }
  function openSoclePillarEditor(index) {
    if (!ensureEditMode("edit a pillar")) {
      refreshReadOnlyLocks();
      return;
    }
    const pillars = normalizeSoclePillars(appData.soclePillars);
    appData.soclePillars = pillars;
    const pillar = pillars[index];
    if (!pillar) return;
    openModal(t('modals.editPillar', 'Modifier le pilier') + ' ' + (index + 1), [
      { name: "title", label: t('fields.title'), value: pillar.title },
      { name: "subtitle", label: t('fields.subtitle', 'Sous-titre'), value: pillar.subtitle },
      { name: "items", label: t('fields.keyItems', 'Éléments clefs (un par ligne)'), type: "textarea", value: (pillar.items || []).join("\n") },
      { name: "owner", label: t('fields.projectLead', 'Chef de projet'), value: pillar.owner, placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(pillar.owner), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') }
    ], (data) => {
      if (!data.title) return false;
      pillar.title = data.title;
      pillar.subtitle = data.subtitle || "";
      pillar.items = data.items
        ? data.items.split(/\r?\n/).map((item) => item.trim()).filter(Boolean)
        : [];
      pillar.owner = data.owner || "";
      saveData();
      renderSocleGrids();
    });
  }
  function renderNis2ProgramRadar() {
    const canvas = document.getElementById("nis2ProgramRadar");
    if (!canvas) return;
    if (charts.nis2ProgramRadar) charts.nis2ProgramRadar.destroy();
    const { textColor, textSecondary, borderColor, secondary, warning } = getThemeVars();
    const categories = CYFUN_CATEGORIES.length > 0 ? CYFUN_CATEGORIES : Object.keys(appData.targetMaturity?.nis2 || {});
    if (categories.length === 0) return;

    const labels = categories.map((k) => {
      const full = CATEGORY_LABELS[k] || k;
      return full.split(" (")[0].trim();
    });

    // Compute per-category doc and impl averages from controls via FrameworkMapper
    var FM = window.CSI_FrameworkMapper;
    var catMaturity = FM ? FM.getCategoryMaturity('cyfun2025') : null;
    const docData = [];
    const implData = [];
    if (catMaturity) {
      categories.forEach(function(catId) {
        var cm = catMaturity[catId];
        docData.push(cm ? cm.docAvg : 0);
        implData.push(cm ? cm.implAvg : 0);
      });
    } else {
      // Fallback: manual computation
      var nis2Controls = appData.nis2Controls || [];
      categories.forEach(function(catId) {
        var controls = nis2Controls.filter(function(c) { return c.categoryId === catId; });
        var docSum = 0, implSum = 0, count = 0;
        controls.forEach(function(c) {
          var d = c.docMaturity || 0;
          var i = c.implMaturity || 0;
          if (d > 0 || i > 0) { docSum += d; implSum += i; count++; }
        });
        docData.push(count > 0 ? +(docSum / count).toFixed(2) : 0);
        implData.push(count > 0 ? +(implSum / count).toFixed(2) : 0);
      });
    }

    const globalTarget = Number(appData.targetMaturity?.nis2Target) || 0;
    const globalData = categories.map(() => globalTarget);
    const renderLegend = typeof renderRadarLegend === "function" ? renderRadarLegend : () => {};
    const fontSize = categories.length > 20 ? 11 : 14;

    charts.nis2ProgramRadar = new Chart(canvas, {
      type: "radar",
      data: {
        labels,
        datasets: [
          {
            label: t('chartLabels.globalTarget', 'Cible globale'),
            data: globalData,
            backgroundColor: withAlpha(warning, 0.08),
            borderColor: withAlpha(warning, 0.55),
            borderWidth: 3,
            pointRadius: 2,
            borderDash: [4, 4],
            fill: true
          },
          {
            label: t('chartLabels.docMaturity', 'Documentation'),
            data: docData,
            backgroundColor: withAlpha('#3b82f6', 0.12),
            borderColor: withAlpha('#3b82f6', 0.85),
            borderWidth: 2,
            pointRadius: 3,
            pointBackgroundColor: withAlpha('#3b82f6', 1),
            fill: true
          },
          {
            label: t('chartLabels.implMaturity', 'Implémentation'),
            data: implData,
            backgroundColor: withAlpha(secondary, 0.18),
            borderColor: withAlpha(secondary, 1),
            borderWidth: 3,
            pointBackgroundColor: withAlpha(secondary, 1),
            pointRadius: 3,
            pointHoverRadius: 5,
            fill: true
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          r: {
            beginAtZero: true,
            suggestedMin: 0,
            suggestedMax: 5,
            ticks: {
              stepSize: 1,
              color: textColor,
              backdropColor: "transparent",
              font: { size: 12 }
            },
            grid: { color: withAlpha(borderColor, 0.7) },
            angleLines: { color: withAlpha(borderColor, 0.7) },
            pointLabels: {
              display: true,
              color: textSecondary || borderColor,
              font: { size: fontSize, weight: "400" },
              callback: (_label, index) => `${index + 1}`
            }
          }
        },
        plugins: { legend: { display: true, position: 'bottom', labels: { usePointStyle: true, padding: 12 } } }
      }
    });
    renderLegend("nis2ProgramRadarLegend", labels);
    attachLegendTooltip("nis2ProgramRadarLegend");
  }
  function renderNis2ActionsChart() {
    const canvas = document.getElementById("nis2ActionsChart");
    if (!canvas) return;
    if (charts.nis2ActionsChart) charts.nis2ActionsChart.destroy();
    ensureNis2Plan();
    const themeVars = getThemeVars();
    const actions = (appData.nis2Plan.domains || []).flatMap((d) => d.actions || []);
    const statuses = ["done", "in_progress", "suspended", "todo"];
    const labels = [t('nis2Statuses.done', 'Réalisé'), t('nis2Statuses.inProgress', 'En cours'), t('nis2Statuses.suspended', 'Suspendu'), t('nis2Statuses.todo', 'À réaliser')];
    const palette = [
      withAlpha(themeVars.success, 0.85),
      withAlpha(themeVars.warning, 0.85),
      withAlpha(themeVars.secondary, 0.85),
      withAlpha(themeVars.primary, 0.85)
    ];
    const borderPalette = [
      withAlpha(themeVars.success, 1),
      withAlpha(themeVars.warning, 1),
      withAlpha(themeVars.secondary, 1),
      withAlpha(themeVars.primary, 1)
    ];
    const data = statuses.map((s) => actions.filter((a) => (a.status || "todo") === s).length);
    const total = data.reduce((a, b) => a + b, 0);
    charts.nis2ActionsChart = new Chart(canvas, {
      type: "doughnut",
      data: {
        labels,
        datasets: [
          {
            data,
            backgroundColor: palette,
            borderColor: borderPalette,
            borderWidth: 2,
            hoverOffset: 10
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: "58%",
        plugins: {
          legend: {
            position: "bottom",
            labels: { usePointStyle: true, padding: 16 }
          },
          tooltip: {
            callbacks: {
              label: (context) => {
                const value = context.raw || 0;
                const suffix = value > 1 ? "actions" : "action";
                const percent = total ? Math.round(value / total * 100) : 0;
                return `${context.label}: ${value} ${suffix} (${percent}%)`;
              }
            }
          }
        }
      }
    });
    renderNis2ActionsLegend(labels, data, borderPalette);
  }
  function renderNis2ActionsByDomainChart() {
    const canvas = document.getElementById("nis2ActionsDomainChart");
    if (!canvas) return;
    if (charts.nis2ActionsByDomainChart) charts.nis2ActionsByDomainChart.destroy();
    ensureNis2Plan();
    const themeVars = getThemeVars();
    const domains = (appData.nis2Plan && appData.nis2Plan.domains) || [];
    const labels = domains.map((domain, index) => {
      const title = (domain.title || "").trim();
      return title || `Domaine ${index + 1}`;
    });
    const data = domains.map((domain) => (domain.actions || []).length);
    const total = data.reduce((sum, value) => sum + value, 0);
    const palette = [
      themeVars.primary,
      themeVars.secondary,
      themeVars.warning,
      themeVars.success,
      themeVars.info,
      themeVars.danger
    ];
    const colors = labels.map((_, idx) => withAlpha(palette[idx % palette.length], 0.8));
    const borderColors = labels.map((_, idx) => withAlpha(palette[idx % palette.length], 1));
    charts.nis2ActionsByDomainChart = new Chart(canvas, {
      type: "doughnut",
      data: {
        labels,
        datasets: [
          {
            data,
            backgroundColor: colors,
            borderColor: borderColors,
            borderWidth: 2,
            hoverOffset: 10
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: "58%",
        plugins: {
          legend: {
            display: labels.length <= 6,
            position: "bottom",
            labels: { usePointStyle: true, padding: 14 }
          },
          tooltip: {
            callbacks: {
              label: (context) => {
                const value = context.raw || 0;
                const suffix = value > 1 ? "actions" : "action";
                const percent = total ? Math.round(value / total * 100) : 0;
                return `${context.label}: ${value} ${suffix} (${percent}%)`;
              }
            }
          }
        }
      }
    });
  }
  function renderNis2ActionsLegend(labels, data, colors) {
    const legend = document.getElementById("nis2ActionsLegend");
    if (!legend) return;
    legend.innerHTML = "";
    legend.style.display = "none";
  }
  function renderNis2StatusList() {
    const list = document.getElementById("nis2StatusList");
    if (!list) return;
    list.innerHTML = "";
    const shortenNis2StatusText = (text) => {
      const normalized = NORMALIZE(text);
      const map = {
        "entite essentielle nis2 enregistree": "Entité essentielle NIS2 enregistrée",
        "analyse de maturite nis2 realisee": "Analyse NIS2 réalisée",
        "systeme de management de la securite implemente (smsi)": "SMSI implémenté",
        "obtention d'une certification iso27001 (perimetre iam)": "Certification ISO27001 (IAM)",
        "atteindre un niveau de maturite superieur a 3": "Maturité >3 atteinte",
        "obtenir le label cyberfun du ccb belgique": "Label CyberFun (CCB)"
      };
      if (map[normalized]) return map[normalized];
      if (normalized.includes("enregistree comme entite essentielle nis2 au ccb")) {
        return "Entité essentielle NIS2 enregistrée";
      }
      return text.length > 70 ? text.slice(0, 67) + "…" : text;
    };
    (appData.nis2Plan.statuses || []).forEach((item, idx) => {
      const normalizedText = NORMALIZE(item.text || "");
      if (normalizedText.includes("trajectoire") && normalizedText.includes("nis2 2027")) return;
      const displayText = shortenNis2StatusText(item.text || `Statut ${idx + 1}`);
      const statusKey = item.status || "todo";
      const dotClass = statusKey === "done" ? "nis2-status-dot--done" : statusKey === "in_progress" ? "nis2-status-dot--progress" : "nis2-status-dot--todo";
      const meta = NIS2_STATUS_META[statusKey] || NIS2_STATUS_META.todo;
      const iconClass = statusKey === "done" ? "fa-circle-check" : statusKey === "in_progress" ? "fa-hourglass-half" : "fa-flag";
      const isCritical = normalizedText.includes("cyberfun");
      const li = document.createElement("li");
      li.className = `nis2-status-item nis2-status-item--${statusKey}${isCritical ? " nis2-status-item--critical" : ""}`;
      li.innerHTML = `
        <div class="nis2-status-icon"><i class="fas ${iconClass}"></i></div>
        <div class="nis2-status-content">
          <div class="nis2-status-chip">
            <span class="nis2-status-dot ${dotClass}" aria-hidden="true"></span>
            <span class="nis2-status-chip__label">${meta.label}</span>
          </div>
          <div class="nis2-status-text">${escapeHtml(displayText)}</div>
        </div>
        <button class="icon-btn" type="button" data-nis2-status-id="${escapeHtml(item.id)}" title="${t('nis2Plan.editKeyStatus', 'Modifier le statut clé')}"><i class="fas fa-pen"></i></button>
      `;
      list.appendChild(li);
    });
  }
  function renderNis2Plan() {
    const container = document.getElementById("nis2PlanContainer");
    if (!container) return;
    ensureNis2Plan();
    ensureNis2PlanToolbar();
    refreshNis2PhaseFilter();
    if (!isReadOnlyMode || !isReadOnlyMode()) renderNis2PhasesPanel();
    const phaseFilter = (document.getElementById("nis2PhaseFilter")?.value || "all").toString();
    const pillarFilter = (document.getElementById("nis2PillarFilter")?.value || "all").toString();
    const viewMode = nis2PlanViewMode || "detail";
    const focusOpen = !!nis2PlanShowOpenOnly;
    const allowEdit = !(isReadOnlyMode && typeof isReadOnlyMode === "function" ? isReadOnlyMode() : false);
    container.classList.toggle("nis2-plan--executive", viewMode === "executive");
    container.classList.toggle("nis2-plan--focus-open", focusOpen);
    container.classList.add("nis2-plan-list");
    container.classList.remove("nis2-plan-grid");
    container.innerHTML = "";
    let displayed = 0;
    let totalActions = 0;
    let doneActions = 0;
    let openActions = 0;
    let progressSum = 0;
    let budgetSum = 0;
    let budgetAll = 0;
    let budget2026Total = 0;
    let budget2027Total = 0;
    let budget2028Total = 0;
    const customBlocks = appData.nis2Plan.customBlocks || [];
    const summaryEl = document.createElement("div");
    summaryEl.className = "nis2-plan-summary-strip";
    const frag = document.createDocumentFragment();
    (appData.nis2Plan.domains || []).forEach((domain, dIndex) => {
      const pillarMatches = pillarFilter === "all" || String(domain.functionId || domain.pillar || "") === pillarFilter;
      if (!pillarMatches) return;
      const phases = getSortedNis2Phases();
      const iconClass = NIS2_DOMAIN_ICONS[NORMALIZE(domain.title || "")] || "fa-circle-nodes";
      let actions = (domain.actions || []).map((a) => {
        const phaseId = normalizePhaseId(a.phase);
        if (a.phase !== phaseId) a.phase = phaseId;
        return a;
      });
      const hadActions = actions.length > 0;
      if (phaseFilter !== "all") actions = actions.filter(a => String(a.phase || "") === phaseFilter);
      if (focusOpen) actions = actions.filter((a) => a.status !== "done");
      if (!actions.length && hadActions) return;
      displayed++;
      const urgencyLabel = NIS2_URGENCY_LABELS[domain.urgency] || NIS2_URGENCY_LABELS.standard;
      const objectiveHtml = domain.objective ? `<p class="info-box__text">${escapeHtml(domain.objective)}</p>` : "";
      const actionsByPhase = {};
      phases.forEach((p) => { actionsByPhase[p.id] = []; });
      actions.forEach((a) => {
        const key = a.phase || normalizePhaseId(a.phase);
        if (!actionsByPhase[key]) actionsByPhase[key] = [];
        actionsByPhase[key].push(a);
      });
      const totalDomainActions = actions.length;
      const doneDomain = actions.filter((a) => a.status === "done").length;
      const openDomain = actions.filter((a) => a.status !== "done").length;
      const progressValues = actions.map((a) => {
        const progress = Number.isFinite(a.progress) ? a.progress : a.status === "done" ? 100 : 0;
        return Math.max(0, Math.min(100, Math.round(progress)));
      });
      const avgProgress = progressValues.length ? Math.round(progressValues.reduce((a, b) => a + b, 0) / progressValues.length) : 0;
      const domainBudgetSum = actions.reduce((sum, a) => {
        const val = getActionBudgetTotal(a);
        return Number.isFinite(val) ? sum + val : sum;
      }, 0);
      actions.forEach((a) => {
        const val = getActionBudgetTotal(a);
        if (Number.isFinite(val)) budgetAll += val;
        const b26 = parseBudgetValue(a.budget2026);
        if (Number.isFinite(b26)) budget2026Total += b26;
        const b27 = parseBudgetValue(a.budget2027);
        if (Number.isFinite(b27)) budget2027Total += b27;
        const b28 = parseBudgetValue(a.budget2028);
        if (Number.isFinite(b28)) budget2028Total += b28;
      });
      const domainBudgetDisplay = Number.isFinite(domainBudgetSum) && domainBudgetSum >= 0 ? formatCurrency(domainBudgetSum) : "—";
      totalActions += totalDomainActions;
      doneActions += doneDomain;
      openActions += openDomain;
      progressSum += progressValues.reduce((a, b) => a + b, 0);
      budgetSum += domainBudgetSum;
      const tableColspan = allowEdit ? 10 : 9;
      let actionsHtml = phases.map((phase) => {
        const rows = actionsByPhase[phase.id] || [];
        if (!rows.length) return "";
        const label = getNis2PhaseLabel(phase.id);
        const rowsHtml = rows.map((action, aIndex) => {
          const meta = NIS2_STATUS_META[action.status] || NIS2_STATUS_META.todo;
          const dotClass = action.status === "done" ? "nis2-status-dot--done" : action.status === "in_progress" ? "nis2-status-dot--progress" : action.status === "suspended" ? "nis2-status-dot--suspended" : "nis2-status-dot--todo";
          const phaseLabel = getNis2PhaseLabel(action.phase);
          const typeLabel = NIS2_ACTION_TYPE_LABELS[action.workType] || NIS2_ACTION_TYPE_LABELS.initiative;
          const ownerLabel = action.owner ? escapeHtml(action.owner) : "—";
          const budget26 = formatBudgetValue(action.budget2026);
          const budget27 = formatBudgetValue(action.budget2027);
          const budget28 = formatBudgetValue(action.budget2028);
          const totalBudget = getActionBudgetTotal(action);
          const budgetTotalLabel = Number.isFinite(totalBudget) ? formatCurrency(totalBudget) : "—";
          const progress = Number.isFinite(action.progress) ? Math.max(0, Math.min(100, Math.round(action.progress))) : (action.status === "done" ? 100 : 0);
          return `
          <tr>
            <td>
              <div class="nis2-action-main">
                <span class="nis2-status-dot ${dotClass}"></span>
                <div>
                  <div class="nis2-action-text">${escapeHtml(action.text || `Action ${dIndex + 1}-${aIndex + 1}`)}</div>
                  <div class="nis2-action-meta">
                    <span class="chip chip--phase">${escapeHtml(phaseLabel)}</span>
                    <span class="chip chip--type">${escapeHtml(typeLabel)}</span>
                    <span class="chip chip--owner">${ownerLabel}</span>
                  </div>
                </div>
              </div>
            </td>
            <td class="text-right">${budget26}</td>
            <td class="text-right">${budget27}</td>
            <td class="text-right">${budget28}</td>
            <td class="text-right">${budgetTotalLabel}</td>
            <td class="text-center">${ownerLabel}</td>
            <td class="text-center">${escapeHtml(phaseLabel)}</td>
            <td class="text-center nis2-progress-cell">
              <div class="progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar__fill" style="width: ${progress}%"></div></div>
              <span class="progress-label">${progress}%</span>
            </td>
            <td><span class="${meta.className}">${meta.label}</span></td>
            ${allowEdit ? `<td class="text-right">
              <button class="icon-btn" type="button" data-nis2-action="edit-action" data-domain-id="${escapeHtml(domain.id)}" data-action-id="${escapeHtml(action.id)}" title="${t('nis2Plan.editAction', "Modifier l'action")}"><i class="fas fa-pen"></i></button>
              <button class="icon-btn icon-btn--danger" type="button" data-nis2-action="delete-action" data-domain-id="${escapeHtml(domain.id)}" data-action-id="${escapeHtml(action.id)}" title="${t('nis2Plan.deleteAction', "Supprimer l'action")}"><i class="fas fa-trash"></i></button>
            </td>` : ""}
          </tr>
        `;
        }).join("");
        return `
          <tr class="nis2-phase-group"><td colspan="${tableColspan}">${escapeHtml(label)}</td></tr>
          ${rowsHtml}
        `;
      }).join("");
      if (!actionsHtml) {
        actionsHtml = `
          <tr>
            <td colspan="${tableColspan}">
              <div class="nis2-empty">${t('nis2Plan.noActionsForDomain', 'Aucune action pour ce domaine.')}</div>
            </td>
          </tr>
        `;
      }
      const card = document.createElement("div");
      card.className = `nis2-plan-card nis2-plan-card--pillar${domain.pillar || 1}`;
      card.innerHTML = `
        <div class="nis2-plan-card__header">
          <div class="nis2-plan-card__header-left">
            <div class="nis2-plan-card__icon"><i class="fas ${iconClass}"></i></div>
            <div>
              <p class="nis2-plan-card__eyebrow">${t('nis2Plan.pillar', 'Pilier')} ${escapeHtml(domain.pillar || "")}</p>
              <h4 class="module__title">${escapeHtml(domain.title || `${t('nis2Plan.domain', 'Domaine')} ${dIndex + 1}`)}</h4>
              ${objectiveHtml}
              <div class="nis2-plan-meta">
                <span class="nis2-tag">${t('nis2Plan.pillar', 'Pilier')} ${escapeHtml(domain.pillar || "")}</span>
                <span class="nis2-tag">${escapeHtml(urgencyLabel)}</span>
                <span class="nis2-tag"><i class="fas fa-list-check"></i> ${doneDomain}/${totalDomainActions} ${t('nis2Plan.delivered', 'livrées')}</span>
              </div>
            </div>
          </div>
          ${allowEdit ? `<div>
            <button class="icon-btn" type="button" data-nis2-action="edit-domain" data-domain-id="${escapeHtml(domain.id)}" title="${t('nis2Plan.editDomain', 'Modifier le domaine')}"><i class="fas fa-pen"></i></button>
          </div>` : ""}
        </div>
        <div class="nis2-plan-stats">
          <span class="stat-chip"><i class="fas fa-gauge-high"></i> ${t('nis2Plan.avgProgress', 'Avancement moyen')} ${avgProgress}%</span>
          <span class="stat-chip"><i class="fas fa-hourglass-half"></i> ${openDomain} ${t('nis2Plan.inProgressTodo', 'en cours / à faire')}</span>
          <span class="stat-chip"><i class="fas fa-check-circle"></i> ${doneDomain} ${t('nis2Plan.completed', 'réalisées')}</span>
          <span class="stat-chip"><i class="fas fa-coins"></i> ${t('nis2Plan.budget', 'Budget')} ${domainBudgetDisplay}</span>
        </div>
        <div class="nis2-actions">
          <table class="table--nis2-plan ${viewMode === "executive" ? "table--nis2-plan-executive" : ""}">
            <thead>
              <tr>
                <th>${t('nis2Plan.colAction', 'Action')}</th>
                <th>${t('nis2Plan.budget', 'Budget')} 2026</th>
                <th>${t('nis2Plan.budget', 'Budget')} 2027</th>
                <th>${t('nis2Plan.budget', 'Budget')} 2028</th>
                <th>${t('nis2Plan.colBudgetTotal', 'Budget total')}</th>
                <th>${t('nis2Plan.colOwner', 'Responsable')}</th>
                <th>${t('nis2Plan.colPhase', 'Phase')}</th>
                <th>${t('nis2Plan.colProgress', 'Avancement')}</th>
                <th>${t('nis2Plan.colStatus', 'Statut')}</th>
                ${allowEdit ? `<th class="text-right">${t('nis2Plan.colActions', 'Actions')}</th>` : ""}
              </tr>
            </thead>
            <tbody>
              ${actionsHtml}
            </tbody>
          </table>
        </div>
        ${allowEdit ? `<div>
          <button class="btn btn--outline btn--sm" type="button" data-nis2-action="add-action" data-domain-id="${escapeHtml(domain.id)}">
            <i class="fas fa-plus"></i> ${t('nis2Plan.addAction', 'Ajouter une action')}
          </button>
        </div>` : ""}
      `;
      frag.appendChild(card);
    });
    if (totalActions > 0) {
      const avg = totalActions ? Math.round(progressSum / totalActions) : 0;
      const budgetDisplay = Number.isFinite(budgetSum) ? formatCurrency(budgetSum) : "—";
      summaryEl.innerHTML = `
        <div class="stat-chip"><i class="fas fa-chart-line"></i> ${totalActions} ${t('nis2Plan.actionsFiltered', 'actions (filtre appliqué)')}</div>
        <div class="stat-chip"><i class="fas fa-check-double"></i> ${doneActions} ${t('nis2Plan.completed', 'réalisées')}</div>
        <div class="stat-chip"><i class="fas fa-circle-notch"></i> ${openActions} ${t('nis2Plan.inProgressTodo', 'en cours / à faire')}</div>
        <div class="stat-chip"><i class="fas fa-gauge"></i> ${t('nis2Plan.avgProgress', 'Avancement moyen')} ${avg}%</div>
        <div class="stat-chip"><i class="fas fa-sack-dollar"></i> ${t('nis2Plan.cumulativeBudget', 'Budget cumulé')} ${budgetDisplay}</div>
      `;
      container.appendChild(summaryEl);
    }
    renderNis2BudgetGrid({ budgetAll, budget2026Total, budget2027Total, budget2028Total, customBlocks });
    container.appendChild(frag);
    if (displayed === 0) {
      container.innerHTML = `<div class="nis2-empty">${t('nis2Plan.noActionsMatchFilters', 'Aucune action ne correspond à vos filtres actuels.')}</div>`;
    }
    renderNis2ActionsChart();
    renderNis2ActionsByDomainChart();
  }
  function renderNis2BudgetGrid({ budgetAll, budget2026Total, budget2027Total, budget2028Total, customBlocks = [] }) {
    const module = document.getElementById("nis2-program");
    const anchor = module ? module.querySelector(".nis2-top-grid") : null;
    if (!module || !anchor) return;
    const customGrid = renderNis2CustomBlocks(customBlocks);
    const budgetBlocks = Array.isArray(appData?.nis2Plan?.budgetBlocks) && appData.nis2Plan.budgetBlocks.length
      ? appData.nis2Plan.budgetBlocks
      : cloneDefaultNis2Plan().budgetBlocks || [];
    const hasBudgetBlocks = budgetBlocks.length > 0;
    const hasBudgetValues = [budgetAll, budget2026Total, budget2027Total, budget2028Total].some((val) => Number.isFinite(val) && val > 0);
    let grid = document.getElementById("nis2BudgetGrid");
    if (!hasBudgetBlocks && !hasBudgetValues) {
      if (grid) grid.remove();
      return;
    }
    if (!grid) {
      grid = document.createElement("div");
      grid.id = "nis2BudgetGrid";
      grid.className = "nis2-budget-grid";
    }
    if (customGrid && customGrid.parentNode) {
      if (customGrid.nextSibling) customGrid.parentNode.insertBefore(grid, customGrid.nextSibling);
      else customGrid.parentNode.appendChild(grid);
    } else {
      module.insertBefore(grid, anchor);
    }
    const safe = (idx, fallback) => budgetBlocks[idx]?.title || fallback;
    const readonly = isReadOnlyMode && typeof isReadOnlyMode === "function" ? isReadOnlyMode() : false;
    const editHint = readonly ? "" : `<div class="nis2-budget-card__hint"><i class="fas fa-pen"></i> Modifier</div>`;
    const budgetValues = [budgetAll, budget2026Total, budget2027Total, budget2028Total];
    const budgetIcons = ["fa-coins", "fa-calendar-alt", "fa-calendar-check", "fa-calendar-plus"];
    const budgetColors = ["kpi-card--primary", "kpi-card--success", "kpi-card--warning", "kpi-card--info"];
    const budgetDefaults = [
      t('templates.budgetTotal', 'Budget total mise en conformité'),
      t('templates.budget2026', 'Budget 2026'),
      t('templates.budget2027', 'Budget 2027'),
      t('templates.budget2028', 'Budget 2028')
    ];
    grid.innerHTML = budgetValues.map((val, idx) => `
      <div class="kpi-card ${budgetColors[idx]}" data-nis2-budget-idx="${idx}" role="button" tabindex="0" aria-label="Modifier le bloc budget ${idx + 1}">
        <div class="kpi-card__icon"><i class="fas ${budgetIcons[idx]}"></i></div>
        <div class="kpi-card__content">
          <p class="kpi-card__eyebrow">${escapeHtml(safe(idx, budgetDefaults[idx]))}</p>
          <div class="kpi-card__value">${formatCurrency(val)}</div>
          ${editHint}
        </div>
      </div>
    `).join("");
    if (!grid.dataset.bound) {
      grid.dataset.bound = "true";
      const handler = (event) => {
        const card = event.target.closest("[data-nis2-budget-idx]");
        if (!card) return;
        const idx = parseInt(card.dataset.nis2BudgetIdx, 10);
        openNis2BudgetBlockModal(Number.isFinite(idx) ? idx : 0);
      };
      grid.addEventListener("click", handler);
      grid.addEventListener("keydown", (event) => {
        if (event.key === "Enter" || event.key === " ") {
          event.preventDefault();
          handler(event);
        }
      });
    }
  }
  function renderNis2CustomBlocks(blocks = []) {
    const module = document.getElementById("nis2-program");
    const anchor = module ? module.querySelector(".nis2-top-grid") : null;
    if (!module || !anchor) return null;
    const fallbackBlocks = cloneDefaultNis2Plan().customBlocks || [];
    const hasBlocks = Array.isArray(blocks) && blocks.length > 0;
    if (!hasBlocks && fallbackBlocks.length === 0) {
      const existing = document.getElementById("nis2CustomBlocks");
      if (existing) existing.remove();
      return null;
    }
    const source = Array.isArray(blocks) && blocks.length ? blocks : fallbackBlocks;
    const minLength = Math.max(3, fallbackBlocks.length || 0);
    const safeBlocks = source.slice(0, Math.max(source.length, minLength));
    while (safeBlocks.length < minLength) {
      const template = fallbackBlocks[safeBlocks.length % fallbackBlocks.length] || { title: `Bloc ${safeBlocks.length + 1}`, value: 0, format: safeBlocks.length > 0 ? "currency" : "percent" };
      const format = (template.format || (safeBlocks.length > 0 ? "currency" : "percent")).toLowerCase();
      const fallbackValue = format === "currency" ? parseLooseNumber(template.value) : clampPercent(template.value, 0);
      safeBlocks.push({
        id: template.id || `nis2-highlight-${safeBlocks.length + 1}`,
        title: template.title,
        value: Number.isFinite(fallbackValue) ? fallbackValue : 0,
        format
      });
    }
    let grid = document.getElementById("nis2CustomBlocks");
    if (!grid) {
      grid = document.createElement("div");
      grid.id = "nis2CustomBlocks";
      grid.className = "nis2-custom-grid";
      module.insertBefore(grid, anchor);
    } else {
      module.insertBefore(grid, anchor);
    }
    const accentClasses = ["kpi-card--primary", "kpi-card--warning", "kpi-card--success"];
    const iconSet = ["fa-bullseye", "fa-rocket", "fa-flag-checkered"];
    grid.innerHTML = safeBlocks.slice(0, 3).map((block, idx) => {
      const format = (block.format || (idx > 0 ? "currency" : "percent")).toLowerCase();
      const rawValue = typeof block.value !== "undefined" ? block.value : block.percent;
      const value = format === "currency"
        ? (() => {
            const num = parseLooseNumber(rawValue);
            return Number.isFinite(num) ? num : 0;
          })()
        : clampPercent(rawValue, 0);
      const accent = accentClasses[idx % accentClasses.length];
      const icon = iconSet[idx % iconSet.length];
      const eyebrow = escapeHtml(block.title || `Bloc ${idx + 1}`);
      const valueDisplay = format === "currency" ? formatCurrency(value) : `${value}%`;
      return `
        <div class="kpi-card nis2-custom-card ${accent}" role="button" tabindex="0" data-nis2-block-id="${escapeHtml(block.id)}" data-nis2-block-index="${idx}" aria-label="Modifier le bloc ${escapeHtml(block.title || `Bloc ${idx + 1}`)}">
          <div class="kpi-card__icon"><i class="fas ${icon}"></i></div>
          <div class="kpi-card__content">
            <p class="kpi-card__eyebrow">${eyebrow}</p>
            <div class="kpi-card__value">${valueDisplay}</div>
            ${isReadOnlyMode && typeof isReadOnlyMode === "function" && isReadOnlyMode() ? "" : `<div class="nis2-custom-card__hint"><i class="fas fa-pen"></i> Modifier</div>`}
          </div>
        </div>
      `;
    }).join("");
    if (!grid.dataset.bound) {
      grid.dataset.bound = "true";
      const openFromEvent = (event) => {
        const card = event.target.closest("[data-nis2-block-id]");
        if (!card) return;
        const index = parseInt(card.dataset.nis2BlockIndex, 10);
        openNis2CustomBlockModal(card.dataset.nis2BlockId, Number.isFinite(index) ? index : 0);
      };
      grid.addEventListener("click", openFromEvent);
      grid.addEventListener("keydown", (event) => {
        if (event.key === "Enter" || event.key === " ") {
          event.preventDefault();
          openFromEvent(event);
        }
      });
    }
    return grid;
  }
  function openNis2CustomBlockModal(blockId = "", fallbackIndex = 0) {
    if (!ensureEditMode("edit a NIS2 objective block")) return;
    ensureNis2Plan();
    const blocks = appData.nis2Plan.customBlocks || [];
    const defaults = cloneDefaultNis2Plan().customBlocks || [];
    const foundIndex = blocks.findIndex((b) => b.id === blockId);
    const index = foundIndex >= 0 ? foundIndex : (Number.isFinite(fallbackIndex) ? fallbackIndex : 0);
    const fallback = defaults[index % defaults.length] || { title: `Bloc ${index + 1}`, value: 0, format: index > 0 ? "currency" : "percent" };
    const block = blocks[index] || fallback;
    const format = (block.format || fallback.format || (index > 0 ? "currency" : "percent")).toLowerCase();
    const currentValue = typeof block.value !== "undefined" ? block.value : (typeof block.percent !== "undefined" ? block.percent : fallback.value);
    const valueFieldLabel = format === "currency" ? "Montant (€)" : "Progression (%)";
    const valueFieldHelp = format === "currency" ? "Coût budgétaire en euros" : "Entre 0 et 100";
    openModal(t('modals.nis2Block', 'Bloc NIS2') + ' ' + (index + 1), [
      { name: "title", label: "Titre du bloc", value: block.title || "" },
      { name: "value", label: valueFieldLabel, type: "number", value: currentValue ?? 0, help: valueFieldHelp }
    ], (data) => {
      const cleanValue = format === "currency"
        ? (() => {
            const num = parseLooseNumber(data.value ?? currentValue ?? 0);
            return Number.isFinite(num) ? num : 0;
          })()
        : clampPercent(data.value ?? currentValue ?? 0, clampPercent(currentValue ?? 0, 0));
      const updated = {
        id: block.id || blockId || `nis2-highlight-${index + 1}`,
        title: data.title || block.title || `Bloc ${index + 1}`,
        value: cleanValue,
        format
      };
      while (blocks.length < Math.max(index + 1, 3)) {
        const template = defaults[blocks.length % defaults.length] || { title: `Bloc ${blocks.length + 1}`, value: 0, format: blocks.length > 0 ? "currency" : "percent" };
        blocks.push({
          id: template.id || `nis2-highlight-${blocks.length + 1}`,
          title: template.title,
          value: Number.isFinite(template.value) ? template.value : 0,
          format: template.format || (blocks.length > 0 ? "currency" : "percent")
        });
      }
      blocks[index] = updated;
      appData.nis2Plan.customBlocks = blocks;
      saveData();
      renderNis2Plan();
      return true;
    });
  }
  function openNis2BudgetBlockModal(index = 0) {
    if (!ensureEditMode("edit a NIS2 budget block")) return;
    ensureNis2Plan();
    const defaults = cloneDefaultNis2Plan().budgetBlocks || [];
    const blocks = Array.isArray(appData.nis2Plan.budgetBlocks) ? [...appData.nis2Plan.budgetBlocks] : [];
    const fallback = defaults[index % (defaults.length || 1)] || { id: `nis2-budget-${index + 1}`, title: `Budget ${index + 1}` };
    const block = blocks[index] || fallback;
    openModal(t('modals.budgetBlock', 'Bloc budget') + ' ' + (index + 1), [
      { name: "title", label: "Titre du bloc budget", value: block.title || fallback.title || `Budget ${index + 1}` }
    ], (data) => {
      while (blocks.length < Math.max(index + 1, defaults.length)) {
        const tpl = defaults[blocks.length % (defaults.length || 1)] || { id: `nis2-budget-${blocks.length + 1}`, title: `Budget ${blocks.length + 1}` };
        blocks.push({ id: tpl.id, title: tpl.title });
      }
      blocks[index] = {
        id: block.id || fallback.id || `nis2-budget-${index + 1}`,
        title: data.title || block.title || fallback.title || `Budget ${index + 1}`
      };
      appData.nis2Plan.budgetBlocks = blocks;
      saveData();
      renderNis2Plan();
      return true;
    });
  }
  function openBudgetCategoriesModal() {
    if (!ensureEditMode(t('editMode.manageBudgetCategories', 'gérer les catégories de budget'))) return;
    ensureNis2Plan();
    var cats = Array.isArray(appData.nis2Plan.budgetCategories) && appData.nis2Plan.budgetCategories.length
      ? appData.nis2Plan.budgetCategories
      : [{ value: "infra", label: "Infrastructure" }, { value: "cyber", label: "Cybersécurité" }, { value: "divers", label: "Divers" }];
    var catsText = cats.map(function(c) { return c.label; }).join('\n');
    openModal(t('modals.budgetCategories', 'Catégories de budget'), [
      { name: "categories", label: t('fields.budgetCategoriesList', 'Une catégorie par ligne (min. 1)'), type: "textarea", value: catsText }
    ], function(data) {
      var lines = (data.categories || '').split('\n').map(function(l) { return l.trim(); }).filter(function(l) { return l.length > 0; });
      if (lines.length === 0) { showToast(t('toasts.budgetCatMin', 'Au moins une catégorie requise'), 'warning'); return false; }
      appData.nis2Plan.budgetCategories = lines.map(function(label) {
        // Try to match existing value key, otherwise generate from label
        var existing = cats.find(function(c) { return c.label === label; });
        var value = existing ? existing.value : label.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
        return { value: value, label: label };
      });
      saveData();
      renderBudgetTab();
      showToast(t('toasts.budgetCatUpdated', 'Catégories de budget mises à jour'), 'success');
      return true;
    });
  }
  function openNis2DomainModal(domainId) {
    if (!ensureEditMode("edit a NIS2 domain")) return;
    const domain = (appData.nis2Plan.domains || []).find(d => d.id === domainId) || { title: "", objective: "", pillar: 1, urgency: "standard" };
    const isEdit = !!domainId;
    openModal(isEdit ? t('modals.editNis2Domain') : t('modals.addNis2Domain'), [
      { name: "title", label: t('fields.domainTitle'), value: domain.title },
      { name: "objective", label: t('fields.objective'), type: "textarea", value: domain.objective },
      { name: "pillar", label: t('fields.pillar'), type: "number", value: domain.pillar || 1 },
      { name: "urgency", label: t('fields.priority'), type: "select", value: domain.urgency || "standard", options: [
        { value: "high", label: t('fields.urgencyHigh') },
        { value: "medium", label: t('fields.urgencyMedium') },
        { value: "standard", label: t('fields.urgencyStandard') }
      ] }
    ], (data) => {
      if (!data.title) return false;
      const clean = {
        title: data.title,
        objective: data.objective || "",
        pillar: parseInt(data.pillar, 10) || 1,
        urgency: data.urgency || "standard"
      };
      if (isEdit) {
        const idx = appData.nis2Plan.domains.findIndex(d => d.id === domainId);
        if (idx >= 0) {
          appData.nis2Plan.domains[idx] = Object.assign({}, appData.nis2Plan.domains[idx], clean);
        }
      } else {
        const newId = "nis2-domain-" + (appData.nis2Plan.domains.length + 1);
        appData.nis2Plan.domains.push(Object.assign({ id: newId, actions: [] }, clean));
      }
      saveData();
      renderNis2Plan();
      return true;
    }, isEdit ? () => deleteNis2Domain(domainId) : null);
  }
  function openNis2ActionModal(domainId = "", actionId = "") {
    if (!ensureEditMode("edit a NIS2 action")) return;
    ensureNis2Plan();
    ensureNis2Phases();
    const domains = (appData.nis2Plan && appData.nis2Plan.domains) || [];
    if (domains.length === 0) {
      showToast(t('toasts.addNis2DomainFirst', "Ajoutez d'abord un domaine NIS2 avant de créer une action."), "warning");
      return;
    }
    const domain = domains.find(d => d.id === domainId) || domains[0];
    const action = (domain.actions || []).find(a => a.id === actionId) || {
      text: "",
      phase: normalizePhaseId(1),
      status: "todo",
      owner: "",
      workType: "initiative",
      progress: 0,
      budget: "",
      budget2026: "",
      budget2027: "",
      budget2028: "",
      budgetCategory: "divers"
    };
    const phases = getSortedNis2Phases();
    const defaultPhase = phases[0]?.id || normalizePhaseId(action.phase);
    const phaseOptions = phases.map((p) => ({ value: p.id, label: p.label }));
    const isEdit = !!actionId;
    const actionTypeOptions = typeof NIS2_ACTION_TYPE_OPTIONS !== "undefined" ? NIS2_ACTION_TYPE_OPTIONS : [
      { value: "service", label: "Service" },
      { value: "projet", label: "Projet" },
      { value: "initiative", label: "Initiative" },
      { value: "programme", label: "Programme" }
    ];
    const budgetCategoryOptions = Array.isArray(appData.nis2Plan?.budgetCategories) && appData.nis2Plan.budgetCategories.length
      ? appData.nis2Plan.budgetCategories
      : [{ value: "infra", label: "Infrastructure" }, { value: "cyber", label: "Cybersécurité" }, { value: "divers", label: "Divers" }];
    openModal(isEdit ? t('modals.editNis2Action') : t('modals.addNis2Action'), [
      { name: "domainId", label: t('fields.domain'), type: "select", value: domain.id, options: domains.map(d => ({ value: d.id, label: d.title })) },
      { name: "text", label: t('fields.actionDescription'), type: "textarea", value: action.text },
      { name: "phase", label: t('fields.phase'), type: "select", value: action.phase || defaultPhase, options: phaseOptions },
      { name: "workType", label: t('fields.workType'), type: "select", value: action.workType || "initiative", options: actionTypeOptions },
      { name: "owner", label: t('fields.actionOwner'), value: action.owner || "", placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(action.owner), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "budget2026", label: t('fields.budget2026'), type: "number", value: action.budget2026 || "" },
      { name: "budget2027", label: t('fields.budget2027'), type: "number", value: action.budget2027 || "" },
      { name: "budget2028", label: t('fields.budget2028'), type: "number", value: action.budget2028 || "" },
      { name: "budget", label: t('fields.budgetGlobal'), value: action.budget || "" },
      { name: "budgetCategory", label: t('fields.budgetCategory'), type: "select", value: action.budgetCategory || "divers", options: budgetCategoryOptions },
      { name: "progress", label: t('fields.progress'), type: "number", value: action.progress ?? (action.status === "done" ? 100 : 0) },
      { name: "status", label: t('fields.status'), type: "select", value: action.status || "todo", options: [
        { value: "todo", label: t('statusLabels.todo') },
        { value: "in_progress", label: t('statusLabels.inProgress') },
        { value: "suspended", label: t('statusLabels.suspended') },
        { value: "done", label: t('statusLabels.done') }
      ] }
    ], (data) => {
      if (!data.text) return false;
      const targetDomain = domains.find(d => d.id === data.domainId) || domain;
      if (!Array.isArray(targetDomain.actions)) targetDomain.actions = [];
      if (isEdit) {
        // FIX-NIS2-ACTION-MOVE (audit 2026-06-10) : l'action éditée vit dans le domaine
        // SOURCE. La chercher dans le domaine CIBLE donnait idx=-1 quand l'utilisateur
        // changeait le champ « Domaine » → aucune écriture, modale fermée comme si tout
        // était sauvegardé. On applique d'abord les champs édités sur l'objet source,
        // puis on déplace l'objet MIS À JOUR vers le domaine cible si celui-ci a changé.
        const srcActions = Array.isArray(domain.actions) ? domain.actions : [];
        const srcIdx = srcActions.findIndex(a => a.id === actionId);
        if (srcIdx >= 0) {
          const updatedAction = Object.assign({}, srcActions[srcIdx], {
            text: data.text,
            phase: normalizePhaseId(data.phase),
            status: data.status || "todo",
            owner: data.owner || "",
            workType: data.workType || "initiative",
            budget: data.budget || "",
            budget2026: data.budget2026 || "",
            budget2027: data.budget2027 || "",
            budget2028: data.budget2028 || "",
            budgetCategory: data.budgetCategory || "divers",
            progress: Math.max(0, Math.min(100, parseInt(data.progress, 10) || 0))
          });
          if (targetDomain === domain) {
            srcActions[srcIdx] = updatedAction;
          } else {
            srcActions.splice(srcIdx, 1);
            targetDomain.actions.push(updatedAction);
          }
        }
      } else {
        const newActionId = generateId("nis2-act");
        targetDomain.actions.push({
          id: newActionId,
          text: data.text,
          phase: normalizePhaseId(data.phase),
          status: data.status || "todo",
          owner: data.owner || "",
          workType: data.workType || "initiative",
          budget: data.budget || "",
          budget2026: data.budget2026 || "",
          budget2027: data.budget2027 || "",
          budget2028: data.budget2028 || "",
          budgetCategory: data.budgetCategory || "divers",
          progress: Math.max(0, Math.min(100, parseInt(data.progress, 10) || 0))
        });
      }
      saveData();
      renderNis2Plan();
      return true;
    }, isEdit ? () => deleteNis2Action(domainId, actionId) : null);
  }
  function deleteNis2Domain(domainId) {
    if (!ensureEditMode("delete a NIS2 domain")) return;
    showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.deleteDomain', 'Supprimer ce domaine et ses actions ?'), function() {
      appData.nis2Plan.domains = (appData.nis2Plan.domains || []).filter(d => d.id !== domainId);
      saveData();
      renderNis2Plan();
      showToast2(t('toasts.itemDeleted'), "success");
    }, { type: 'danger' });
  }
  function deleteNis2Action(domainId, actionId) {
    if (!ensureEditMode("delete a NIS2 action")) return;
    showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.deleteAction', 'Supprimer cette action ?'), function() {
      const domain = (appData.nis2Plan.domains || []).find(d => d.id === domainId);
      if (!domain) return;
      domain.actions = (domain.actions || []).filter(a => a.id !== actionId);
      saveData();
      renderNis2Plan();
      showToast2(t('toasts.itemDeleted'), "success");
    }, { type: 'danger' });
  }
  function openNis2PhaseModal(phaseId = "") {
    if (!ensureEditMode("edit NIS2 phases")) return;
    ensureNis2Phases();
    const phases = getSortedNis2Phases();
    const phase = phases.find((p) => p.id === phaseId) || { label: "", order: phases.length + 1 };
    const isEdit = !!phaseId;
    openModal(isEdit ? "Modifier une phase NIS2" : "Nouvelle phase NIS2", [
      { name: "label", label: "Nom de la phase", value: phase.label },
      { name: "order", label: "Ordre d'affichage", type: "number", value: phase.order || phases.length + 1 }
    ], (data) => {
      if (!data.label) return false;
      const order = parseInt(data.order, 10);
      if (isEdit) {
        const idx = appData.nis2Plan.phases.findIndex((p) => p.id === phaseId);
        if (idx >= 0) {
          appData.nis2Plan.phases[idx] = Object.assign({}, appData.nis2Plan.phases[idx], {
            label: data.label,
            order: isNaN(order) ? appData.nis2Plan.phases[idx].order : order
          });
        }
      } else {
        const newId = generateId("phase");
        appData.nis2Plan.phases.push({
          id: newId,
          label: data.label,
          order: isNaN(order) ? phases.length + 1 : order,
          fallback: false
        });
      }
      appData.nis2Plan.phases = getSortedNis2Phases();
      saveData();
      renderNis2Plan();
      return true;
    }, isEdit && !phase.fallback ? () => deleteNis2Phase(phaseId) : null);
  }
  function deleteNis2Phase(phaseId) {
    if (!ensureEditMode("delete a NIS2 phase")) return;
    ensureNis2Phases();
    const phases = getSortedNis2Phases();
    const target = phases.find((p) => p.id === phaseId);
    if (!target || target.fallback) return;
    const fallback = phases.find((p) => p.fallback && p.id !== phaseId) || phases.find((p) => p.id !== phaseId);
    if (!fallback) return;
    showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.deletePhase', 'Supprimer cette phase ? Les actions seront déplacées vers la phase de secours.'), function() {
      appData.nis2Plan.phases = phases.filter((p) => p.id !== phaseId);
      (appData.nis2Plan.domains || []).forEach((d) => {
        d.actions = (d.actions || []).map((a) => {
          if (a.phase === phaseId) a.phase = fallback.id;
          return a;
        });
      });
      saveData();
      renderNis2Plan();
    }, { type: 'danger' });
  }
  function setupNis2PlanInteractions() {
    const container = document.getElementById("nis2PlanContainer");
    if (container) {
      container.addEventListener("click", (event) => {
        const btn = event.target.closest("[data-nis2-action]");
        if (!btn) return;
        const action = btn.dataset.nis2Action;
        const domainId = btn.dataset.domainId;
        const actionId = btn.dataset.actionId;
        if (isReadOnlyMode && typeof isReadOnlyMode === "function" && isReadOnlyMode()) return;
        if (action === "edit-domain") return openNis2DomainModal(domainId);
        if (action === "add-action") return openNis2ActionModal(domainId);
        if (action === "edit-action") return openNis2ActionModal(domainId, actionId);
        if (action === "delete-action") return deleteNis2Action(domainId, actionId);
      });
    }
    const phasesPanel = document.getElementById("nis2PhasesPanel");
    if (phasesPanel && !phasesPanel.dataset.bound) {
      phasesPanel.dataset.bound = "true";
      phasesPanel.addEventListener("click", (event) => {
        const addBtn = event.target.closest("[data-phase-action=\"add\"]");
        if (addBtn) return openNis2PhaseModal();
        const editBtn = event.target.closest("[data-phase-action=\"edit\"]");
        if (editBtn) return openNis2PhaseModal(editBtn.dataset.phaseId);
        const delBtn = event.target.closest("[data-phase-action=\"delete\"]");
        if (delBtn) return deleteNis2Phase(delBtn.dataset.phaseId);
      });
    }
    const toolbar = document.getElementById("nis2PlanToolbar");
    if (toolbar && !toolbar.dataset.bound) {
      toolbar.dataset.bound = "true";
      toolbar.addEventListener("click", (event) => {
        const viewBtn = event.target.closest("[data-view-mode]");
        if (viewBtn) {
          nis2PlanViewMode = viewBtn.dataset.viewMode || "detail";
          ensureNis2PlanToolbar();
          return renderNis2Plan();
        }
        const focusBtn = event.target.closest("#nis2PlanFocusBtn");
        if (focusBtn) {
          nis2PlanShowOpenOnly = !nis2PlanShowOpenOnly;
          ensureNis2PlanToolbar();
          return renderNis2Plan();
        }
      });
    }
    const statusList = document.getElementById("nis2StatusList");
    if (statusList) {
      statusList.addEventListener("click", (event) => {
        const btn = event.target.closest("[data-nis2-status-id]");
        if (!btn) return;
        openNis2StatusModal(btn.dataset.nis2StatusId);
      });
    }
  }
  window.renderNis2Plan = renderNis2Plan;
  window.loadNis2Program = loadNis2Program;
  window.openNis2DomainModal = openNis2DomainModal;
  window.openNis2ActionModal = openNis2ActionModal;
  window.openNis2BudgetBlockModal = openNis2BudgetBlockModal;
  function openNis2StatusModal(statusId = "") {
    ensureNis2Plan();
    const statuses = appData.nis2Plan.statuses || [];
    const status = statuses.find(s => s.id === statusId) || { text: "", status: "todo" };
    const isEdit = !!statusId;
    openModal(isEdit ? t('modals.editNis2Status') : t('modals.addNis2Status'), [
      { name: "text", label: t('fields.description'), type: "textarea", value: status.text || "" },
      { name: "status", label: t('fields.status'), type: "select", value: status.status || "todo", options: [
        { value: "todo", label: t('statusLabels.todo') },
        { value: "in_progress", label: t('statusLabels.inProgress') },
        { value: "suspended", label: t('statusLabels.suspended') },
        { value: "done", label: t('statusLabels.done') }
      ] }
    ], (data) => {
      if (!data.text) return false;
      if (isEdit) {
        const idx = statuses.findIndex(s => s.id === statusId);
        if (idx >= 0) statuses[idx] = Object.assign({}, statuses[idx], { text: data.text, status: data.status });
      } else {
        statuses.push({
          id: generateId("nis2-status"),
          text: data.text,
          status: data.status || "todo"
        });
      }
      appData.nis2Plan.statuses = statuses;
      saveData();
      renderNis2StatusList();
      return true;
    }, isEdit ? () => {
      const idx = statuses.findIndex(s => s.id === statusId);
      if (idx >= 0) {
        showConfirmModal(t('common.delete', 'Supprimer'), t('confirms.deleteKeyStatus', 'Supprimer ce statut clé ?'), function() {
          statuses.splice(idx, 1);
          saveData();
          renderNis2StatusList();
        }, { type: 'danger' });
      }
    } : null);
  }

  // modules/critical-assets.js
  function getCriticalAssetAvailabilityOptions() {
    return [
      { value: "level1", label: t('assetAvailability.level1') },
      { value: "level2", label: t('assetAvailability.level2') },
      { value: "level3", label: t('assetAvailability.level3') },
      { value: "level4", label: t('assetAvailability.level4') }
    ];
  }
  const CRITICAL_ASSET_AVAILABILITY_OPTIONS = getCriticalAssetAvailabilityOptions();
  function getCriticalAssetConfOptions() {
    return [
      { value: "sensible", label: t('assetConfidentiality.sensible') },
      { value: "tresSensible", label: t('assetConfidentiality.tresSensible') },
      { value: "restreint", label: t('assetConfidentiality.restreint') },
      { value: "public", label: t('assetConfidentiality.public') }
    ];
  }
  const CRITICAL_ASSET_CONF_OPTIONS = getCriticalAssetConfOptions();
  function getCriticalAssetIntegrityOptions() {
    return [
      { value: "faible", label: t('assetIntegrity.faible') },
      { value: "moderee", label: t('assetIntegrity.moderee') },
      { value: "elevee", label: t('assetIntegrity.elevee') },
      { value: "critique", label: t('assetIntegrity.critique') }
    ];
  }
  const CRITICAL_ASSET_INTEGRITY_OPTIONS = getCriticalAssetIntegrityOptions();
  function getCriticalAssetTypeOptions() {
    // Typologie des biens supports alignée ISO/IEC 27005 & EBIOS RM (liste à plat, sans sous-niveaux).
    return [
      { value: "hardware", label: t('assetType.hardware') },
      { value: "software", label: t('assetType.software') },
      { value: "sourceCode", label: t('assetType.sourceCode') },
      { value: "network", label: t('assetType.network') },
      { value: "service", label: t('assetType.service') },
      { value: "document", label: t('assetType.document') },
      { value: "employee", label: t('assetType.employee') },
      { value: "site", label: t('assetType.site') },
      { value: "organization", label: t('assetType.organization') }
    ];
  }
  const CRITICAL_ASSET_TYPE_OPTIONS = getCriticalAssetTypeOptions();
  function getCriticalAssetImportanceOptions() {
    return [
      { value: "normal", label: t('assetImportance.normal') },
      { value: "important", label: t('assetImportance.important') },
      { value: "critique", label: t('assetImportance.critique') }
    ];
  }
  const CRITICAL_ASSET_IMPORTANCE_OPTIONS = getCriticalAssetImportanceOptions();
  function getCriticalAssetImpactOptions() {
    return [
      { value: "societal", label: t('assetImpact.societal') },
      { value: "operationnel", label: t('assetImpact.operationnel') },
      { value: "financier", label: t('assetImpact.financier') },
      { value: "reputation", label: t('assetImpact.reputation') },
      { value: "reglementaire", label: t('assetImpact.reglementaire') }
    ];
  }
  const CRITICAL_ASSET_IMPACT_OPTIONS = getCriticalAssetImpactOptions();
  function getCriticalAssetMfaOptions() {
    return [
      { value: "nonImplemente", label: t('assetMfa.nonImplemente') },
      { value: "partiel", label: t('assetMfa.partiel') },
      { value: "implemente", label: t('assetMfa.implemente') },
      { value: "externe", label: t('assetMfa.externe') }
    ];
  }
  const CRITICAL_ASSET_MFA_STATUS_OPTIONS = getCriticalAssetMfaOptions();
  function getCriticalAssetDacpOptions() {
    return [
      { value: "oui", label: t('assetDacp.oui') },
      { value: "non", label: t('assetDacp.non') }
    ];
  }
  const CRITICAL_ASSET_DACP_OPTIONS = getCriticalAssetDacpOptions();
  // Résout une valeur d'actif stockée (clé énum 'software' OU libellé 'Logiciel') vers son
  // libellé d'affichage. RÉTRO-COMPATIBLE : si la valeur n'est pas une clé connue (ex. libellés
  // des données de démo, ou valeur libre), elle est renvoyée inchangée. Utilisé au rendu ET aux
  // exports pour la fidélité (un actif édité au modal stocke la clé, il faut afficher le libellé).
  function resolveCriticalAssetLabel(value, options) {
    if (!value || !Array.isArray(options)) return value;
    for (var _rcaI = 0; _rcaI < options.length; _rcaI++) {
      if (options[_rcaI] && options[_rcaI].value === value) return options[_rcaI].label;
    }
    return value;
  }
  function getDefaultCriticalAssetsMeta() {
    return {
      sectionTitle: "Actifs critiques",
      tableTitle: "Actifs critiques",
      subtitle: "Inventaire des actifs critiques et exigences de continuité.",
      metaLine: ""
    };
  }
  function normalizeCriticalAssetsMeta(meta) {
    const defaults = getDefaultCriticalAssetsMeta();
    const clean = { ...defaults };
    if (meta && typeof meta === "object") {
      ["sectionTitle", "tableTitle", "subtitle", "metaLine"].forEach((key) => {
        if (typeof meta[key] === "string") {
          const trimmed = meta[key].trim();
          if (key === "sectionTitle" || key === "tableTitle") {
            clean[key] = trimmed || defaults[key];
          } else {
            clean[key] = trimmed;
          }
        }
      });
    }
    return clean;
  }
  function normalizeCriticalAssetEntry(entry = {}, index = 0) {
    const clean = typeof entry === "object" && entry ? { ...entry } : {};
    if (!clean.id) clean.id = generateId("asset");
    const priority = parseInt(clean.priority, 10);
    clean.priority = Number.isFinite(priority) && priority > 0 ? priority : index + 1;
    const toText = (value) => typeof value === "string" ? value.trim() : "";
    clean.beneficiary = toText(clean.beneficiary);
    clean.productCode = toText(clean.productCode);
    clean.productName = toText(clean.productName);
    if (Array.isArray(clean.productTags)) {
      clean.productTags = clean.productTags.map((tag) => String(tag).trim()).filter(Boolean);
    } else if (typeof clean.productTags === "string") {
      clean.productTags = clean.productTags.split(",").map((tag) => tag.trim()).filter(Boolean);
    } else {
      clean.productTags = [];
    }
    clean.rto = toText(clean.rto);
    clean.rpo = toText(clean.rpo);
    clean.availability = toText(clean.availability);
    clean.availabilityNote = toText(clean.availabilityNote);
    clean.confidentiality = toText(clean.confidentiality);
    clean.integrity = toText(clean.integrity);
    clean.assetType = toText(clean.assetType);
    clean.importance = toText(clean.importance);
    clean.impact = toText(clean.impact);
    const dacpRaw = clean.dacpSensitive;
    if (typeof dacpRaw === "boolean") {
      clean.dacpSensitive = dacpRaw ? "oui" : "non";
    } else {
      const dacpText = toText(dacpRaw);
      clean.dacpSensitive = (dacpText || "non").toLowerCase() === "oui" ? "oui" : "non";
    }
    clean.mfaStatus = toText(clean.mfaStatus);
    clean.mfaDetail = toText(clean.mfaDetail);
    clean.comments = Array.isArray(clean.comments) ? clean.comments : [];
    return clean;
  }
  function normalizeCriticalAssets(list) {
    if (!Array.isArray(list)) return [];
    return list.map((entry, index) => normalizeCriticalAssetEntry(entry, index));
  }
  function normalizeCriticalAssetLinks(list) {
    if (!Array.isArray(list)) return;
    list.forEach((item) => {
      if (!item) return;
      if (Array.isArray(item.criticalAssetIds)) {
        item.criticalAssetIds = item.criticalAssetIds.map((id) => String(id || "").trim()).filter(Boolean);
      } else if (typeof item.criticalAssetIds === "string") {
        item.criticalAssetIds = item.criticalAssetIds.split(",").map((id) => id.trim()).filter(Boolean);
      } else {
        item.criticalAssetIds = [];
      }
    });
  }
  function ensureCriticalAssets() {
    appData.criticalAssets = normalizeCriticalAssets(appData.criticalAssets || []);
    appData.criticalAssetsMeta = normalizeCriticalAssetsMeta(appData.criticalAssetsMeta);
  }
  function getCriticalAssetLabel(asset) {
    if (!asset) return "";
    const code = asset.productCode ? asset.productCode.trim() : "";
    const name = asset.productName ? asset.productName.trim() : "";
    const beneficiary = asset.beneficiary ? asset.beneficiary.trim() : "";
    const parts = [];
    if (code) parts.push(code);
    if (name) parts.push(name);
    if (parts.length) return parts.join(" — ");
    if (beneficiary) return beneficiary;
    return asset.id || "";
  }
  function getCriticalAssetLookup() {
    return new Map((appData.criticalAssets || []).map((asset) => [asset.id, asset]));
  }
  function getCriticalAssetOptions(selectedIds = []) {
    const options = (appData.criticalAssets || []).map((asset) => ({
      value: asset.id,
      label: getCriticalAssetLabel(asset)
    }));
    const existing = new Set(options.map((opt) => opt.value));
    (Array.isArray(selectedIds) ? selectedIds : []).forEach((id) => {
      const clean = String(id || "").trim();
      if (clean && !existing.has(clean)) {
        options.push({ value: clean, label: clean });
        existing.add(clean);
      }
    });
    return options;
  }
  function buildCriticalAssetBadges(assetIds) {
    const ids = Array.isArray(assetIds) ? assetIds.filter(Boolean) : [];
    if (!ids.length) return "—";
    const lookup = getCriticalAssetLookup();
    return ids.map((id) => {
      const asset = lookup.get(id);
      const label = asset ? getCriticalAssetLabel(asset) : id;
      return `<a href="#" class="link-badge link-badge--asset critical-asset-link" data-asset-id="${escapeHtml(id)}" title="${t('criticalAssets.openAsset', 'Voir l\'actif')}">${escapeHtml(label || id)}</a>`;
    }).join(" ");
  }
  function attachCriticalAssetLinkHandlers(container) {
    if (!container) return;
    container.querySelectorAll(".critical-asset-link").forEach((link) => {
      link.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        const assetId = link.dataset.assetId;
        switchModule("critical-assets");
        setTimeout(() => {
          openCriticalAssetModal(assetId);
        }, 200);
      });
    });
  }
  function renderCriticalAssetsMeta() {
    ensureCriticalAssets();
    const meta = appData.criticalAssetsMeta || getDefaultCriticalAssetsMeta();
    safeSetText("criticalAssetsSectionTitle", meta.sectionTitle || "Actifs critiques");
    safeSetText("criticalAssetsTableTitle", meta.tableTitle || "Actifs critiques");
    safeSetText("criticalAssetsSectionSubtitle", meta.subtitle || "");
    const metaLine = document.getElementById("criticalAssetsMetaLine");
    if (metaLine) metaLine.textContent = meta.metaLine || "";
  }
  function updateCriticalAssetsFilterOptions() {
    const beneficiaries = new Set();
    const availability = new Set();
    const impacts = new Set();
    (appData.criticalAssets || []).forEach((asset) => {
      if (asset.beneficiary) beneficiaries.add(asset.beneficiary);
      if (asset.availability) availability.add(asset.availability);
      if (asset.impact) impacts.add(asset.impact);
    });
    var toOpts = function(values) {
      return Array.from(values).sort(SortUtils.text).map(function(v) { return { value: v, label: v }; });
    };
    SectionToolbar.updateFilterOptions("critical-assets", "beneficiary", toOpts(beneficiaries));
    SectionToolbar.updateFilterOptions("critical-assets", "availability", toOpts(availability));
    SectionToolbar.updateFilterOptions("critical-assets", "impact", toOpts(impacts));
  }
  function resetCriticalAssetsFilters() {
    if (typeof SectionToolbar !== 'undefined') {
      SectionToolbar.init("critical-assets", {
        filters: [
          { key: "beneficiary", label: "B\u00e9n\u00e9ficiaire", options: [] },
          { key: "availability", label: "Disponibilit\u00e9", options: [] },
          { key: "impact", label: "Impact", options: [] }
        ],
        sort: {
          options: [
            { key: "priority", label: "Priorit\u00e9", default: true, dir: "asc" },
            { key: "name", label: "Nom" }
          ]
        },
        onUpdate: function() { loadCriticalAssetsTable(); }
      });
    }
    loadCriticalAssetsTable();
  }
  function getAssetTagClass(tag) {
    const lower = String(tag || "").toLowerCase();
    if (lower.includes("obsol")) return "asset-tag asset-tag--alert";
    return "asset-tag";
  }
  function loadCriticalAssetsTable() {
    const tbody = document.getElementById("criticalAssetsTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('critical-assets')) return;
    ensureCriticalAssets();
    renderCriticalAssetsMeta();
    updateCriticalAssetsFilterOptions();
    const _f = SectionToolbar.getFilters("critical-assets");
    const _q = (SectionToolbar.getSearch("critical-assets") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const beneficiaryFilter = _f.beneficiary || "";
    const availabilityFilter = _f.availability || "";
    const impactFilter = _f.impact || "";
    const filtered = (appData.criticalAssets || []).filter((asset) => {
      if (beneficiaryFilter && asset.beneficiary !== beneficiaryFilter) return false;
      if (availabilityFilter && asset.availability !== availabilityFilter) return false;
      if (impactFilter && asset.impact !== impactFilter) return false;
      if (_q && ((asset.productName || "") + " " + (asset.productCode || "") + " " + (asset.beneficiary || "")).toLowerCase().indexOf(_q) < 0) return false;
      return true;
    });
    const _s = SectionToolbar.getSort("critical-assets");
    const _sortKey = _s ? _s.key : "priority";
    const _sortDir = _s ? _s.dir : "asc";
    const sorted = filtered.slice().sort((a, b) => {
      let diff = _sortKey === "name" ? SortUtils.text(a.productName, b.productName) : SortUtils.numeric(a.priority, b.priority);
      return _sortDir === "desc" ? -diff : diff;
    });
    tbody.innerHTML = "";
    if (!sorted.length) {
      const row = document.createElement("tr");
      row.innerHTML = `<td colspan="9"><div class="nis2-empty">Aucun actif critique.</div></td>`;
      tbody.appendChild(row);
      return;
    }
    sorted.forEach((asset, index) => {
      const row = document.createElement("tr");
      const tagsHtml = (asset.productTags || []).map((tag) => `<span class="${getAssetTagClass(tag)}">${escapeHtml(tag)}</span>`).join(" ");
      const productHtml = `
        <div class="asset-product">
          ${tagsHtml ? `<div class="asset-product__tags">${tagsHtml}</div>` : ""}
          <div class="asset-product__code">${escapeHtml(asset.productCode || "")}</div>
          <div class="asset-product__name">${escapeHtml(asset.productName || "")}</div>
        </div>
      `;
      // Lot 1a (refonte UX) : availabilityNote n'est plus affichée en liste (reste éditable en modale)
      const availabilityHtml = `
        <div>${escapeHtml(resolveCriticalAssetLabel(asset.availability, CRITICAL_ASSET_AVAILABILITY_OPTIONS) || "")}</div>
      `;
      const assetTypeHtml = asset.assetType ? `<span class="asset-tag">${escapeHtml(resolveCriticalAssetLabel(asset.assetType, CRITICAL_ASSET_TYPE_OPTIONS))}</span>` : "—";
      const impactHtml = asset.impact ? `<span class="asset-tag asset-tag--impact">${escapeHtml(resolveCriticalAssetLabel(asset.impact, CRITICAL_ASSET_IMPACT_OPTIONS))}</span>` : "—";
      const dacpValue = resolveCriticalAssetLabel(asset.dacpSensitive, CRITICAL_ASSET_DACP_OPTIONS) || "";
      // FIX-DACP-BADGE (audit 2026-06-10) : normalizeCriticalAssetEntry stocke toujours
      // 'oui'/'non' en minuscules — la comparaison stricte à "Oui" n'était jamais vraie.
      // Comparaison insensible à la casse, valeurs FR ('oui') et EN ('yes') acceptées.
      const _dacpNorm = String(dacpValue).trim().toLowerCase();
      const dacpClass = (_dacpNorm === "oui" || _dacpNorm === "yes") ? "asset-tag asset-tag--yes" : "asset-tag asset-tag--no";
      const dacpHtml = dacpValue ? `<span class="${dacpClass}">${escapeHtml(dacpValue)}</span>` : "—";
      const mfaStatus = resolveCriticalAssetLabel(asset.mfaStatus, CRITICAL_ASSET_MFA_STATUS_OPTIONS) || "";
      let mfaStatusClass = "asset-tag";
      if (mfaStatus) {
        // La couleur du badge se calcule sur la valeur BRUTE stockée (clé 'partiel' /
        // 'nonImplemente', neutre en langue) — PAS sur le libellé résolu, sinon en anglais
        // ('Partial' / 'Not implemented') la comparaison sur sous-chaînes FR échouerait.
        const lower = String(asset.mfaStatus || "").toLowerCase();
        mfaStatusClass = lower.includes("non") || lower.includes("partiel") ? "asset-tag asset-tag--mfa" : "asset-tag asset-tag--yes";
      }
      // Lot 1a (refonte UX) : mfaDetail n'est plus affiché en liste (reste éditable en modale)
      const mfaStatusHtml = mfaStatus ? `<span class="${mfaStatusClass}">${escapeHtml(mfaStatus)}</span>` : "";
      const mfaHtml = mfaStatusHtml
        ? `<div class="asset-mfa">${mfaStatusHtml}</div>`
        : "—";
      // Lot 1a (refonte UX) : RTO et RPO fusionnés en une colonne (deux pills empilées)
      const rtoRpoHtml = (asset.rto || asset.rpo)
        ? `<div class="asset-rto-rpo">${asset.rto ? `<span class="asset-pill">${escapeHtml(asset.rto)}</span>` : "—"} / ${asset.rpo ? `<span class="asset-pill">${escapeHtml(asset.rpo)}</span>` : "—"}</div>`
        : "—";
      row.innerHTML = `
        <td><span class="asset-priority">${escapeHtml(asset.priority || index + 1)}</span></td>
        <td>${escapeHtml(asset.beneficiary || "")}</td>
        <td>${productHtml}</td>
        <td>${assetTypeHtml}</td>
        <td>${rtoRpoHtml}</td>
        <td>${availabilityHtml}</td>
        <td>${impactHtml}</td>
        <td>${dacpHtml}</td>
        <td>${mfaHtml}</td>
      `;
      row.classList.add("row-clickable");
      row.addEventListener("click", () => {
        openCriticalAssetModal(asset.id);
      });
      tbody.appendChild(row);
    });
  }
  function openCriticalAssetsTitlesModal() {
    ensureCriticalAssets();
    const meta = appData.criticalAssetsMeta || getDefaultCriticalAssetsMeta();
    openModal(t('modals.renameSection'), [
      { name: "sectionTitle", label: "Titre de la section", value: meta.sectionTitle },
      { name: "tableTitle", label: "Titre du tableau", value: meta.tableTitle },
      { name: "subtitle", label: "Sous-titre", value: meta.subtitle },
      { name: "metaLine", label: "Ligne d'information", value: meta.metaLine }
    ], (data) => {
      appData.criticalAssetsMeta = normalizeCriticalAssetsMeta(data);
      saveData();
      renderCriticalAssetsMeta();
      return true;
    });
  }
  function ensureSelectOptions(options, value) {
    if (!value) return options;
    return options.some((opt) => opt.value === value)
      ? options
      : [...options, { value, label: value }];
  }
  function openCriticalAssetModal(assetId = "") {
    ensureCriticalAssets();
    const asset = (appData.criticalAssets || []).find((a) => a.id === assetId);
    const isEdit = !!asset;
    const current = asset || normalizeCriticalAssetEntry({}, (appData.criticalAssets || []).length);
    const availabilityOptions = ensureSelectOptions(CRITICAL_ASSET_AVAILABILITY_OPTIONS, current.availability);
    const confOptions = ensureSelectOptions(CRITICAL_ASSET_CONF_OPTIONS, current.confidentiality);
    const integrityOptions = ensureSelectOptions(CRITICAL_ASSET_INTEGRITY_OPTIONS, current.integrity);
    const assetTypeOptions = ensureSelectOptions(CRITICAL_ASSET_TYPE_OPTIONS, current.assetType);
    const importanceOptions = ensureSelectOptions(CRITICAL_ASSET_IMPORTANCE_OPTIONS, current.importance);
    const impactOptions = ensureSelectOptions(CRITICAL_ASSET_IMPACT_OPTIONS, current.impact);
    const mfaOptions = ensureSelectOptions(CRITICAL_ASSET_MFA_STATUS_OPTIONS, current.mfaStatus);
    const dacpOptions = ensureSelectOptions(CRITICAL_ASSET_DACP_OPTIONS, current.dacpSensitive);
    openModal(isEdit ? t('modals.editCriticalAsset') : t('modals.addCriticalAsset'), [
      { name: "priority", label: t('criticalAssets.form.priority', 'Priorité'), type: "number", value: current.priority },
      { name: "beneficiary", label: t('criticalAssets.form.beneficiary', 'Bénéficiaire'), value: current.beneficiary },
      { name: "productCode", label: t('criticalAssets.form.productCode', 'Produit (code)'), value: current.productCode },
      { name: "productName", label: t('criticalAssets.form.productName', 'Produit (nom)'), value: current.productName },
      { name: "productTags", label: t('criticalAssets.form.productTags', 'Tags produit (séparés par des virgules)'), value: (current.productTags || []).join(", ") },
      { name: "assetType", label: t('fields.assetType'), type: "select", value: current.assetType, options: assetTypeOptions },
      { name: "importance", label: t('fields.importance'), type: "select", value: current.importance, options: importanceOptions },
      { name: "rto", label: "RTO", value: current.rto },
      { name: "rpo", label: "RPO", value: current.rpo },
      { name: "availability", label: t('criticalAssets.form.availability', 'Disponibilité'), type: "select", value: current.availability, options: availabilityOptions },
      { name: "availabilityNote", label: t('criticalAssets.form.availabilityNote', 'Note disponibilité'), value: current.availabilityNote },
      { name: "confidentiality", label: t('criticalAssets.form.confidentiality', 'Confidentialité'), type: "select", value: current.confidentiality, options: confOptions },
      { name: "integrity", label: t('fields.integrity'), type: "select", value: current.integrity, options: integrityOptions },
      { name: "impact", label: t('fields.impact'), type: "select", value: current.impact, options: impactOptions },
      { name: "dacpSensitive", label: t('criticalAssets.form.dacpSensitive', 'DACP sensible'), type: "select", value: current.dacpSensitive, options: dacpOptions },
      { name: "mfaStatus", label: t('criticalAssets.form.mfaStatus', 'Obligation MFA'), type: "select", value: current.mfaStatus, options: mfaOptions },
      { name: "mfaDetail", label: t('criticalAssets.form.mfaDetail', 'Détail MFA'), value: current.mfaDetail }
    ], (data) => {
      const clean = normalizeCriticalAssetEntry({
        id: current.id,
        priority: data.priority || current.priority,
        beneficiary: data.beneficiary,
        productCode: data.productCode,
        productName: data.productName,
        productTags: data.productTags,
        rto: data.rto,
        rpo: data.rpo,
        availability: data.availability,
        availabilityNote: data.availabilityNote,
        confidentiality: data.confidentiality,
        integrity: data.integrity,
        assetType: data.assetType,
        importance: data.importance,
        impact: data.impact,
        dacpSensitive: data.dacpSensitive,
        mfaStatus: data.mfaStatus,
        mfaDetail: data.mfaDetail
      }, 0);
      if (isEdit) {
        const idx = appData.criticalAssets.findIndex((a) => a.id === assetId);
        if (idx >= 0) appData.criticalAssets[idx] = { ...appData.criticalAssets[idx], ...clean };
      } else {
        appData.criticalAssets.push(clean);
      }
      ensureCriticalAssets();
            addAuditEntry(isEdit ? 'edit' : 'add', 'criticalAssets', clean.productName);
      saveData();
      loadCriticalAssetsTable();
      loadActionsTable();
      loadRisksTable();
      loadThreatsTable();
      loadNonConformitiesTable();
      return true;
    }, isEdit ? () => deleteCriticalAsset(assetId) : null);
  }
  function deleteCriticalAsset(assetId) {
    showConfirmModal(t('confirms.deleteAsset'), t('confirms.deleteAsset'), function() {
      var _asset = (appData.criticalAssets || []).find(function(a) { return a.id === assetId; });
      appData.criticalAssets = (appData.criticalAssets || []).filter((asset) => asset.id !== assetId);
      if (Array.isArray(appData.actions)) {
        appData.actions.forEach((action) => {
          action.criticalAssetIds = (action.criticalAssetIds || []).filter((id) => id !== assetId);
        });
      }
      if (Array.isArray(appData.risks)) {
        appData.risks.forEach((risk) => {
          risk.criticalAssetIds = (risk.criticalAssetIds || []).filter((id) => id !== assetId);
        });
      }
      if (Array.isArray(appData.threats)) {
        appData.threats.forEach((threat) => {
          threat.criticalAssetIds = (threat.criticalAssetIds || []).filter((id) => id !== assetId);
        });
      }
      if (Array.isArray(appData.nonconformities)) {
        appData.nonconformities.forEach((nc) => {
          nc.criticalAssetIds = (nc.criticalAssetIds || []).filter((id) => id !== assetId);
        });
      }
      ensureCriticalAssets();
            addAuditEntry('delete', 'criticalAssets', _asset ? _asset.productName : assetId);
      saveData();
      _csiPersistAffectedTabs(['criticalAssets', 'actions', 'risks', 'threats', 'nonconformities']); // FIX-CASCADE-PERSIST : persiste les sections croisées nettoyées
      loadCriticalAssetsTable();
      loadActionsTable();
      loadRisksTable();
      loadThreatsTable();
      loadNonConformitiesTable();
      if (typeof updateDashboard === 'function') updateDashboard();
      showToast2(t('toasts.itemDeleted'), "success");
    }, { type: 'danger' });
  }
  window.loadCriticalAssetsTable = loadCriticalAssetsTable;
  window.openCriticalAssetModal = openCriticalAssetModal;

  // modules/project-risks.js
  function getProjectRiskStatusOptions() {
    return [
      { value: "red", label: t('projectRiskStatuses.red') },
      { value: "yellow", label: t('projectRiskStatuses.yellow') },
      { value: "green", label: t('projectRiskStatuses.green') },
      { value: "todo", label: t('projectRiskStatuses.todo') },
      { value: "suspended", label: t('projectRiskStatuses.suspended') }
    ];
  }
  let PROJECT_RISK_STATUS_OPTIONS = getProjectRiskStatusOptions();
  function getProjectRiskStatusLabels() {
    return {
      red: t('projectRiskStatuses.red'),
      yellow: t('projectRiskStatuses.yellow'),
      green: t('projectRiskStatuses.green'),
      todo: t('projectRiskStatuses.todo'),
      suspended: t('projectRiskStatuses.suspended')
    };
  }
  let PROJECT_RISK_STATUS_LABELS = getProjectRiskStatusLabels();
  const PROJECT_RISK_STATUS_BADGES = {
    red: "status-badge--rouge",
    yellow: "status-badge--jaune",
    green: "status-badge--vert",
    todo: "status-badge--a_realiser",
    suspended: "status-badge--suspendue"
  };
  function normalizeProjectRiskStatus(status) {
    const raw = NORMALIZE(String(status || "").trim()).replace(/[_-]+/g, " ");
    if (["rouge", "red"].includes(raw)) return "red";
    if (["jaune", "yellow"].includes(raw)) return "yellow";
    if (["vert", "green"].includes(raw)) return "green";
    if (["a realiser", "todo"].includes(raw)) return "todo";
    if (["suspendue", "suspendu", "suspended"].includes(raw)) return "suspended";
    return "todo";
  }
  function normalizeProjectRiskRecommendations(list) {
    if (!Array.isArray(list)) return [];
    return list.map((rec) => {
      const clean = typeof rec === "object" && rec ? { ...rec } : {};
      if (!clean.id) clean.id = generateId("proj-reco");
      clean.title = typeof clean.title === "string" ? clean.title.trim() : "";
      clean.text = typeof clean.text === "string" ? clean.text.trim() : "";
      clean.implemented = !!clean.implemented;
      return clean;
    }).filter((rec) => rec.title || rec.text);
  }
  function normalizeProjectRiskEntry(entry = {}, index = 0) {
    const clean = typeof entry === "object" && entry ? { ...entry } : {};
    if (!clean.id) clean.id = generateId("proj-risk");
    const toText = (value) => typeof value === "string" ? value.trim() : "";
    clean.projectName = toText(clean.projectName);
    clean.beneficiary = toText(clean.beneficiary);
    clean.reportNumber = toText(clean.reportNumber);
    const revision = parseInt(clean.revisionNumber, 10);
    clean.revisionNumber = Number.isFinite(revision) ? revision : 0;
    clean.description = toText(clean.description);
    clean.analysisLink = toText(clean.analysisLink);
    clean.completionDate = toText(clean.completionDate);
    clean.status = normalizeProjectRiskStatus(clean.status);
    clean.recommendations = normalizeProjectRiskRecommendations(clean.recommendations);
    clean.sourceAnalysisId = clean.sourceAnalysisId || null;
    // Ignorer les tags importés — on ne conserve que le champ priority
    delete clean.tags;
    // AUDIT-H3: Migrer les priorités legacy (française) avant validation.
    // Avant: sans le module legacy-import, les priorités 'haute'/'normale'/'basse'/'critique'
    // n'étaient pas dans la whitelist et devenaient '' (chaîne vide) → perte de données.
    var _prioMigration = { 'normale': 'medium', 'haute': 'high', 'basse': 'low', 'critique': 'critical' };
    if (clean.priority && _prioMigration[clean.priority]) {
      clean.priority = _prioMigration[clean.priority];
    }
    var validPriorities = ['high', 'medium', 'low', 'critical'];
    clean.priority = validPriorities.indexOf(clean.priority) >= 0 ? clean.priority : '';
    clean.note = typeof clean.note === 'string' ? clean.note.trim() : '';
    clean.comments = Array.isArray(clean.comments) ? clean.comments : [];
    return clean;
  }
  function normalizeProjectRisks(list) {
    if (!Array.isArray(list)) return [];
    return list.map((entry, index) => normalizeProjectRiskEntry(entry, index));
  }
  function ensureProjectRisks() {
    appData.projectRisks = normalizeProjectRisks(appData.projectRisks || []);
  }
  function updateProjectRiskFilterOptions() {
    // Toolbar v2 : le filtre Bénéficiaire (dynamique) vit dans la SectionToolbar.
    if (typeof SectionToolbar === 'undefined' || !SectionToolbar.updateFilterOptions) return;
    const beneficiaries = new Set();
    (appData.projectRisks || []).forEach((risk) => {
      if (risk.beneficiary) beneficiaries.add(risk.beneficiary);
    });
    const options = Array.from(beneficiaries).sort(SortUtils.text).map((value) => ({ value, label: value }));
    SectionToolbar.updateFilterOptions("project-risks", "beneficiary", options);
  }
  var PROJECT_RISK_STATUS_ICONS = { green: 'fa-check', yellow: 'fa-exclamation', red: 'fa-xmark', todo: 'fa-circle', suspended: 'fa-pause' };
  function getProjectRiskStatusMeta(status) {
    const key = normalizeProjectRiskStatus(status);
    return {
      key,
      label: PROJECT_RISK_STATUS_LABELS[key] || key,
      badge: PROJECT_RISK_STATUS_BADGES[key] || "status-badge",
      icon: PROJECT_RISK_STATUS_ICONS[key] || 'fa-circle'
    };
  }
  function buildProjectRiskRecommendationRow(rec = {}, options = {}) {
    const row = document.createElement("div");
    row.className = "project-risk-reco";
    row.dataset.recoId = rec.id || generateId("proj-reco");
    row.innerHTML = `
      <div class="project-risk-reco__row">
        <label class="modal__label">${t('projectRisks.recoTitleLabel')}</label>
        <input type="text" class="form-control project-risk-reco__title" value="${escapeHtml(rec.title || "")}" data-readonly-lock="true">
      </div>
      <div class="project-risk-reco__row">
        <label class="modal__label">${t('projectRisks.recoTextLabel')}</label>
        <textarea class="form-control project-risk-reco__text" data-readonly-lock="true">${escapeHtml(rec.text || "")}</textarea>
      </div>
      <div class="project-risk-reco__meta">
        <label class="project-risk-reco__check">
          <input type="checkbox" class="project-risk-reco__implemented" ${rec.implemented ? "checked" : ""} data-readonly-lock="true">
          ${t('projectRisks.recoImplemented')}
        </label>
        <button type="button" class="btn btn--outline btn--sm project-risk-reco__remove" data-readonly-lock="true">
          <i class="fas fa-trash"></i> ${t('projectRisks.recoRemove')}
        </button>
      </div>
    `;
    const removeBtn = row.querySelector(".project-risk-reco__remove");
    if (options.readOnly && removeBtn) {
      removeBtn.disabled = true;
    } else if (removeBtn) {
      removeBtn.addEventListener("click", () => {
        row.remove();
        if (typeof options.onRemove === "function") options.onRemove();
      });
    }
    return row;
  }
  function renderProjectRiskRecommendations(container, recommendations = []) {
    const section = document.createElement("div");
    section.className = "project-risk-recos";
    section.innerHTML = `
      <div class="project-risk-recos__header">
        <h4 class="project-risk-recos__title">${t('projectRisks.recoTitle')}</h4>
        <button type="button" class="btn btn--outline btn--sm project-risk-recos__add" data-readonly-lock="true">
          <i class="fas fa-plus"></i> ${t('projectRisks.recoAdd')}
        </button>
      </div>
      <div class="project-risk-recos__list"></div>
      <p class="project-risk-recos__empty">${t('projectRisks.recoEmpty')}</p>
    `;
    const list = section.querySelector(".project-risk-recos__list");
    const empty = section.querySelector(".project-risk-recos__empty");
    const addBtn = section.querySelector(".project-risk-recos__add");
    const readOnly = typeof isReadOnlyMode === "function" && isReadOnlyMode();
    const updateEmpty = () => {
      if (empty) empty.hidden = list.children.length > 0;
    };
    const addRow = (rec) => {
      list.appendChild(buildProjectRiskRecommendationRow(rec, { readOnly, onRemove: updateEmpty }));
      updateEmpty();
    };
    recommendations.forEach(addRow);
    updateEmpty();
    if (addBtn) {
      if (readOnly) {
        addBtn.disabled = true;
      } else {
        addBtn.addEventListener("click", () => addRow({}));
      }
    }
    container.appendChild(section);
    refreshReadOnlyLocks();
    return section;
  }
  function collectProjectRiskRecommendations(section) {
    if (!section) return [];
    const rows = section.querySelectorAll(".project-risk-reco");
    const recommendations = [];
    rows.forEach((row) => {
      const title = row.querySelector(".project-risk-reco__title")?.value.trim() || "";
      const text = row.querySelector(".project-risk-reco__text")?.value.trim() || "";
      const implemented = !!row.querySelector(".project-risk-reco__implemented")?.checked;
      if (!title && !text) return;
      recommendations.push({
        id: row.dataset.recoId || generateId("proj-reco"),
        title,
        text,
        implemented
      });
    });
    return recommendations;
  }
  function updateProjectRisksKpi() {
    const risks = appData.projectRisks || [];
    const total = risks.length;
    // Completed = any analysis with a traffic light (green, yellow, red)
    const completedStatuses = ["green", "yellow", "red"];
    const done = risks.filter(r => completedStatuses.includes(r.status)).length;
    // Per-status counts
    const statusCounts = { red: 0, yellow: 0, green: 0, todo: 0, suspended: 0 };
    risks.forEach(r => {
      const s = r.status || "todo";
      if (statusCounts.hasOwnProperty(s)) statusCounts[s]++;
    });
    let totalRecos = 0, implRecos = 0;
    risks.forEach(r => {
      (r.recommendations || []).forEach(rec => {
        totalRecos++;
        if (rec.implemented) implRecos++;
      });
    });
    const analysesDoneEl = document.getElementById("prKpiAnalysesDone");
    const analysesTotalEl = document.getElementById("prKpiAnalysesTotal");
    const analysesBar = document.getElementById("prKpiAnalysesBar");
    if (analysesDoneEl) analysesDoneEl.textContent = done;
    if (analysesTotalEl) analysesTotalEl.textContent = total;
    if (analysesBar) analysesBar.style.width = total ? `${(done / total * 100)}%` : "0%";
    const recosImplEl = document.getElementById("prKpiRecosImpl");
    const recosTotalEl = document.getElementById("prKpiRecosTotal");
    const recosBar = document.getElementById("prKpiRecosBar");
    if (recosImplEl) recosImplEl.textContent = implRecos;
    if (recosTotalEl) recosTotalEl.textContent = totalRecos;
    if (recosBar) recosBar.style.width = totalRecos ? `${(implRecos / totalRecos * 100)}%` : "0%";
    // Status breakdown badges
    const breakdownEl = document.getElementById("prKpiStatusBreakdown");
    if (breakdownEl) {
      const statusConfig = [
        { key: "green",     icon: "fa-check",        cssClass: "pr-status-count--green" },
        { key: "yellow",    icon: "fa-exclamation",  cssClass: "pr-status-count--yellow" },
        { key: "red",       icon: "fa-xmark",        cssClass: "pr-status-count--red" },
        { key: "todo",      icon: "fa-circle",       cssClass: "pr-status-count--todo" },
        { key: "suspended", icon: "fa-pause",        cssClass: "pr-status-count--suspended" }
      ];
      breakdownEl.innerHTML = statusConfig
        .filter(s => statusCounts[s.key] > 0)
        .map(s => {
          const label = t(`projectRiskStatuses.${s.key}`);
          return `<span class="pr-status-count ${s.cssClass}" title="${label}"><i class="fas ${s.icon}"></i> ${statusCounts[s.key]}</span>`;
        }).join("");
    }
  }
  var _prClickDelegate = null;
  function loadProjectRisksTable() {
    const tbody = document.getElementById("projectRisksTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('project-risks')) return;
    // Remove previous delegated click handler to avoid accumulation
    if (_prClickDelegate) {
      tbody.removeEventListener("click", _prClickDelegate);
      _prClickDelegate = null;
    }
    ensureProjectRisks();
    updateProjectRiskFilterOptions();
    tbody.innerHTML = "";
    // Toolbar v2 : recherche + filtres lus depuis la SectionToolbar ('' = tous)
    const _prf = SectionToolbar.getFilters("project-risks");
    const searchQuery = NORMALIZE((SectionToolbar.getSearch("project-risks") || "").trim());
    const beneficiaryFilter = _prf.beneficiary || "";
    const statusFilter = _prf.status || "";
    const pendingOnly = _prf.pending === "pending";
    const filtered = (appData.projectRisks || []).filter((risk) => {
      if (beneficiaryFilter && risk.beneficiary !== beneficiaryFilter) return false;
      if (statusFilter && normalizeProjectRiskStatus(risk.status) !== statusFilter) return false;
      if (pendingOnly) {
        const recs = risk.recommendations || [];
        if (!recs.some((rec) => !rec.implemented)) return false;
      }
      if (searchQuery) {
        const haystack = NORMALIZE([
          risk.projectName,
          risk.beneficiary,
          risk.reportNumber,
          risk.description,
          risk.analysisLink,
          risk.completionDate,
          risk.priority ? t('projectRisks.priority' + risk.priority.charAt(0).toUpperCase() + risk.priority.slice(1)) : '',
          risk.note
        ].filter(Boolean).join(" "));
        if (!haystack.includes(searchQuery)) return false;
      }
      return true;
    });
    const _stb = SectionToolbar.getSort("project-risks");
    const _prSortDir = _stb ? _stb.dir : "asc";
    filtered.sort((a, b) => {
      let diff = 0;
      const _sk = _stb ? _stb.key : "name";
      switch (_sk) {
        case "level":
        case "priority": {
          var pOrder = { high: 0, medium: 1, low: 2 };
          diff = SortUtils.orderedMap(pOrder)(a.priority, b.priority);
          break;
        }
        case "title":
        case "name":
          diff = SortUtils.text(a.projectName, b.projectName); break;
        case "beneficiary":
          diff = SortUtils.text(a.beneficiary, b.beneficiary); break;
        case "status": {
          const order = { red: 0, yellow: 1, todo: 2, suspended: 3, green: 4 };
          diff = SortUtils.orderedMap(order)(normalizeProjectRiskStatus(a.status), normalizeProjectRiskStatus(b.status));
          break;
        }
        case "date":
          diff = SortUtils.date(a.completionDate, b.completionDate); break;
        default:
          diff = SortUtils.text(a.projectName, b.projectName); break;
      }
      return _prSortDir === "desc" ? -diff : diff;
    });
    if (!filtered.length) {
      const row = document.createElement("tr");
      const message = appData.projectRisks.length
        ? t('projectRisks.noResults')
        : t('projectRisks.noData');
      row.innerHTML = `<td colspan="7"><div class="nis2-empty">${message}</div></td>`;
      tbody.appendChild(row);
      updateProjectRisksKpi();
      return;
    }
    filtered.forEach((risk) => {
      const row = document.createElement("tr");
      const statusMeta = getProjectRiskStatusMeta(risk.status);
      row.classList.add("project-risk-row", `project-risk-row--${statusMeta.key}`);
      row.dataset.riskId = risk.id;
      // Direct onclick handler (primary mechanism) — closure captures risk.id
      (function(riskId, riskName) {
        row.onclick = function(e) {
          // (audit 2026-07-03, revue effets de bord N) inclure .pr-ra-open : la pastille « Auto »
          // (autoBadge) porte pr-ra-open SANS pr-link-btn ; sans ce garde, row.onclick faisait
          // stopPropagation avant que le clic n'atteigne le délégué tbody → clic « Auto » ouvrait
          // la fiche du risque au lieu de l'analyse liée. Les deux ancres (lien interne + Auto)
          // laissent désormais bouillonner l'événement jusqu'au handler délégué _prClickDelegate.
          if (e.target && e.target.closest && e.target.closest(".pr-link-btn, .pr-ra-open")) return;
          e.stopPropagation(); // Prevent delegation handler from also firing
          // Ensure modal overlay is properly closed before reopening
          var ov = document.getElementById("modalOverlay");
          if (ov && ov.classList.contains("modal-overlay--visible")) {
            if (typeof closeModal === 'function') closeModal();
            else ov.classList.remove("modal-overlay--visible");
          }
          openProjectRiskModal(riskId);
        };
      })(risk.id, risk.projectName);
      // Build link button (compact icon in name column)
      let linkBtn = '';
      if (risk.analysisLink) {
        const internalMatch = risk.analysisLink.match(/^#risk-analysis:(.+)$/);
        if (internalMatch && typeof window.raOpenWizard === 'function') {
          // (audit 2026-07-03, BLOQUANT XSS) id porté par un data-attribut ÉCHAPPÉ (contexte
          // attribut, sûr) + handler délégué — l'ancien onclick interpolait la valeur dans du
          // JS-dans-attribut : escapeHtml(') = &#39; que le parseur HTML re-décode en ' AVANT
          // l'exécution JS → break-out → XSS stocké via analysisLink chez tout co-éditeur.
          linkBtn = `<a class="pr-link-btn pr-ra-open" href="javascript:void(0)" data-ra-id="${escapeHtml(internalMatch[1])}" title="${escapeHtml(t('projectRisks.riskAnalysisLink'))}"><i class="fas fa-external-link-alt"></i></a>`;
        } else {
          linkBtn = isSafeUrl(risk.analysisLink) ? `<a class="pr-link-btn" href="${escapeHtml(risk.analysisLink)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml(risk.analysisLink)}" onclick="event.stopPropagation()"><i class="fas fa-external-link-alt"></i></a>` : '';
        }
      }
      const rawDate = risk.completionDate || "";
      let dateLabel = "—";
      if (rawDate) {
        const dateValue = new Date(rawDate);
        dateLabel = !Number.isNaN(dateValue.getTime()) ? dateValue.toLocaleDateString() : rawDate;
      }
      let autoBadge = '';
      if (risk.sourceAnalysisId) {
        const autoMatch = risk.analysisLink ? risk.analysisLink.match(/^#risk-analysis:(.+)$/) : null;
        if (autoMatch && typeof window.raOpenWizard === 'function') {
          autoBadge = `<a class="pr-auto-badge pr-auto-badge--link pr-ra-open" href="javascript:void(0)" data-ra-id="${escapeHtml(autoMatch[1])}" title="${escapeHtml(t('projectRisks.openAnalysis'))}"><i class="fas fa-link"></i> Auto</a>`;
        } else {
          autoBadge = `<span class="pr-auto-badge"><i class="fas fa-link"></i> Auto</span>`;
        }
      }
      row.innerHTML = `
        <td>
          <div class="project-risk-name">
            <span class="project-risk-indicator project-risk-indicator--${statusMeta.key}" aria-hidden="true"><i class="fas ${statusMeta.icon}"></i></span>
            <strong>${escapeHtml(risk.projectName || "")}</strong>${autoBadge}${linkBtn}
          </div>
        </td>
        <td>${escapeHtml(risk.beneficiary || "")}</td>
        <td><div class="project-risk-desc">${escapeHtml(risk.description || "")}</div></td>
        <td>${escapeHtml(dateLabel)}</td>
        <td><span class="status-badge ${statusMeta.badge}"><i class="fas ${statusMeta.icon}"></i> ${escapeHtml(statusMeta.label)}</span></td>
        <td>${risk.priority ? `<span class="pr-priority-badge pr-priority-badge--${risk.priority}">${escapeHtml(t('projectRisks.priority' + risk.priority.charAt(0).toUpperCase() + risk.priority.slice(1)))}</span>` : '—'}</td>
        <td><div class="project-risk-note">${escapeHtml(risk.note || '')}</div></td>
      `;
      tbody.appendChild(row);
    });
    // Event delegation: backup click handler on tbody for all rows
    _prClickDelegate = function(e) {
      // (audit 2026-07-03) ouverture de l'analyse liée : lit l'id depuis data-ra-id (jamais
      // interpolé dans du JS) et le passe en variable — remplace l'onclick vulnérable au XSS.
      var raEl = e.target && e.target.closest(".pr-ra-open");
      if (raEl && raEl.dataset.raId) {
        if (typeof window.raOpenWizard === 'function') window.raOpenWizard(raEl.dataset.raId);
        var _ram = document.querySelector('[data-module="risk-analysis"]');
        if (_ram) _ram.click();
        return;
      }
      if (e.target && e.target.closest(".pr-link-btn")) return;
      const row = e.target.closest("tr.project-risk-row");
      if (!row || !row.dataset.riskId) return;
      openProjectRiskModal(row.dataset.riskId);
    };
    tbody.addEventListener("click", _prClickDelegate);
    updateProjectRisksKpi();
  }
  function openProjectRiskModal(riskId = "") {
    ensureProjectRisks();
    const risk = (appData.projectRisks || []).find((r) => r.id === riskId);
    const isEdit = !!risk;
    const current = risk || normalizeProjectRiskEntry({}, (appData.projectRisks || []).length);
    const isAutoSync = !!(current.sourceAnalysisId);
    let recommendationsSection = null;
    const fields = [
      { name: "projectName", label: t('projectRisks.projectName'), value: current.projectName, readOnly: isAutoSync },
      { name: "beneficiary", label: t('projectRisks.beneficiaryLabel'), value: current.beneficiary },
      { name: "reportNumber", label: t('projectRisks.reportNumber'), value: current.reportNumber, readOnly: isAutoSync },
      { name: "revisionNumber", label: t('projectRisks.revisionNumber'), type: "number", value: current.revisionNumber || 0 },
      { name: "description", label: t('projectRisks.projectDescription'), type: "textarea", value: current.description },
      { name: "analysisLink", label: t('projectRisks.riskAnalysisLink'), type: "url", value: current.analysisLink, readOnly: isAutoSync },
      { name: "completionDate", label: t('projectRisks.completionDate'), type: "date", value: current.completionDate, readOnly: isAutoSync },
      { name: "status", label: t('fields.status'), type: "select", value: current.status, options: PROJECT_RISK_STATUS_OPTIONS, readOnly: isAutoSync },
      { name: "priority", label: t('projectRisks.priorityLabel'), type: "select", value: current.priority || '', options: [
        { value: '', label: '—' },
        // FIX-I18N-PRIORITY-CRITICAL (audit 2026-06-10, bundle.js:18244) : la migration
        // AUDIT-H3 produit la valeur 'critical' (depuis 'critique') et validPriorities
        // l'accepte, mais l'option manquait ici — toute re-sauvegarde de la fiche
        // réinitialisait silencieusement la priorité à ''.
        { value: 'critical', label: t('projectRisks.priorityCritical') },
        { value: 'high', label: t('projectRisks.priorityHigh') },
        { value: 'medium', label: t('projectRisks.priorityMedium') },
        { value: 'low', label: t('projectRisks.priorityLow') }
      ] },
      { name: "note", label: t('projectRisks.note'), type: "textarea", value: current.note || '' }
    ];
    var modalTitle = isEdit ? (isReadOnlyMode() ? t('modals.viewProjectRisk') : t('modals.editProjectRisk')) : t('modals.addProjectRisk');
    openModal(modalTitle, fields, (data) => {
      if (!data.projectName || !data.projectName.trim()) { showToast2(t('toasts.requiredFields') || 'Veuillez remplir les champs obligatoires.', 'warning'); return false; }
      const recommendations = collectProjectRiskRecommendations(recommendationsSection);
      const clean = normalizeProjectRiskEntry({
        id: current.id,
        projectName: isAutoSync ? current.projectName : data.projectName,
        beneficiary: data.beneficiary,
        reportNumber: isAutoSync ? current.reportNumber : data.reportNumber,
        revisionNumber: data.revisionNumber,
        description: data.description,
        analysisLink: isAutoSync ? current.analysisLink : data.analysisLink,
        completionDate: isAutoSync ? current.completionDate : data.completionDate,
        status: isAutoSync ? current.status : data.status,
        priority: data.priority || '',
        note: data.note || '',
        recommendations,
        sourceAnalysisId: current.sourceAnalysisId
      }, 0);
      if (isEdit) {
        const idx = appData.projectRisks.findIndex((r) => r.id === riskId);
        if (idx >= 0) appData.projectRisks[idx] = { ...appData.projectRisks[idx], ...clean };
      } else {
        appData.projectRisks.push(clean);
      }
            addAuditEntry(isEdit ? 'edit' : 'add', 'projectRisks', clean.projectName || current.projectName);
      saveData();
      loadProjectRisksTable();
    }, isEdit ? () => deleteProjectRisk(riskId) : null);
    const modalBody = document.getElementById("modalBody");
    if (modalBody) {
      // Add auto-sync banner if applicable
      if (isAutoSync) {
        const banner = document.createElement("div");
        banner.className = "pr-auto-sync-banner";
        banner.innerHTML = `<i class="fas fa-link"></i> ${t('projectRisks.autoSyncBanner').replace('{title}', escapeHtml(current.projectName))}`;
        modalBody.insertBefore(banner, modalBody.firstChild);
      }
      recommendationsSection = renderProjectRiskRecommendations(modalBody, current.recommendations || []);
    }
  }
  function addProjectRisk() {
    openProjectRiskModal();
  }
  function deleteProjectRisk(riskId) {
    showConfirmModal(t('confirms.deleteProjectRisk'), t('confirms.deleteProjectRisk'), function() {
      var _pr = (appData.projectRisks || []).find(function(r) { return r.id === riskId; });
      appData.projectRisks = (appData.projectRisks || []).filter((risk) => risk.id !== riskId);
            addAuditEntry('delete', 'projectRisks', _pr ? _pr.projectName : riskId);
      saveData();
      loadProjectRisksTable();
      if (typeof updateDashboard === 'function') updateDashboard();
      showToast2(t('toasts.itemDeleted'), "success");
    }, { type: 'danger' });
  }
  window.loadProjectRisksTable = loadProjectRisksTable;
  window.openProjectRiskModal = openProjectRiskModal;
  window.addProjectRisk = addProjectRisk;
  window.deleteProjectRisk = deleteProjectRisk;

  // modules/risks.js
  function getImpactLevelOptions() {
    return [
      { value: 1, label: t('impactLevels.1') },
      { value: 2, label: t('impactLevels.2') },
      { value: 3, label: t('impactLevels.3') },
      { value: 4, label: t('impactLevels.4') },
      { value: 5, label: t('impactLevels.5') }
    ];
  }
  var IMPACT_LEVEL_OPTIONS = getImpactLevelOptions();
  function getImpactLevelLabels() {
    return {
      1: t('impactLevels.1'),
      2: t('impactLevels.2'),
      3: t('impactLevels.3'),
      4: t('impactLevels.4'),
      5: t('impactLevels.5')
    };
  }
  var IMPACT_LEVEL_LABELS = getImpactLevelLabels();
  function getProbabilityLevelOptions() {
    return [
      { value: 1, label: t('probabilityLevels.1') },
      { value: 2, label: t('probabilityLevels.2') },
      { value: 3, label: t('probabilityLevels.3') },
      { value: 4, label: t('probabilityLevels.4') },
      { value: 5, label: t('probabilityLevels.5') }
    ];
  }
  var PROBABILITY_LEVEL_OPTIONS = getProbabilityLevelOptions();
  function getProbabilityLevelLabels() {
    return {
      1: t('probabilityLevels.1'),
      2: t('probabilityLevels.2'),
      3: t('probabilityLevels.3'),
      4: t('probabilityLevels.4'),
      5: t('probabilityLevels.5')
    };
  }
  var PROBABILITY_LEVEL_LABELS = getProbabilityLevelLabels();
  function getTreatmentOptions() {
    return [
      { value: "Réduction", label: t('treatments.reduction') },
      { value: "Acceptation", label: t('treatments.acceptation') },
      { value: "Transfert", label: t('treatments.transfert') },
      { value: "Évitement", label: t('treatments.evitement') }
    ];
  }
  var TREATMENT_OPTIONS = getTreatmentOptions();
  // Returns canonical French risk level (stored in data)
  function calculateRiskLevel(score) {
    if (score >= 20) return "Critique";
    if (score >= 15) return "Grave";
    if (score >= 10) return "Significatif";
    if (score >= 5) return "Mineur";
    return "Insignifiant";
  }
  // Returns translated risk level for display
  const RISK_LEVEL_I18N = {
    'Critique': 'riskLevels.critical',
    'Grave': 'riskLevels.high',
    'Significatif': 'riskLevels.medium',
    'Mineur': 'riskLevels.low',
    'Insignifiant': 'riskLevels.negligible'
  };
  function translateRiskLevel(level) {
    return RISK_LEVEL_I18N[level] ? t(RISK_LEVEL_I18N[level]) : level;
  }
  function generateRiskId() {
    return generateId('risk');
  }
  function ensureRiskIds() {
    if (!Array.isArray(appData.risks)) appData.risks = [];
    appData.risks.forEach(risk => {
      if (!risk.id) risk.id = generateRiskId();
    });
  }
  function loadRisksTable() {
    const tbody = document.getElementById("risksTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('risks')) return;
    tbody.innerHTML = "";
    const levelOrder = {
      "Critique": 0,
      "Grave": 1,
      "Significatif": 2,
      "Mineur": 3,
      "Insignifiant": 4
    };
    if (!Array.isArray(appData.risks) || appData.risks.length === 0) {
      var emptyRow = document.createElement('tr');
      var emptyTd = document.createElement('td');
      emptyTd.setAttribute('colspan', '7');
      emptyTd.className = 'table__empty-state';
      var emptyIcon = document.createElement('i');
      emptyIcon.className = 'fas fa-exclamation-triangle';
      emptyIcon.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      emptyTd.appendChild(emptyIcon);
      emptyTd.appendChild(document.createTextNode(t('emptyStates.noRisks', 'Aucun risque identifié.')));
      emptyRow.appendChild(emptyTd);
      tbody.appendChild(emptyRow);
      return;
    }
    const _s = SectionToolbar.getSort("risks");
    const _q = (SectionToolbar.getSearch("risks") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const _sortKey = _s ? _s.key : "level";
    const _sortDir = _s ? _s.dir : "asc";
    const sortedRisks = [...appData.risks]
      .filter((r) => !_q || ((r.title || "") + " " + (r.description || "")).toLowerCase().indexOf(_q) >= 0).sort((a, b) => {
      let diff = 0;
      switch (_sortKey) {
        case "title": diff = SortUtils.text(a.title, b.title); break;
        case "impact": diff = SortUtils.numeric(a.impact, b.impact); break;
        case "probability": diff = SortUtils.numeric(a.probability, b.probability); break;
        default: diff = SortUtils.orderedMap(levelOrder, 999)(a.level, b.level); break;
      }
      return _sortDir === "desc" ? -diff : diff;
    });
    sortedRisks.forEach((risk) => {
      const row = document.createElement("tr");
      row.classList.add('risk-row--' + (risk.level || '').toLowerCase());
      const linkedActions = (appData.actions || []).filter((a) => actionLinksToRisk(a, risk.id));
      const linkedActionsHtml = linkedActions.length
        ? linkedActions.map((action) => `
          <a href="#" class="link-badge link-badge--risk risk-action-link" data-action-id="${escapeHtml(action.id)}" title="${escapeHtml(t('labels.openAction', 'Ouvrir l\'action'))}">
            ${escapeHtml(action.title || action.id)}
          </a>
        `).join(" ")
        : "—";
      const assetBadges = buildCriticalAssetBadges(risk.criticalAssetIds);
        const levelClasses = {
          "Critique": {
            badge: "status-badge--critique",
            icon: "fas fa-bolt"
          },
          "Grave": {
            badge: "status-badge--grave",
            icon: "fas fa-exclamation-triangle"
          },
          "Significatif": {
            badge: "status-badge--significatif",
            icon: "fas fa-exclamation-circle"
          },
          "Mineur": {
            badge: "status-badge--mineur",
            icon: "fas fa-info-circle"
          },
          "Insignifiant": {
            badge: "status-badge--insignifiant",
            icon: "fas fa-check"
          }
        };
      const cls = levelClasses[risk.level] || { badge: "status-badge--mineur", icon: "fas fa-minus" };
      row.innerHTML = `
            <td><div class="cell-clamp cell-clamp--2"><strong>${escapeHtml(risk.title)}</strong></div></td>
            <td>${escapeHtml(String(IMPACT_LEVEL_LABELS[risk.impact] || risk.impact || ''))}</td>
            <td>${escapeHtml(String(PROBABILITY_LEVEL_LABELS[risk.probability] || risk.probability || ''))}</td>
            <td><span class="status-badge ${cls.badge}"><i class="${cls.icon}"></i>${escapeHtml(translateRiskLevel(risk.level))}</span></td>
            <td><div class="cell-clamp cell-clamp--2">${linkedActionsHtml}</div></td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(risk.treatment || "")}</div></td>
            <td><input type="date" class="risk-codir-date" data-risk-id="${escapeHtml(risk.id)}" value="${escapeHtml(risk.codirDate || '')}"></td>
        `;
      row.querySelectorAll(".risk-action-link").forEach((link) => {
        link.addEventListener("click", (event) => {
          event.preventDefault();
          event.stopPropagation();
          const actionId = link.dataset.actionId;
          if (!actionId) return;
          switchModule("actions");
          setTimeout(() => {
            editAction(actionId);
          }, 200);
        });
      });
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger") || e.target.closest('.risk-codir-date') || e.target.closest(".critical-asset-link")) return;
        editRisk(risk.id);
      });
      attachCriticalAssetLinkHandlers(row);
      tbody.appendChild(row);
    });
    tbody.querySelectorAll('.risk-codir-date').forEach((input) => {
      input.dataset.readonlyLock = "true";
      if (isReadOnlyMode()) input.disabled = true;
      input.addEventListener('input', (e) => {
        if (isReadOnlyMode()) {
          showReadOnlyNotice("mettre à jour la date CODIR");
          refreshReadOnlyLocks();
          return;
        }
        const r = (appData.risks || []).find((rr) => rr.id === input.dataset.riskId);
        if (r) {
          r.codirDate = input.value;
          saveData();
        }
      });
    });
  }
  function editRisk(riskId) {
    const risk = (appData.risks || []).find((r) => r.id === riskId);
    if (!risk) return;
    const criticalAssetIds = Array.isArray(risk.criticalAssetIds) ? risk.criticalAssetIds : [];
    const criticalAssetOptions = getCriticalAssetOptions(criticalAssetIds);
    openModal(t('modals.editRisk'), [
      { name: "title", label: "Titre du risque", value: risk.title },
      { name: "description", label: t('fields.description'), type: "textarea", value: risk.description },
      { name: "impact", label: t('fields.impact'), type: "select", value: String(risk.impact), options: IMPACT_LEVEL_OPTIONS },
      { name: "probability", label: "Probabilit\xE9", type: "select", value: String(risk.probability), options: PROBABILITY_LEVEL_OPTIONS },
      { name: "level", label: t('fields.level'), value: translateRiskLevel(calculateRiskLevel(risk.impact * risk.probability)), readOnly: true },
      { name: "treatment", label: t('fields.treatment'), type: "select", value: risk.treatment, options: TREATMENT_OPTIONS },
      // M17 (audit 2026-07-10) : propriétaire de risque (ISO 27001 6.1.2 c) 2)) — champ hybride
      // (nom libre OU partie prenante existante), même patron que les responsables NC/contrôles.
      { name: "owner", label: t('fields.riskOwner', 'Propriétaire du risque'), type: "text", value: risk.owner || "", placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(risk.owner), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "criticalAssetIds", label: t('fields.criticalAssets'), type: "multiselect", value: criticalAssetIds, options: criticalAssetOptions },
      { name: "existingComments", label: t('fields.existingComments'), type: "textarea", value: formatCommentsReadonly(risk.comments), readOnly: true },
      { name: "newComment", label: t('fields.newComment'), type: "textarea" }
    ], (data) => {
      risk.title = data.title;
      risk.description = data.description;
      risk.impact = parseInt(data.impact, 10) || 1;
      risk.probability = parseInt(data.probability, 10) || 1;
      risk.score = risk.impact * risk.probability;
      risk.level = calculateRiskLevel(risk.score);
      risk.treatment = data.treatment || "";
      risk.owner = data.owner || ""; // M17
      risk.criticalAssetIds = Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : [];
      if (appendComment(risk, data.newComment, 10) === 'max') {
        showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', 10), 'warning');
      }
            addAuditEntry('edit', 'risks', risk.title);
      saveData();
      loadRisksTable();
      updateDashboard();
    }, () => deleteRisk(riskId));
    const impactInput = document.getElementById("modal-input-impact");
    const probInput = document.getElementById("modal-input-probability");
    const levelInput = document.getElementById("modal-input-level");
    const updateLvl = () => {
      const score = (parseInt(impactInput.value, 10) || 1) * (parseInt(probInput.value, 10) || 1);
      levelInput.value = translateRiskLevel(calculateRiskLevel(score));
    };
    if (impactInput && probInput && levelInput) {
      impactInput.addEventListener("input", updateLvl);
      probInput.addEventListener("input", updateLvl);
    }
  }
  function viewRiskDetails(riskId) {
    const risk = (appData.risks || []).find((r) => r.id === riskId);
    if (!risk) return;
    const actionList = (appData.actions || [])
      .filter((a) => actionLinksToRisk(a, risk.id))
      .map((a) => a.title || a.id)
      .join(", ");
    openModal(t('modals.riskDetails', 'Détails du risque'), [
      { name: 'title', label: t('fields.title', 'Titre'), type: 'text', value: risk.title || '', readOnly: true },
      { name: 'description', label: t('fields.description', 'Description'), type: 'textarea', value: risk.description || '', readOnly: true },
      { name: 'impact', label: t('fields.impact', 'Impact'), type: 'text', value: (IMPACT_LEVEL_LABELS[risk.impact] || risk.impact || ''), readOnly: true },
      { name: 'probability', label: t('fields.probability', 'Probabilité'), type: 'text', value: (PROBABILITY_LEVEL_LABELS[risk.probability] || risk.probability || ''), readOnly: true },
      { name: 'score', label: t('fields.score', 'Score'), type: 'text', value: String(risk.score || ''), readOnly: true },
      { name: 'level', label: t('fields.level', 'Niveau'), type: 'text', value: risk.level || '', readOnly: true },
      { name: 'treatment', label: t('fields.treatment', 'Traitement'), type: 'text', value: risk.treatment || '', readOnly: true },
      { name: 'owner', label: t('fields.riskOwner', 'Propriétaire du risque'), type: 'text', value: risk.owner || t('labels.none', 'Aucun'), readOnly: true }, // M17
      { name: 'actions', label: t('fields.linkedActions', 'Actions liées'), type: 'text', value: actionList || t('labels.none', 'Aucune'), readOnly: true }
    ], null, null, { hideCancel: true, saveLabel: t('common.close', 'Fermer') });
  }
  function addRisk() {
    const criticalAssetOptions = getCriticalAssetOptions();
    openModal(t('modals.addRisk'), [
      { name: "title", label: "Titre du risque" },
      { name: "description", label: t('fields.description'), type: "textarea" },
      { name: "impact", label: t('fields.impact'), type: "select", value: "1", options: IMPACT_LEVEL_OPTIONS },
      { name: "probability", label: "Probabilit\xE9", type: "select", value: "1", options: PROBABILITY_LEVEL_OPTIONS },
      { name: "level", label: t('fields.level'), value: translateRiskLevel(calculateRiskLevel(1)), readOnly: true },
      { name: "treatment", label: t('fields.treatment'), type: "select", options: TREATMENT_OPTIONS },
      // M17 : propriétaire de risque (hybride nom libre / partie prenante).
      { name: "owner", label: t('fields.riskOwner', 'Propriétaire du risque'), type: "text", value: "", placeholder: t('fields.ownerOrStakeholderPlaceholder', 'Nom libre ou partie prenante'), suggestions: _stakeholderNameSuggestions(''), help: t('fields.ownerOrStakeholderHelp', 'Saisissez un nom libre ou choisissez une partie prenante existante.') },
      { name: "criticalAssetIds", label: t('fields.criticalAssets'), type: "multiselect", value: [], options: criticalAssetOptions },
      { name: "comment", label: t('fields.comment'), type: "textarea" }
    ], (data) => {
      if (!data.title) return false;
      const id = generateRiskId();
      const impact = parseInt(data.impact, 10) || 1;
      const probability = parseInt(data.probability, 10) || 1;
      const score = impact * probability;
      appData.risks.push({
        id,
        title: data.title,
        description: data.description || "",
        impact,
        probability,
        score,
        level: calculateRiskLevel(score),
        treatment: data.treatment || "",
        owner: data.owner || "", // M17
        codirDate: "",
        criticalAssetIds: Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : [],
        comments: data.comment ? [{ text: data.comment, date: new Date().toISOString() }] : []
      });
            addAuditEntry('add', 'risks', data.title);
      saveData();
      loadRisksTable();
      updateRiskMatrix();
      updateDashboard();
    });
    const impactInput = document.getElementById("modal-input-impact");
    const probInput = document.getElementById("modal-input-probability");
    const levelInput = document.getElementById("modal-input-level");
    const updateLvl = () => {
      const score = (parseInt(impactInput.value, 10) || 1) * (parseInt(probInput.value, 10) || 1);
      levelInput.value = translateRiskLevel(calculateRiskLevel(score));
    };
    if (impactInput && probInput && levelInput) {
      impactInput.addEventListener("input", updateLvl);
      probInput.addEventListener("input", updateLvl);
    }
  }
  function deleteRisk(riskId) {
    showConfirmModal(t('confirms.deleteRisk'), t('confirms.deleteRisk'), function() {
      var _risk = (appData.risks || []).find(function(r) { return r.id === riskId; });
      appData.risks = (appData.risks || []).filter((r) => r.id !== riskId);
      (appData.actions || []).forEach(function(a) {
        if (a.linkType === "risk" && Array.isArray(a.linkId)) {
          a.linkId = a.linkId.filter(function(id) { return id !== riskId; });
        }
      });
      appData.actions = (appData.actions || []).filter(function(a) {
        if (a.linkType === "risk") return Array.isArray(a.linkId) ? a.linkId.length > 0 : (a.linkId && a.linkId !== riskId);
        return true;
      });
      if (Array.isArray(appData.soa)) {
        appData.soa.forEach(function(entry) {
          if (Array.isArray(entry.linkedRiskIds)) {
            entry.linkedRiskIds = entry.linkedRiskIds.filter(function(id) { return id !== riskId; });
          }
        });
      }
            addAuditEntry('delete', 'risks', _risk ? _risk.title : riskId);
      saveData();
      _csiPersistAffectedTabs(['risks', 'actions', 'soa']); // FIX-CASCADE-PERSIST : persiste les sections croisées nettoyées
      loadRisksTable();
      loadActionsTable(); // PASS2-301: Refresh actions table after cascade delete
      updateRiskMatrix();
      updateDashboard();
    }, { type: 'danger' });
  }

  function manageRiskComments(riskId) {
    manageComments('risks', riskId, { titlePrefix: t('risks.manageComments', 'Commentaires du risque') });
  }
  window.loadRisksTable = loadRisksTable;
  window.editRisk = editRisk;
  window.viewRiskDetails = viewRiskDetails;
  window.addRisk = addRisk;
  window.deleteRisk = deleteRisk;
  window.manageRiskComments = manageRiskComments;

  // modules/threats.js
function getThreatStatusLabels() {
    return {
      reduction: t('threatStatuses.reduction'),
      acceptation: t('threatStatuses.acceptation'),
      transfert: t('threatStatuses.transfert'),
      evitement: t('threatStatuses.evitement')
    };
}
var THREAT_STATUS_LABELS = getThreatStatusLabels();
function getThreatStatusOptions() {
  return [
    { value: "reduction", label: t('threatStatuses.reduction') },
    { value: "acceptation", label: t('threatStatuses.acceptation') },
    { value: "transfert", label: t('threatStatuses.transfert') },
    { value: "evitement", label: t('threatStatuses.evitement') }
  ];
}
var THREAT_STATUS_OPTIONS = getThreatStatusOptions();
  function loadThreatsTable() {
    const tbody = document.getElementById("threatsTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('threats-section')) return;
    tbody.innerHTML = "";
    const levelOrder = {
      "Critique": 0,
      "Grave": 1,
      "Significatif": 2,
      "Mineur": 3,
      "Insignifiant": 4
    };
    if (!Array.isArray(appData.threats) || appData.threats.length === 0) {
      var emptyRow = document.createElement('tr');
      var emptyTd = document.createElement('td');
      emptyTd.setAttribute('colspan', '7');
      emptyTd.className = 'table__empty-state';
      var emptyIcon = document.createElement('i');
      emptyIcon.className = 'fas fa-shield-halved';
      emptyIcon.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      emptyTd.appendChild(emptyIcon);
      emptyTd.appendChild(document.createTextNode(t('emptyStates.noThreats', 'Aucune menace identifiée. Cliquez sur "Nouvelle Menace" pour commencer.')));
      emptyRow.appendChild(emptyTd);
      tbody.appendChild(emptyRow);
      return;
    }
    const _s = SectionToolbar.getSort("threats-section");
    const _q = (SectionToolbar.getSearch("threats-section") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const _sortKey = _s ? _s.key : "level";
    const _sortDir = _s ? _s.dir : "asc";
    const sortedThreats = [...appData.threats]
      .filter((th) => !_q || ((th.title || "") + " " + (th.description || "")).toLowerCase().indexOf(_q) >= 0).sort((a, b) => {
      let diff = 0;
      switch (_sortKey) {
        case "title": diff = SortUtils.text(a.title, b.title); break;
        case "score": diff = SortUtils.numeric((a.impact || 0) * (a.likelihood || 0), (b.impact || 0) * (b.likelihood || 0)); break;
        case "status": diff = SortUtils.text(a.status || "", b.status || ""); break;
        default: diff = SortUtils.orderedMap(levelOrder, 999)(a.level, b.level); break;
      }
      return _sortDir === "desc" ? -diff : diff;
    });
    sortedThreats.forEach((threat) => {
      const row = document.createElement("tr");
      // Lot 1a (refonte UX) : « Actions liées » + « Actifs critiques » fusionnés en compteurs compacts
      const linkedActionsCount = appData.actions
        .filter((a) => a.linkType === "threat" && a.linkId === threat.id)
        .length;
      const linkedAssetsCount = Array.isArray(threat.criticalAssetIds) ? threat.criticalAssetIds.length : 0;
      const linkParts = [];
      if (linkedActionsCount) linkParts.push(t('threats.linkedActionsCount', '{n} action(s)').replace('{n}', linkedActionsCount));
      if (linkedAssetsCount) linkParts.push(t('threats.linkedAssetsCount', '{n} actif(s)').replace('{n}', linkedAssetsCount));
      const linksText = linkParts.length ? linkParts.join(" · ") : "—";
      const levelClasses = {
        "Critique": "status-badge--critique",
        "Grave": "status-badge--grave",
        "Significatif": "status-badge--significatif",
        "Mineur": "status-badge--mineur",
        "Insignifiant": "status-badge--insignifiant"
      };
      row.innerHTML = `
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(threat.title)}</div></td>
            <td>${escapeHtml(String(IMPACT_LEVEL_LABELS[threat.impact] || threat.impact || ''))}</td>
            <td>${escapeHtml(String(PROBABILITY_LEVEL_LABELS[threat.likelihood] || threat.likelihood || ''))}</td>
            <td>${threat.score}</td>
            <td><span class="status-badge ${levelClasses[threat.level]}">${escapeHtml(translateRiskLevel(threat.level))}</span></td>
            <td>${escapeHtml(THREAT_STATUS_LABELS[threat.status] || threat.status || "")}</td>
            <td>${escapeHtml(linksText)}</td>
        `;
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger") || e.target.closest(".critical-asset-link")) return;
        editThreat(threat.id);
      });
      attachCriticalAssetLinkHandlers(row);
      tbody.appendChild(row);
    });
  }
  function showThreatForm(threatId = null) {
    const threat = threatId ? appData.threats.find((t) => t.id === threatId) : null;
    const isEdit = !!threat;
    const likelihood = threat ? threat.likelihood : 1;
    const impact = threat ? threat.impact : 1;
    const criticalAssetIds = threat && Array.isArray(threat.criticalAssetIds) ? threat.criticalAssetIds : [];
    const criticalAssetOptions = getCriticalAssetOptions(criticalAssetIds);
    openModal(isEdit ? t('modals.editThreat') : t('modals.addThreat'), [
      { name: "title", label: t('fields.title'), value: threat ? threat.title : "", required: true },
      { name: "context", label: t('fields.context'), value: threat ? threat.context : "" },
      { name: "description", label: t('fields.description'), type: "textarea", value: threat ? threat.description : "" },
      { name: "likelihood", label: t('fields.likelihood'), type: "select", value: String(likelihood), options: PROBABILITY_LEVEL_OPTIONS },
      { name: "impact", label: t('fields.impact'), type: "select", value: String(impact), options: IMPACT_LEVEL_OPTIONS },
      { name: "level", label: t('fields.level'), value: translateRiskLevel(calculateRiskLevel(likelihood * impact)), readOnly: true },
      { name: "status", label: t('fields.status'), type: "select", value: threat ? threat.status : "reduction", options: THREAT_STATUS_OPTIONS },
      { name: "criticalAssetIds", label: t('fields.criticalAssets'), type: "multiselect", value: criticalAssetIds, options: criticalAssetOptions }
    ], (data) => {
      const li = parseInt(data.likelihood, 10) || 1;
      const im = parseInt(data.impact, 10) || 1;
      const score = li * im;
      if (isEdit) {
        threat.title = data.title;
        threat.context = data.context;
        threat.description = data.description;
        threat.likelihood = li;
        threat.impact = im;
        threat.score = score;
        threat.level = calculateRiskLevel(score);
        threat.status = data.status;
        threat.criticalAssetIds = Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : [];
        threat.updatedAt = new Date().toISOString();
      } else {
        const newThreat = {
          id: generateId("threat"),
          title: data.title,
          context: data.context || "",
          description: data.description || "",
          likelihood: li,
          impact: im,
          score,
          level: calculateRiskLevel(score),
          status: data.status || "reduction",
          criticalAssetIds: Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : [],
          createdAt: new Date().toISOString(),
          updatedAt: new Date().toISOString()
        };
        appData.threats.push(newThreat);
      }
            addAuditEntry(isEdit ? 'edit' : 'add', 'threats', data.title);
      saveData();
      loadThreatsTable();
      updateDashboard();
    }, threat ? () => deleteThreat(threatId) : null);
    const likInput = document.getElementById("modal-input-likelihood");
    const impInput = document.getElementById("modal-input-impact");
    const lvlInput = document.getElementById("modal-input-level");
    const updateLvl = () => {
      const sc = (parseInt(impInput.value, 10) || 1) * (parseInt(likInput.value, 10) || 1);
      lvlInput.value = translateRiskLevel(calculateRiskLevel(sc));
    };
    if (likInput && impInput && lvlInput) {
      likInput.addEventListener("input", updateLvl);
      impInput.addEventListener("input", updateLvl);
    }
  }
  function editThreat(threatId) {
    showThreatForm(threatId);
  }
  function viewThreatDetails(threatId) {
    const threat = appData.threats.find((t2) => t2.id === threatId);
    if (!threat) return;
    const actionList = appData.actions
      .filter((a) => a.linkType === "threat" && a.linkId === threat.id)
      .map((a) => a.title || a.id)
      .join(", ");
    openModal(t('modals.threatDetails', 'Détails de la menace'), [
      { name: 'title', label: t('fields.title', 'Titre'), type: 'text', value: threat.title || '', readOnly: true },
      { name: 'context', label: t('fields.context', 'Contexte'), type: 'text', value: threat.context || '', readOnly: true },
      { name: 'description', label: t('fields.description', 'Description'), type: 'textarea', value: threat.description || '', readOnly: true },
      { name: 'probability', label: t('fields.probability', 'Probabilité'), type: 'text', value: (PROBABILITY_LEVEL_LABELS[threat.likelihood] || threat.likelihood || ''), readOnly: true },
      { name: 'impact', label: t('fields.impact', 'Impact'), type: 'text', value: (IMPACT_LEVEL_LABELS[threat.impact] || threat.impact || ''), readOnly: true },
      { name: 'score', label: t('fields.score', 'Score'), type: 'text', value: String(threat.score || ''), readOnly: true },
      { name: 'level', label: t('fields.level', 'Niveau'), type: 'text', value: threat.level || '', readOnly: true },
      { name: 'status', label: t('fields.status', 'Statut'), type: 'text', value: (THREAT_STATUS_LABELS[threat.status] || threat.status || ''), readOnly: true },
      { name: 'actions', label: t('fields.linkedActions', 'Actions liées'), type: 'text', value: actionList || t('labels.none', 'Aucune'), readOnly: true }
    ], null, null, { hideCancel: true, saveLabel: t('common.close', 'Fermer') });
  }
  function deleteThreat(threatId) {
    showConfirmModal(t('confirms.deleteThreat'), t('confirms.deleteThreat'), function() {
      var _thr = appData.threats.find(function(th) { return th.id === threatId; });
      appData.threats = appData.threats.filter((t) => t.id !== threatId);
      appData.actions = (appData.actions || []).filter((a) => !(a.linkType === "threat" && a.linkId === threatId));
            addAuditEntry('delete', 'threats', _thr ? _thr.title : threatId);
      saveData();
      _csiPersistAffectedTabs(['threats', 'actions']); // FIX-CASCADE-PERSIST : persiste les sections croisées nettoyées
      loadThreatsTable();
      loadActionsTable(); // PASS2-301: Refresh actions table after cascade delete
      updateDashboard();
    }, { type: 'danger' });
  }
  window.loadThreatsTable = loadThreatsTable;
  window.showThreatForm = showThreatForm;
  window.editThreat = editThreat;
  window.viewThreatDetails = viewThreatDetails;
  window.deleteThreat = deleteThreat;

  // modules/nonconformities.js
function getNcStatusOptions() {
  return [
    { value: "detection", label: t('ncStatuses.detection') },
    { value: "analyse", label: t('ncStatuses.analyse') },
    { value: "action", label: t('ncStatuses.action') },
    { value: "cloture", label: t('ncStatuses.cloture') }
  ];
}
var STATUS_OPTIONS = getNcStatusOptions();
var NC_STATUS_LABELS = {};
STATUS_OPTIONS.forEach(function(opt) { NC_STATUS_LABELS[opt.value] = opt.label; });
function getNcTypeOptions() {
  return [
    { value: "Majeure", label: t('ncTypes.majeure') },
    { value: "Mineure", label: t('ncTypes.mineure') }
  ];
}
var NC_TYPE_OPTIONS = getNcTypeOptions();
  function loadNonConformitiesTable() {
    const tbody = document.getElementById("ncTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('nonconformities')) return;
    tbody.innerHTML = "";
    updateNcAuditFilterOptions();
    updateNcOwnerFilterOptions();
    const _f = SectionToolbar.getFilters("nonconformities");
    const _q = (SectionToolbar.getSearch("nonconformities") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const auditFilter = _f.audit || "";
    const ownerFilter = _f.owner || "";
    const _s = SectionToolbar.getSort("nonconformities");
    const sortBy = _s ? _s.key : "type";
    const sortDir = _s ? _s.dir : "asc";
    const typeOrder = { Majeure: 0, Mineure: 1 };
    const statusOrder = STATUS_OPTIONS.reduce((acc, option, index) => {
      acc[option.value] = index;
      return acc;
    }, {});
    const auditMap = new Map((appData.audits || []).map((audit) => [audit.id, audit.title || audit.id]));
    const getAuditTitle = (nc) => auditMap.get(nc.auditId) || "";
    const sortedNCs = [...appData.nonconformities].sort((a, b) => {
      let diff = 0;
      switch (sortBy) {
        case "status":
          diff = SortUtils.orderedMap(statusOrder, 99)(a.status, b.status);
          break;
        case "date":
          diff = SortUtils.date(b.detectionDate, a.detectionDate);
          break;
        case "audit":
          diff = SortUtils.text(getAuditTitle(a), getAuditTitle(b));
          break;
        case "owner":
          diff = SortUtils.text(a.assignedTo, b.assignedTo);
          break;
        case "type":
        default:
          diff = SortUtils.orderedMap(typeOrder, 99)(a.type, b.type);
          break;
      }
      return diff !== 0 ? diff : SortUtils.text(a.title, b.title);
    });
    if (sortDir === "desc") sortedNCs.reverse();
    if (!sortedNCs.length) {
      var _er = document.createElement('tr'); var _et = document.createElement('td');
      _et.setAttribute('colspan', '7'); _et.className = 'table__empty-state';
      var _ei = document.createElement('i'); _ei.className = 'fas fa-clipboard-check';
      _ei.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      _et.appendChild(_ei); _et.appendChild(document.createTextNode(t('emptyStates.noNonconformities', 'Aucune non-conformité')));
      _er.appendChild(_et); tbody.appendChild(_er); return;
    }
    sortedNCs.forEach((nc) => {
      if (auditFilter && nc.auditId !== auditFilter) return;
      if (_q && ((nc.title || "") + " " + (nc.assignedTo || "")).toLowerCase().indexOf(_q) < 0) return;
      if (ownerFilter) {
        const assignedTo = (nc.assignedTo || "").trim();
        if (ownerFilter === "unassigned") {
          if (assignedTo) return;
        } else if (assignedTo !== ownerFilter) {
          return;
        }
      }
      const row = document.createElement("tr");
      const typeClass = nc.type === "Majeure" ? "status-badge--eleve" : "status-badge--moyen";
      const relatedActions = (appData.actions || []).filter((a) => {
        return (a.ncIds || []).includes(nc.id) || a.linkType === "nc" && a.linkId === nc.id;
      });
      // Lot 1a (refonte UX) : « Actions liées » = compteur compact ; « — » quand vide
      const actionsText = relatedActions.length
        ? t('nc.linkedActionsCount', '{n} action(s)').replace('{n}', relatedActions.length)
        : "—";
      const audit = appData.audits.find(a => a.id === nc.auditId);
      const auditText = audit ? audit.title : "—";
      // FIX-I18N-NCTYPE-CASE (audit 2026-06-10, bundle.js:18974) : nc.type est stocké
      // "Majeure"/"Mineure" mais les clés de locale sont ncTypes.majeure/mineure —
      // resolveKey étant sensible à la casse, le badge n'était jamais traduit.
      row.innerHTML = `
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(nc.title)}</div></td>
            <td><span class="status-badge ${typeClass}">${escapeHtml(t('ncTypes.' + String(nc.type || '').toLowerCase(), nc.type))}</span></td>
            <td>${escapeHtml(NC_STATUS_LABELS[nc.status] || nc.status || "")}</td>
            <td>${nc.detectionDate ? new Date(nc.detectionDate).toLocaleDateString() : "—"}</td>
            <td>${nc.assignedTo ? escapeHtml(nc.assignedTo) : "—"}</td>
            <td>${escapeHtml(auditText)}</td>
            <td>${escapeHtml(actionsText)}</td>
        `;
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger") || e.target.closest(".critical-asset-link")) return;
        editNC(nc.id);
      });
      attachCriticalAssetLinkHandlers(row);
      tbody.appendChild(row);
    });
  }
  function editNC(ncId) {
    const nc = appData.nonconformities.find((n) => n.id === ncId);
    if (!nc) return;
    const linkedActions = (appData.actions || []).filter((a) => (a.ncIds || []).includes(nc.id)).map((a) => a.id);
    const criticalAssetIds = Array.isArray(nc.criticalAssetIds) ? nc.criticalAssetIds : [];
    const criticalAssetOptions = getCriticalAssetOptions(criticalAssetIds);
    openModal(t('modals.editNc'), [
      { name: "title", label: t('fields.title'), value: nc.title, required: true },
      { name: "type", label: t('fields.type', 'Type'), type: "select", value: nc.type, options: NC_TYPE_OPTIONS },
      { name: "status", label: t('fields.status'), type: "select", value: nc.status, options: STATUS_OPTIONS },
      { name: "detectionDate", label: t('fields.detectionDate'), type: "date", value: nc.detectionDate },
      {
        name: "assignedTo",
        label: t('fields.owner'),
        type: "select",
        value: nc.assignedTo,
        options: [{ value: '', label: '\u2014 ' + t('common.select', 'Sélectionner') + ' \u2014' }].concat(appData.stakeholders.map((s) => ({ value: s.name, label: s.name })))
      },
      { name: "auditId", label: t('fields.linkedAudit', 'Audit lié'), type: "select", value: nc.auditId, options: [{ value: '', label: '\u2014 ' + t('common.select', 'Sélectionner') + ' \u2014' }].concat(appData.audits.map(a => ({ value: a.id, label: a.title }))) },
      { name: "criticalAssetIds", label: t('fields.criticalAssets'), type: "multiselect", value: criticalAssetIds, options: criticalAssetOptions },
      { name: "description", label: t('fields.operations', 'Opérations'), type: "textarea", value: nc.description },
    ], (data) => {
      nc.title = data.title;
      nc.type = data.type;
      nc.status = data.status;
      nc.detectionDate = data.detectionDate;
      nc.assignedTo = data.assignedTo;
      nc.auditId = data.auditId;
      nc.description = data.description;
      nc.criticalAssetIds = Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : [];
            addAuditEntry('edit', 'nc', nc.title);
      saveData();
      loadNonConformitiesTable();
      updateDashboard();
    }, () => deleteNC(ncId));
  }
  function updateNcAuditFilterOptions() {
    const audits = Array.isArray(appData.audits) ? appData.audits : [];
    SectionToolbar.updateFilterOptions("nonconformities", "audit", audits.map(function(audit) {
      return { value: audit.id, label: audit.title || audit.id };
    }));
  }
  function updateNcOwnerFilterOptions() {
    const names = new Set();
    const addName = (value) => {
      const cleaned = typeof value === "string" ? value.trim() : "";
      if (cleaned) names.add(cleaned);
    };
    (Array.isArray(appData.stakeholders) ? appData.stakeholders : []).forEach((st) => addName(st.name));
    (Array.isArray(appData.nonconformities) ? appData.nonconformities : []).forEach((nc) => addName(nc.assignedTo));
    const sorted = Array.from(names).sort(SortUtils.text);
    const hasUnassigned = (Array.isArray(appData.nonconformities) ? appData.nonconformities : []).some((nc) => !nc.assignedTo);
    var opts = [];
    if (hasUnassigned) opts.push({ value: "unassigned", label: "Non assign\u00e9" });
    sorted.forEach(function(name) { opts.push({ value: name, label: name }); });
    SectionToolbar.updateFilterOptions("nonconformities", "owner", opts);
  }
  function deleteNC(ncId) {
    showConfirmModal(t('confirms.deleteNc'), t('confirms.deleteNc'), function() {
      var _nc = appData.nonconformities.find(function(n) { return n.id === ncId; });
      appData.nonconformities = appData.nonconformities.filter((n) => n.id !== ncId);
      (appData.actions || []).forEach(function(a) {
        if (Array.isArray(a.ncIds)) a.ncIds = a.ncIds.filter(function(id) { return id !== ncId; });
      });
      appData.actions = (appData.actions || []).filter((a) => !(a.linkType === "nc" && a.linkId === ncId));
            addAuditEntry('delete', 'nc', _nc ? _nc.title : ncId);
      saveData();
      _csiPersistAffectedTabs(['nonconformities', 'actions']); // FIX-CASCADE-PERSIST : persiste les sections croisées nettoyées
      loadNonConformitiesTable();
      loadActionsTable(); // PASS2-301: Refresh actions table after cascade delete
      updateDashboard();
    }, { type: 'danger' });
  }
  function addNC() {
    const criticalAssetOptions = getCriticalAssetOptions();
    openModal(t('modals.addNc'), [
      { name: "title", label: t('fields.title'), required: true },
      { name: "type", label: t('fields.type', 'Type'), type: "select", value: "Mineure", options: NC_TYPE_OPTIONS },
      { name: "status", label: t('fields.status'), type: "select", value: "detection", options: STATUS_OPTIONS },
      { name: "detectionDate", label: t('fields.detectionDate'), type: "date", value: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) },
      {
        name: "assignedTo",
        label: t('fields.owner'),
        type: "select",
        options: [{ value: '', label: '-- ' + t('fields.select', 'Sélectionner') + ' --' }].concat(
            appData.stakeholders.map(function(s) { return { value: s.name, label: s.name }; })
        )
      },
      { name: "auditId", label: t('fields.linkedAudit', 'Audit lié'), type: "select", options: [{ value: '', label: '\u2014 ' + t('labels.select', 'Sélectionner') + ' \u2014' }].concat(appData.audits.map(a => ({ value: a.id, label: a.title }))) },
      { name: "criticalAssetIds", label: t('fields.criticalAssets'), type: "multiselect", value: [], options: criticalAssetOptions },
      { name: "description", label: t('fields.operations', 'Opérations'), type: "textarea" }
    ], (data) => {
      if (!data.title) return false;
      const id = generateId("nc");
      appData.nonconformities.push({
        id,
        title: data.title,
        description: data.description || "",
        type: data.type || "Mineure",
        status: data.status || "detection",
        detectionDate: data.detectionDate || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
        assignedTo: data.assignedTo || "",
        auditId: data.auditId || "",
        criticalAssetIds: Array.isArray(data.criticalAssetIds) ? data.criticalAssetIds : []
      });
            addAuditEntry('add', 'nc', data.title);
      saveData();
      loadNonConformitiesTable();
      updateDashboard();
    });
  }
  window.loadNonConformitiesTable = loadNonConformitiesTable;
  window.editNC = editNC;
  window.deleteNC = deleteNC;
  window.addNC = addNC;

  // modules/audits.js
function getAuditStatusOptions() {
    return [
      { value: "planifie", label: t('auditStatuses.planifie') },
      { value: "en_cours", label: t('auditStatuses.enCours', 'En cours') },
      { value: "realise", label: t('auditStatuses.realise') },
      { value: "annule", label: t('auditStatuses.annule', 'Annulé') }
    ];
}
var AUDIT_STATUS_OPTIONS = getAuditStatusOptions();
var AUDIT_STATUS_LABELS = {};
AUDIT_STATUS_OPTIONS.forEach(function(opt) { AUDIT_STATUS_LABELS[opt.value] = opt.label; });
// Badge de statut Audits — cohérence avec les autres sections (contrôles/actions) qui
// affichent une pastille colorée et non du texte brut. Classes CSS existantes réutilisées.
var AUDIT_STATUS_BADGE = {
  planifie: "status-badge--a_realiser",
  en_cours: "status-badge--en_cours",
  realise: "status-badge--implemente",
  annule: "status-badge--suspendue"
};
function getAuditTypeOptions() {
    return [
      { value: "interne", label: t('auditTypes.interne') },
      { value: "externe", label: t('auditTypes.externe') },
      { value: "certification", label: t('auditTypes.certification', 'Certification') },
      { value: "surveillance", label: t('auditTypes.surveillance', 'Surveillance') }
    ];
}
var AUDIT_TYPE_OPTIONS = getAuditTypeOptions();
var AUDIT_TYPE_LABELS = {};
AUDIT_TYPE_OPTIONS.forEach(function(opt) { AUDIT_TYPE_LABELS[opt.value] = opt.label; });
  function loadAuditsTable() {
    const tbody = document.getElementById("auditsTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('audits')) return;
    tbody.innerHTML = "";
    if (!Array.isArray(appData.audits) || appData.audits.length === 0) {
      var _er = document.createElement('tr'); var _et = document.createElement('td');
      _et.setAttribute('colspan', '7'); _et.className = 'table__empty-state';
      var _ei = document.createElement('i'); _ei.className = 'fas fa-search';
      _ei.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      _et.appendChild(_ei); _et.appendChild(document.createTextNode(t('emptyStates.noAudits', 'Aucun audit enregistré. Cliquez sur "Nouvel Audit" pour planifier un audit.')));
      _er.appendChild(_et); tbody.appendChild(_er); return;
    }
    const _s = SectionToolbar.getSort("audits");
    const _q = (SectionToolbar.getSearch("audits") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const _sortKey = _s ? _s.key : "date";
    const _sortDir = _s ? _s.dir : "desc";
    const sortedAudits = [...appData.audits]
      .filter((a) => !_q || ((a.title || "") + " " + (a.scope || "") + " " + (a.auditor || "")).toLowerCase().indexOf(_q) >= 0).sort((a, b) => {
      let diff = 0;
      switch (_sortKey) {
        case "title": diff = SortUtils.text(a.title, b.title); break;
        case "type": diff = SortUtils.text(a.type || "", b.type || ""); break;
        case "status": diff = SortUtils.text(a.status || "", b.status || ""); break;
        default: diff = SortUtils.date(a.plannedDate, b.plannedDate); break;
      }
      return _sortDir === "desc" ? -diff : diff;
    });
    sortedAudits.forEach((audit) => {
      const row = document.createElement("tr");
      const linkedActionIds = appData.actions
        .filter((a) => a.linkType === "audit" && a.linkId === audit.id)
        .map((a) => a.title || a.id)
        .join(", ");
      const _auditOverdue = _csiIsPastDate(audit.plannedDate) && audit.status !== "realise" && audit.status !== "annule";
      row.innerHTML = `
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(audit.title)}</div></td>
            <td>${escapeHtml(AUDIT_TYPE_LABELS[audit.type] || audit.type || "")}</td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(audit.scope)}</div></td>
            <td${_auditOverdue ? ` class="cell-overdue" title="${escapeHtml(t('audits.overdue', 'Audit en retard'))}"` : ""}>${audit.plannedDate ? new Date(audit.plannedDate).toLocaleDateString() : "—"}</td>
            <td>${audit.auditor ? escapeHtml(audit.auditor) : "—"}</td>
            <td>${audit.status ? `<span class="status-badge ${AUDIT_STATUS_BADGE[audit.status] || ""}">${escapeHtml(AUDIT_STATUS_LABELS[audit.status] || audit.status)}</span>` : "—"}</td>
            <td>${escapeHtml(linkedActionIds) || "—"}</td>
        `;
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger")) return;
        editAudit(audit.id);
      });
      tbody.appendChild(row);
    });
  }
  function editAudit(auditId) {
    const audit = appData.audits.find((a) => a.id === auditId);
    if (!audit) return;
    openModal(t('modals.editAudit'), [
      { name: "title", label: t('fields.auditTitle'), value: audit.title, required: true },
      { name: "type", label: t('fields.type'), type: "select", value: audit.type, options: AUDIT_TYPE_OPTIONS },
      { name: "scope", label: t('fields.scope'), value: audit.scope },
      { name: "plannedDate", label: t('fields.plannedDate'), value: audit.plannedDate, type: "date" },
      { name: "executionDate", label: t('fields.executionDate', "Date d'exécution"), type: "date", value: audit.executionDate },
      { name: "auditor", label: t('fields.auditor'), value: audit.auditor },
      { name: "status", label: t('fields.status'), type: "select", value: audit.status, options: AUDIT_STATUS_OPTIONS },
      { name: "findings", label: t('fields.findings', 'Constatations'), type: "textarea", value: audit.findings },
      { name: "recommendations", label: t('fields.recommendations', 'Recommandations'), type: "textarea", value: audit.recommendations }
    ], (data) => {
      audit.title = data.title;
      audit.type = data.type;
      audit.scope = data.scope;
      audit.plannedDate = data.plannedDate;
      audit.executionDate = data.executionDate;
      audit.auditor = data.auditor;
      audit.status = data.status;
      audit.findings = data.findings;
      audit.recommendations = data.recommendations;
            addAuditEntry('edit', 'audits', audit.title);
      saveData();
      loadAuditsTable();
      updateDashboard();
    }, () => deleteAudit(auditId));
  }
  function deleteAudit(auditId) {
    showConfirmModal(t('confirms.deleteAudit'), t('confirms.deleteAudit'), function() {
      var _aud = appData.audits.find(function(a) { return a.id === auditId; });
      appData.audits = appData.audits.filter((a) => a.id !== auditId);
      (appData.nonconformities || []).forEach(function(nc) { if (nc.auditId === auditId) nc.auditId = ""; });
      appData.actions = (appData.actions || []).filter((a) => !(a.linkType === "audit" && a.linkId === auditId));
            addAuditEntry('delete', 'audits', _aud ? _aud.title : auditId);
      saveData();
      _csiPersistAffectedTabs(['audits', 'nonconformities', 'actions']); // FIX-CASCADE-PERSIST : persiste les sections croisées nettoyées
      loadAuditsTable();
      loadNonConformitiesTable();
      loadActionsTable();
      updateDashboard();
    }, { type: 'danger' });
  }
  function addAudit() {
    openModal(t('modals.addAudit'), [
      { name: "title", label: t('fields.auditTitle'), required: true },
      { name: "type", label: t('fields.type'), type: "select", value: "interne", options: AUDIT_TYPE_OPTIONS },
      { name: "scope", label: t('fields.scope') },
      { name: "plannedDate", label: t('fields.plannedDate'), type: "date" },
      { name: "auditor", label: t('fields.auditor') },
      { name: "status", label: t('fields.status'), type: "select", value: "planifie", options: AUDIT_STATUS_OPTIONS }
    ], (data) => {
      if (!data.title) return false;
      const id = generateId("audit");
      appData.audits.push({
        id,
        title: data.title,
        type: data.type || "interne",
        description: "",
        scope: data.scope,
        plannedDate: data.plannedDate,
        auditor: data.auditor,
        status: data.status || "planifie"
      });
            addAuditEntry('add', 'audits', data.title);
      saveData();
      loadAuditsTable();
      updateDashboard();
    });
  }
  window.loadAuditsTable = loadAuditsTable;
  window.editAudit = editAudit;
  window.deleteAudit = deleteAudit;
  window.addAudit = addAudit;

  // modules/reviews.js
  function loadReviewsTable() {
    const tbody = document.getElementById("reviewsTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('managementReviews')) return;
    tbody.innerHTML = "";
    if (!Array.isArray(appData.reviews) || appData.reviews.length === 0) {
      var _er = document.createElement('tr'); var _et = document.createElement('td');
      _et.setAttribute('colspan', '5'); _et.className = 'table__empty-state';
      var _ei = document.createElement('i'); _ei.className = 'fas fa-user-tie';
      _ei.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      _et.appendChild(_ei); _et.appendChild(document.createTextNode(t('emptyStates.noReviews', 'Aucune revue de direction. Cliquez sur "Nouvelle Revue" pour en créer une.')));
      _er.appendChild(_et); tbody.appendChild(_er); return;
    }
    const _s = SectionToolbar.getSort("managementReviews");
    const _sortDir = _s ? _s.dir : "desc";
    const sortedReviews = [...appData.reviews].sort((a, b) => {
      var diff = SortUtils.date(a.date, b.date);
      return _sortDir === "desc" ? -diff : diff;
    });
    sortedReviews.forEach((review) => {
      const row = document.createElement("tr");
      row.innerHTML = `
            <td>${review.date ? new Date(review.date).toLocaleDateString() : "—"}</td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(review.participants || "")}</div></td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(review.inputs || "")}</div></td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(review.decisions || "")}</div></td>
            <td>${(review.comments || []).length}</td>`;
      row.classList.add("row-clickable");
      row.addEventListener("click", () => {
        editReview(review.id);
      });
      tbody.appendChild(row);
    });
  }
  function addReview() {
    openModal(t('modals.addReview'), [
      { name: "date", label: "Date", type: "date", value: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), required: true },
      { name: "participants", label: t('fields.participants') },
      { name: "inputs", label: t('fields.reviewInputs', 'Entrées de la revue') },
      { name: "decisions", label: t('fields.reviewDecisions', 'Décisions et actions') },
      { name: "comment", label: t('fields.comment'), type: "textarea" }
    ], (data) => {
      if (!data.date) return false;
      const id = generateId("review");
      appData.reviews.push({
        id,
        date: data.date,
        participants: data.participants,
        inputs: data.inputs,
        decisions: data.decisions,
        comments: data.comment ? [{ text: data.comment, date: new Date().toISOString() }] : []
      });
            addAuditEntry('add', 'reviews', data.date);
      saveData();
      loadReviewsTable();
      updateDashboard();
    });
  }
  function editReview(reviewId) {
    const review = appData.reviews.find((r) => r.id === reviewId);
    if (!review) return;
    openModal(t('modals.editReview'), [
      { name: "date", label: "Date", type: "date", value: review.date, required: true },
      { name: "participants", label: t('fields.participants'), value: review.participants },
      { name: "inputs", label: t('fields.reviewInputs', 'Entrées de la revue'), value: review.inputs },
      { name: "decisions", label: t('fields.reviewDecisions', 'Décisions et actions'), value: review.decisions },
      { name: "existingComments", label: t('fields.existingComments'), type: "textarea", value: formatCommentsReadonly(review.comments), readOnly: true },
      { name: "newComment", label: t('fields.newComment'), type: "textarea" }
    ], (data) => {
      review.date = data.date;
      review.participants = data.participants;
      review.inputs = data.inputs;
      review.decisions = data.decisions;
      if (appendComment(review, data.newComment, 10) === 'max') {
        showToast2(t('comments.max', 'Maximum {n} commentaires atteint.').replace('{n}', 10), 'warning');
      }
            addAuditEntry('edit', 'reviews', review.date);
      saveData();
      loadReviewsTable();
      updateDashboard();
    }, () => deleteReview(reviewId));
  }
  function deleteReview(reviewId) {
    showConfirmModal(t('confirms.deleteReview'), t('confirms.deleteReview'), function() {
      var _rev = appData.reviews.find(function(r) { return r.id === reviewId; });
      appData.reviews = appData.reviews.filter((r) => r.id !== reviewId);
            addAuditEntry('delete', 'reviews', _rev ? _rev.date : reviewId);
      saveData();
      loadReviewsTable();
      updateDashboard();
    }, { type: 'danger' });
  }
  // Unification 2026-07-10 : implémentation dédiée remplacée par le système générique
  // (même modale, mêmes garde-fous — id/auteur, plafond, suppression par id).
  function manageReviewComments(reviewId) {
    manageComments('reviews', reviewId, { titlePrefix: t('reviews.manageComments', 'Commentaires de revue') });
  }
  window.loadReviewsTable = loadReviewsTable;
  window.addReview = addReview;
  window.editReview = editReview;
  window.deleteReview = deleteReview;
  window.manageReviewComments = manageReviewComments;

  // modules/stakeholders.js
  // Lot 1b : rôles normalisés des parties prenantes (valeurs stockées = slugs stables).
  // Rétrocompatibilité : les données existantes en texte libre restent affichées telles
  // quelles et sont proposées comme option « (valeur actuelle) » dans le select.
  const STAKEHOLDER_ROLE_SLUGS = ['internal', 'supplier', 'client', 'authority', 'partner', 'other'];
  function stakeholderRoleLabel(role) {
    if (!role) return '';
    return STAKEHOLDER_ROLE_SLUGS.indexOf(role) >= 0 ? t('stakeholderRoles.' + role, role) : role;
  }
  function stakeholderRoleOptions(current) {
    const opts = [{ value: '', label: '—' }].concat(STAKEHOLDER_ROLE_SLUGS.map(function(s) {
      return { value: s, label: t('stakeholderRoles.' + s, s) };
    }));
    if (current && current !== '' && STAKEHOLDER_ROLE_SLUGS.indexOf(current) < 0) {
      opts.push({ value: current, label: current + ' ' + t('stakeholderRoles.currentValue', '(valeur actuelle)') });
    }
    return opts;
  }
  function loadStakeholdersTable() {
    const tbody = document.getElementById("stakeholdersTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('stakeholders')) return;
    tbody.innerHTML = "";
    // Lot 1b : options du filtre Rôle (slugs + valeurs libres héritées présentes)
    if (typeof SectionToolbar !== 'undefined' && SectionToolbar.updateFilterOptions) {
      var _roleOpts = STAKEHOLDER_ROLE_SLUGS.map(function(s) {
        return { value: s, label: t('stakeholderRoles.' + s, s) };
      });
      (appData.stakeholders || []).forEach(function(st) {
        if (st && st.role && STAKEHOLDER_ROLE_SLUGS.indexOf(st.role) < 0 &&
            !_roleOpts.some(function(o) { return o.value === st.role; })) {
          _roleOpts.push({ value: st.role, label: st.role });
        }
      });
      SectionToolbar.updateFilterOptions('stakeholders', 'role', _roleOpts);
    }
    if (!Array.isArray(appData.stakeholders) || appData.stakeholders.length === 0) {
      var _er = document.createElement('tr'); var _et = document.createElement('td');
      _et.setAttribute('colspan', '4'); _et.className = 'table__empty-state';
      var _ei = document.createElement('i'); _ei.className = 'fas fa-users';
      _ei.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      _et.appendChild(_ei); _et.appendChild(document.createTextNode(t('emptyStates.noStakeholders', 'Aucune partie prenante. Cliquez sur "Nouvelle Partie prenante" pour en ajouter.')));
      _er.appendChild(_et); tbody.appendChild(_er); return;
    }
    const _s = SectionToolbar.getSort("stakeholders");
    const _sortDir = _s ? _s.dir : "asc";
    const _filters = (typeof SectionToolbar !== 'undefined' && SectionToolbar.getFilters) ? SectionToolbar.getFilters("stakeholders") : {};
    const _roleFilter = _filters.role || "";
    const _q = (SectionToolbar.getSearch("stakeholders") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const sortedStakeholders = [...appData.stakeholders]
      .filter((st) => !_roleFilter || st.role === _roleFilter)
      .filter((st) => !_q || ((st.name || "") + " " + (st.contact || "")).toLowerCase().indexOf(_q) >= 0)
      .sort((a, b) => {
        var diff = SortUtils.text(a.name, b.name);
        return _sortDir === "desc" ? -diff : diff;
      });
    sortedStakeholders.forEach((st) => {
      const row = document.createElement("tr");
      row.innerHTML = `
            <td>${escapeHtml(st.name || "")}</td>
            <td>${st.role ? escapeHtml(stakeholderRoleLabel(st.role)) : "—"}</td>
            <td>${escapeHtml(st.contact || "")}</td>
            <td><div class="cell-clamp cell-clamp--2">${escapeHtml(st.notes || "")}</div></td>
        `;
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger")) return;
        editStakeholder(st.id);
      });
      tbody.appendChild(row);
    });
  }
  function addStakeholder() {
    openModal(t('modals.addStakeholder'), [
      { name: "name", label: t('fields.name') },
      { name: "role", label: t('fields.role'), type: "select", value: "", options: stakeholderRoleOptions("") },
      { name: "contact", label: t('fields.contact') },
      { name: "notes", label: t('fields.comments') }
    ], (data) => {
      if (!data.name) return false;
      const id = generateId("stakeholder");
      appData.stakeholders.push({
        id,
        name: data.name,
        role: data.role,
        contact: data.contact,
        notes: data.notes
      });
            addAuditEntry('add', 'stakeholders', data.name);
      saveData();
      loadStakeholdersTable();
    });
  }
  function editStakeholder(stakeholderId) {
    const st = appData.stakeholders.find((s) => s.id === stakeholderId);
    if (!st) return;
    const oldName = st.name;
    openModal(t('modals.editStakeholder'), [
      { name: "name", label: t('fields.name'), value: st.name },
      { name: "role", label: t('fields.role'), type: "select", value: st.role || "", options: stakeholderRoleOptions(st.role || "") },
      { name: "contact", label: t('fields.contact'), value: st.contact },
      { name: "notes", label: t('fields.comments'), value: st.notes }
    ], (data) => {
      st.name = data.name;
      st.role = data.role;
      st.contact = data.contact;
      st.notes = data.notes;
      if (oldName !== data.name) {
        (appData.actions || []).forEach(a => {
          if (a.assignedTo === oldName) a.assignedTo = data.name;
        });
        appData.nonconformities.forEach(nc => {
          if (nc.assignedTo === oldName) nc.assignedTo = data.name;
        });
        (appData.risks || []).forEach(function(r) { if (r.owner === oldName) r.owner = data.name; });
        (appData.threats || []).forEach(function(th) { if (th.owner === oldName) th.owner = data.name; });
        (appData.nis2Controls || []).forEach(function(c) {
          if (c.owner === oldName) c.owner = data.name;
          if (c.reviewer === oldName) c.reviewer = data.name;
        });
        (appData.recyfControls || []).forEach(function(c) {
          if (c.owner === oldName) c.owner = data.name;
          if (c.reviewer === oldName) c.reviewer = data.name;
        });
        // FIX-STAKEHOLDER-CASCADE (audit 2026-07-10) : sections « responsable » qui
        // proposent désormais les parties prenantes en datalist — le renommage doit
        // suivre pour éviter les références orphelines.
        (appData.soa || []).forEach(function(e) { if (e.owner === oldName) e.owner = data.name; });
        (appData.documentReview || []).forEach(function(e) { if (e.responsible === oldName) e.responsible = data.name; });
        (appData.controls || []).forEach(function(c) { if (c.owner === oldName) c.owner = data.name; });
        (appData.soclePillars || []).forEach(function(p) { if (p && p.owner === oldName) p.owner = data.name; });
        if (appData.nis2Plan) {
          (appData.nis2Plan.domains || []).forEach(function(d) {
            (d.actions || []).forEach(function(a) { if (a.owner === oldName) a.owner = data.name; });
          });
          (appData.nis2Plan.budgetLines || []).forEach(function(l) { if (l.owner === oldName) l.owner = data.name; });
        }
      }
            addAuditEntry('edit', 'stakeholders', data.name);
      saveData();
      // (audit global 2026-07-03 ; complété revue effets de bord) le renommage cascade sur
      // actions/nonconformities (assignedTo) ET risks/threats (owner) — sections DISTINCTES de
      // stakeholders, toutes mutées ci-dessus. En mode serveur saveData() ne pousse que l'onglet
      // courant : sans ceci, ces références restent l'ancien nom côté serveur (orphelines
      // durables). Pattern FIX-CASCADE-PERSIST des autres cascades. No-op hors mode serveur.
      _csiPersistAffectedTabs(['actions', 'nonconformities', 'risks', 'threats', 'nis2Controls', 'recyfControls', 'soa', 'documentReview', 'controls', 'soclePillars', 'nis2Plan']);
      loadStakeholdersTable();
      loadActionsTable();
      loadNonConformitiesTable();
      if (typeof loadNis2ControlsTable === 'function') loadNis2ControlsTable();
      if (typeof loadRecyfControlsTable === 'function') loadRecyfControlsTable();
    }, () => deleteStakeholder(stakeholderId));
  }
  function deleteStakeholder(stakeholderId) {
    showConfirmModal(t('confirms.deleteStakeholder'), t('confirms.deleteStakeholder'), function() {
      const st = appData.stakeholders.find((s) => s.id === stakeholderId);
      appData.stakeholders = appData.stakeholders.filter((s) => s.id !== stakeholderId);
      if (st) {
        (appData.actions || []).forEach(a => {
          if (a.assignedTo === st.name) a.assignedTo = "";
        });
        (appData.nonconformities || []).forEach(nc => {
          if (nc.assignedTo === st.name) nc.assignedTo = "";
        });
        // Purge references in risks and threats
        (appData.risks || []).forEach(function(r) {
          if (r.owner === st.name) r.owner = '';
        });
        appData.threats.forEach(function(th) {
          if (th.owner === st.name) th.owner = '';
        });
        (appData.audits || []).forEach(function(a) {
          if (a.auditor === st.name) a.auditor = '';
        });
        (appData.nis2Controls || []).forEach(function(c) {
          if (c.owner === st.name) c.owner = '';
          if (c.reviewer === st.name) c.reviewer = '';
        });
        (appData.recyfControls || []).forEach(function(c) {
          if (c.owner === st.name) c.owner = '';
          if (c.reviewer === st.name) c.reviewer = '';
        });
        // FIX-STAKEHOLDER-CASCADE (audit 2026-07-10) : purge des sections « responsable »
        // désormais reliées aux parties prenantes (datalist).
        (appData.soa || []).forEach(function(e) { if (e.owner === st.name) e.owner = ''; });
        (appData.documentReview || []).forEach(function(e) { if (e.responsible === st.name) e.responsible = ''; });
        (appData.controls || []).forEach(function(c) { if (c.owner === st.name) c.owner = ''; });
        (appData.soclePillars || []).forEach(function(p) { if (p && p.owner === st.name) p.owner = ''; });
        if (appData.nis2Plan) {
          (appData.nis2Plan.domains || []).forEach(function(d) {
            (d.actions || []).forEach(function(a) { if (a.owner === st.name) a.owner = ''; });
          });
          (appData.nis2Plan.budgetLines || []).forEach(function(l) { if (l.owner === st.name) l.owner = ''; });
        }
      }
            addAuditEntry('delete', 'stakeholders', st ? st.name : stakeholderId);
      saveData();
      // (audit global 2026-07-03 ; complété revue effets de bord) la suppression cascade sur
      // actions/nonconformities (assignedTo), audits (auditor) ET risks/threats (owner) — toutes
      // purgées ci-dessus, sections distinctes. Persistance explicite en mode serveur (sinon
      // références orphelines durables vers un intervenant supprimé). No-op hors mode serveur.
      _csiPersistAffectedTabs(['actions', 'nonconformities', 'audits', 'risks', 'threats', 'nis2Controls', 'recyfControls', 'soa', 'documentReview', 'controls', 'soclePillars', 'nis2Plan']);
      loadStakeholdersTable();
      loadActionsTable();
      loadNonConformitiesTable();
      if (typeof loadNis2ControlsTable === 'function') loadNis2ControlsTable();
      if (typeof loadRecyfControlsTable === 'function') loadRecyfControlsTable();
      if (typeof updateDashboard === 'function') updateDashboard();
    }, { type: 'danger' });
  }
  window.loadStakeholdersTable = loadStakeholdersTable;
  window.addStakeholder = addStakeholder;
  window.editStakeholder = editStakeholder;
function loadDocumentsTable() {
    const tbody = document.getElementById("documentsTableBody");
    if (!tbody) return;
    if (_csiRenderConsolidatedSectionIfNeeded('documents')) return;
    tbody.innerHTML = "";
    if (!Array.isArray(appData.documents) || appData.documents.length === 0) {
      var _er = document.createElement('tr'); var _et = document.createElement('td');
      _et.setAttribute('colspan', '4'); _et.className = 'table__empty-state';
      var _ei = document.createElement('i'); _ei.className = 'fas fa-book';
      _ei.style.cssText = 'opacity:0.3;font-size:1.5rem;display:block;margin-bottom:8px';
      _et.appendChild(_ei); _et.appendChild(document.createTextNode(t('emptyStates.noDocuments', 'Aucun document enregistré. Cliquez sur "Nouveau Document" pour commencer.')));
      _er.appendChild(_et); tbody.appendChild(_er); return;
    }
    const _s = SectionToolbar.getSort("documents");
    const _q = (SectionToolbar.getSearch("documents") || "").trim().toLowerCase(); // Toolbar v2 : recherche plein texte
    const _sortDir = _s ? _s.dir : "asc";
    const _sortKey = (_s && _s.key) || "level";
    // Lot 1b : tri par défaut = Niveau selon la pyramide documentaire, puis Type.
    const _docLevelOrder = { "Politique": 1, "Processus": 2, "Procédure": 3, "Enregistrement": 4 };
    const sortedDocuments = [...appData.documents]
      .filter((d) => !_q || ((d.category || "") + " " + (d.type || "") + " " + (d.tool || "")).toLowerCase().indexOf(_q) >= 0)
      .sort((a, b) => {
      var diff;
      if (_sortKey === "type") {
        diff = SortUtils.text(a.type || "", b.type || "");
      } else {
        diff = ((_docLevelOrder[a.category] || 99) - (_docLevelOrder[b.category] || 99))
          || SortUtils.text(a.type || "", b.type || "");
      }
      return _sortDir === "desc" ? -diff : diff;
    });
    sortedDocuments.forEach((doc) => {
      const row = document.createElement("tr");
      // Lot 1b : Niveau en badge ; Lien = bouton-icône « Ouvrir » (URL complète en title).
      row.innerHTML = `
            <td>${doc.category ? `<span class="audit-cat-badge">${escapeHtml(doc.category)}</span>` : "—"}</td>
            <td>${escapeHtml(doc.type || "")}</td>
            <td>${escapeHtml(doc.tool || "")}</td>
            <td>${doc.link && isSafeUrl(doc.link) ? `<a class="pr-link-btn" href="${escapeHtml(doc.link)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml(doc.link)}"><i class="fas fa-external-link-alt"></i> ${escapeHtml(t('common.open', 'Ouvrir'))}</a>` : (doc.link ? escapeHtml(doc.link) : "—")}</td>
        `;
      row.classList.add("row-clickable");
      row.addEventListener("click", (e) => {
        if (e.target.closest(".icon-btn--danger")) return;
        if (e.target.closest("a")) return; // Don't open edit modal when clicking a link
        editDocument(doc.id);
      });
      tbody.appendChild(row);
    });
}
function addDocument() {
    openModal(t('modals.addDocument'), [
      { name: "category", label: t('fields.level'), type: "select", options: [
        { value: "Politique", label: t('docCategories.policy', 'Politique') },
        { value: "Processus", label: t('docCategories.process', 'Processus') },
        { value: "Procédure", label: t('docCategories.procedure', 'Procédure') },
        { value: "Enregistrement", label: t('docCategories.record', 'Enregistrement') }
      ] },
      { name: "type", label: t('fields.deliverableType', 'Type de livrable') },
      { name: "tool", label: t('fields.toolUsed', 'Outil utilisé') },
      { name: "link", label: t('fields.accessLink', 'Lien d\'accès') }
    ], (data) => {
      if (!data.type) return false;
      const id = generateId("doc");
      appData.documents.push({ id, category: data.category, type: data.type, tool: data.tool, link: data.link });
            addAuditEntry('add', 'documents', data.type);
      saveData();
      loadDocumentsTable();
      updateDashboard();
    });
}
function editDocument(id) {
    const doc = appData.documents.find((d) => d.id === id);
    if (!doc) return;
    var docCategoryOptions = [
      { value: "Politique", label: t('docCategories.policy', 'Politique') },
      { value: "Processus", label: t('docCategories.process', 'Processus') },
      { value: "Procédure", label: t('docCategories.procedure', 'Procédure') },
      { value: "Enregistrement", label: t('docCategories.record', 'Enregistrement') }
    ];
    if (doc.category && !docCategoryOptions.some(function(o) { return o.value === doc.category; })) {
      docCategoryOptions.push({ value: doc.category, label: doc.category });
    }
    openModal(t('modals.editDocument'), [
      { name: "category", label: t('fields.level'), type: "select", options: docCategoryOptions, value: doc.category },
      { name: "type", label: t('fields.deliverableType', 'Type de livrable'), value: doc.type },
      { name: "tool", label: t('fields.toolUsed', 'Outil utilisé'), value: doc.tool },
      { name: "link", label: t('fields.accessLink', 'Lien d\'accès'), value: doc.link }
    ], (data) => {
      doc.type = data.type;
      doc.category = data.category;
      doc.tool = data.tool;
      doc.link = data.link;
            addAuditEntry('edit', 'documents', doc.type);
      saveData();
      loadDocumentsTable();
      updateDashboard();
    }, () => deleteDocument(id));
}
function deleteDocument(id) {
    showConfirmModal(t('confirms.deleteDocument'), t('confirms.deleteDocument'), function() {
      var _doc = appData.documents.find(function(d) { return d.id === id; });
      appData.documents = appData.documents.filter((d) => d.id !== id);
            addAuditEntry('delete', 'documents', _doc ? _doc.type : id);
      saveData();
      loadDocumentsTable();
      updateDashboard();
    }, { type: 'danger' });
}
window.loadDocumentsTable = loadDocumentsTable;
window.addDocument = addDocument;
window.editDocument = editDocument;
window.deleteDocument = deleteDocument;
  window.deleteStakeholder = deleteStakeholder;

  // modules/setup.js
  function setupNavigation() {
    const navItems = document.querySelectorAll(".nav__item");
    const modules = document.querySelectorAll(".module");
    navItems.forEach((item) => {
      item.addEventListener("click", function() {
        const tabId = this.getAttribute("data-tab");
        navItems.forEach((nav) => { nav.classList.remove("nav__item--active"); nav.setAttribute('aria-selected', 'false'); });
        this.classList.add("nav__item--active");
        this.setAttribute('aria-selected', 'true');
        modules.forEach((module) => module.classList.remove("module--active"));
        const targetModule = document.getElementById(tabId);
        if (targetModule) {
          targetModule.classList.add("module--active");
          loadModuleContent(tabId);
        } else {
          void 0;
        }
        // Update ribbon: show parent category & move active dot
        updateRibbonActiveCategory(this);
      });
    });
    const title = document.getElementById("appTitle");
    if (title) {
      title.addEventListener("click", () => {
        const dashboardBtn = document.querySelector('.nav__item[data-tab="dashboard"]');
        if (dashboardBtn) dashboardBtn.click();
      });
      title.addEventListener("keypress", (e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          const dashboardBtn = document.querySelector('.nav__item[data-tab="dashboard"]');
          if (dashboardBtn) dashboardBtn.click();
        }
      });
    }
    setupRibbonNavigation();
  }

  function setupRibbonNavigation() {
    const cats = document.querySelectorAll('.nav-ribbon__cat');
    const panels = document.querySelectorAll('.nav-ribbon__panel');
    cats.forEach(function(cat) {
      cat.addEventListener('click', function() {
        cats.forEach(function(c) { c.classList.remove('nav-ribbon__cat--selected'); });
        cat.classList.add('nav-ribbon__cat--selected');
        panels.forEach(function(p) { p.classList.remove('nav-ribbon__panel--active'); });
        var target = cat.dataset.category;
        var panel = document.querySelector('[data-panel="' + target + '"]');
        if (panel) panel.classList.add('nav-ribbon__panel--active');
      });
    });
  }

  function updateRibbonActiveCategory(activeItem) {
    var panel = activeItem.closest('.nav-ribbon__panel');
    if (!panel) return;
    var categoryId = panel.dataset.panel;
    // Move the has-active dot
    var cats = document.querySelectorAll('.nav-ribbon__cat');
    cats.forEach(function(c) { c.classList.remove('nav-ribbon__cat--has-active'); });
    var activeCat = document.querySelector('.nav-ribbon__cat[data-category="' + categoryId + '"]');
    if (activeCat) activeCat.classList.add('nav-ribbon__cat--has-active');
    // Also reveal that category's panel
    cats.forEach(function(c) { c.classList.remove('nav-ribbon__cat--selected'); });
    if (activeCat) activeCat.classList.add('nav-ribbon__cat--selected');
    var panels = document.querySelectorAll('.nav-ribbon__panel');
    panels.forEach(function(p) { p.classList.remove('nav-ribbon__panel--active'); });
    panel.classList.add('nav-ribbon__panel--active');
  }
  function setupKpiCardNavigation() {
    const cards = document.querySelectorAll(".kpi-card[data-nav-target]");
    const goTo = (target) => {
      if (!target) return;
      if (_csiDashboardConsolidatedActive() && _csiIsSectionConsolidable(target)) {
        _csiOpenConsolidatedSection(target);
        return;
      }
      if (typeof switchModule === "function") {
        switchModule(target);
        return;
      }
      const navBtn = document.querySelector(`.nav__item[data-tab="${target}"]`);
      if (navBtn) navBtn.click();
      else loadModuleContent(target);
    };
    cards.forEach((card) => {
      const target = card.dataset.navTarget;
      const handler = () => goTo(target);
      card.addEventListener("click", handler);
      card.addEventListener("keypress", (e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          handler();
        }
      });
    });
  }
  function openAuditLogModule() {
    document.querySelectorAll('.nav__item').forEach(n => n.classList.remove('nav__item--active'));
    document.querySelectorAll('.module').forEach(m => m.classList.remove('module--active'));
    var mod = document.getElementById('auditLog');
    if (mod) mod.classList.add('module--active');
    loadModuleContent('auditLog');
  }

  function initSectionToolbars() {
    if (typeof SectionToolbar === 'undefined') return;

    // 1. Risks (sort-only)
    SectionToolbar.init("risks", {
      search: true,
      sort: {
        options: [
          { key: "level", label: t('sort.level', 'Niveau'), default: true, dir: "asc" },
          { key: "title", label: t('sort.title', 'Titre') },
          { key: "impact", label: t('sort.impact', 'Impact') },
          { key: "probability", label: t('sort.probability', 'Probabilité') }
        ]
      },
      onUpdate: function() { loadRisksTable(); }
    });

    // 2. Threats (sort-only)
    SectionToolbar.init("threats-section", {
      search: true,
      sort: {
        options: [
          { key: "level", label: t('sort.level', 'Niveau'), default: true, dir: "asc" },
          { key: "title", label: t('sort.title', 'Titre') },
          { key: "score", label: t('sort.score', 'Score') },
          { key: "status", label: t('sort.status', 'Statut') }
        ]
      },
      onUpdate: function() { loadThreatsTable(); }
    });

    // 3. Audits (sort-only)
    SectionToolbar.init("audits", {
      search: true,
      sort: {
        options: [
          { key: "date", label: t('sort.date', 'Date'), default: true, dir: "desc" },
          { key: "title", label: t('sort.title', 'Titre') },
          { key: "type", label: t('sort.type', 'Type') },
          { key: "status", label: t('sort.status', 'Statut') }
        ]
      },
      onUpdate: function() { loadAuditsTable(); }
    });

    // 4. Management Reviews (sort-only)
    SectionToolbar.init("managementReviews", {
      sort: {
        options: [
          { key: "date", label: t('sort.date', 'Date'), default: true, dir: "desc" }
        ]
      },
      onUpdate: function() { loadReviewsTable(); }
    });

    // 5. Stakeholders — Lot 1b : + filtre par rôle
    SectionToolbar.init("stakeholders", {
      search: true,
      filters: [
        { key: "role", label: t('stakeholders.filterRole', 'Rôle'), options: [] }
      ],
      sort: {
        options: [
          { key: "name", label: t('sort.name', 'Nom'), default: true, dir: "asc" }
        ]
      },
      onUpdate: function() { loadStakeholdersTable(); }
    });

    // 6. Documents (sort-only) — Lot 1b : défaut = Niveau (pyramide documentaire) puis Type
    SectionToolbar.init("documents", {
      search: true,
      sort: {
        options: [
          { key: "level", label: t('sort.level', 'Niveau'), default: true, dir: "asc" },
          { key: "type", label: t('documents.typeCol', 'Type') }
        ]
      },
      onUpdate: function() { loadDocumentsTable(); }
    });

    // 7. KPIs (sort-only)
    SectionToolbar.init("kpis", {
      sort: {
        options: [
          { key: "name", label: t('sort.name', 'Nom'), default: true, dir: "asc" },
          { key: "value", label: t('sort.value', 'Valeur') }
        ]
      },
      onUpdate: function() { loadKpiTable(); }
    });

    // 8. Project Risks (search + filters + sort) — Toolbar v2 : remplace la barre
    // legacy .project-risks__filters (recherche, Bénéficiaire, Statut, checkbox
    // « Recommandations non implémentées » devenue un select à 1 option, Réinitialiser).
    SectionToolbar.init("project-risks", {
      search: true,
      filters: [
        { key: "beneficiary", label: t('projectRisks.beneficiaryLabel', 'Bénéficiaire'), allLabel: t('projectRisks.allBeneficiaries', 'Tous les bénéficiaires'), options: [] },
        { key: "status", label: t('projectRisks.statusFilterLabel', 'Statut'), allLabel: t('projectRisks.allStatuses', 'Tous les statuts'), options: PROJECT_RISK_STATUS_OPTIONS },
        { key: "pending", label: t('projectRisks.recoTitle', 'Recommandations'), options: [
          { value: "pending", label: t('projectRisks.pendingFilter', 'Recommandations non implémentées') }
        ]}
      ],
      sort: {
        options: [
          { key: "priority", label: t('projectRisks.sortPriority', 'Priorité'), default: true, dir: "asc" },
          { key: "name", label: t('projectRisks.sortName', 'Nom du projet') },
          { key: "beneficiary", label: t('projectRisks.sortBeneficiary', 'Bénéficiaire') },
          { key: "status", label: t('projectRisks.sortStatus', 'Statut') },
          { key: "date", label: t('projectRisks.sortDate', 'Date') }
        ]
      },
      onUpdate: function() { loadProjectRisksTable(); }
    });

    // 9. Controls (filters + sort)
    SectionToolbar.init("controls", {
      search: true,
      filters: [
        { key: "category", label: t('controlsISO.allDomains', 'Tous les domaines'), options: [
          { value: "Organisationnel", label: t('domains.organisationnel', 'Organisationnel') + " (5.x)" },
          { value: "Humain", label: t('domains.humain', 'Humain') + " (6.x)" },
          { value: "Physique", label: t('domains.physique', 'Physique') + " (7.x)" },
          { value: "Technologique", label: t('domains.technologique', 'Technologique') + " (8.x)" }
        ]},
        { key: "status", label: t('fields.status', 'Statut'), options: CONTROL_STATUS_OPTIONS },
        { key: "cmm", label: t('sort.maturity', 'Maturité'), options: CMM_LEVEL_OPTIONS }
      ],
      sort: {
        options: [
          { key: "reference", label: t('sort.reference', 'Réf.'), default: true, dir: "asc" },
          { key: "title", label: t('fields.title', 'Titre') },
          { key: "status", label: t('fields.status', 'Statut') },
          { key: "maturity", label: t('sort.maturity', 'Maturité') }
        ]
      },
      onUpdate: function() { loadControlsTable(); }
    });

    // 10. NIS2 Controls (filters + sort)
    SectionToolbar.init("nis2-controls", {
      search: true,
      filters: [
        { key: "function", label: t('controlsNIS2.filterFunction', 'Fonction'), options: [] },
        { key: "category", label: t('controlsNIS2.filterCategory', 'Catégorie'), advanced: true, options: [] },
        { key: "level", label: t('controlsNIS2.filterLevel', 'Niveau'), advanced: true, options: [
          { value: "basic", label: t('cyfun.levelBasic', 'BASIC') },
          { value: "important", label: t('cyfun.levelImportant', 'IMPORTANT') },
          { value: "essential", label: t('cyfun.levelEssential', 'ESSENTIAL') }
        ]},
        { key: "keyMeasure", label: t('controlsNIS2.filterMeasure', 'Mesure'), advanced: true, options: [
          { value: "key", label: t('controlsNIS2.keyMeasures', 'Mesures clés') },
          { value: "other", label: t('controlsNIS2.otherMeasures', 'Autres mesures') }
        ]},
        { key: "scoring", label: t('controlsNIS2.filterScoring', 'Évaluation'), options: [
          { value: "scored", label: t('controlsNIS2.scored', 'Évalués') },
          { value: "unscored", label: t('controlsNIS2.unscored', 'Non évalués') }
        ]},
        { key: "owner", label: t('controlsNIS2.filterOwner', 'Responsable'), advanced: true, options: [] },
        { key: "reviewDue", label: t('controlsNIS2.filterReview', 'Revue'), advanced: true, options: [] }
      ],
      sort: {
        options: [
          { key: "reference", label: t('sort.reference', 'Réf.'), default: true, dir: "asc" },
          { key: "function", label: t('sort.function', 'Fonction') },
          { key: "owner", label: t('controlsNIS2.owner', 'Responsable') },
          { key: "nextReviewDate", label: t('controlsNIS2.nextReviewDate', 'Prochaine revue') },
          { key: "docMaturity", label: t('sort.docMaturity', 'Doc.') },
          { key: "implMaturity", label: t('sort.implMaturity', 'Impl.') }
        ]
      },
      onUpdate: function() { loadNis2ControlsTable(); }
    });

    // 10b. ReCyF Controls (France / ANSSI) — filtres + tri
    SectionToolbar.init("recyf-controls", {
      search: true,
      filters: [
        { key: "objective", label: t('recyfControls.filterObjective', 'Objectif'), options: [] },
        { key: "theme", label: t('recyfControls.filterTheme', 'Thématique'), advanced: true, options: [] },
        { key: "target", label: t('recyfControls.filterTarget', 'Cible'), advanced: true, options: [
          { value: "EI", label: t('recyfControls.eiEe', 'Entités Importante et Essentielle') },
          { value: "EE_only", label: t('recyfControls.eeOnly', 'Entité Essentielle uniquement') }
        ]},
        { key: "conformity", label: t('recyfControls.filterConformity', 'Conformité'), options: (window.RECYF_CONFORMITY_OPTIONS || []).map(function(o) {
          var en = window.CSI_i18n && CSI_i18n.getLanguage && CSI_i18n.getLanguage() === 'en';
          return { value: o.value, label: (en && o.labelEn) ? o.labelEn : o.label };
        }) },
        { key: 'owner', label: t('recyfControls.filterOwner', 'Responsable'), advanced: true, options: [] },
        { key: 'reviewDue', label: t('recyfControls.filterReview', 'Revue'), advanced: true, options: [] }
      ],
      sort: {
        options: [
          { key: "reference", label: t('sort.reference', 'Réf.'), default: true, dir: "asc" },
          { key: "objective", label: t('recyfControls.objective', 'Objectif') },
          { key: "owner", label: t('recyfControls.owner', 'Responsable') },
          { key: "nextReviewDate", label: t('recyfControls.nextReviewDate', 'Prochaine revue') }
        ]
      },
      onUpdate: function() { loadRecyfControlsTable(); }
    });

    // 11. SoA (filters + sort)
    SectionToolbar.init("soa", {
      search: true,
      filters: [
        { key: "applicability", label: t('soa.applicability', 'Applicabilité'), options: [
          { value: "selected", label: t('soa.applicable', 'Applicable') },
          { value: "not_selected", label: t('soa.nonApplicable', 'Non applicable') }
        ]},
        { key: "domain", label: t('controlsISO.allDomains', 'Tous les domaines'), options: [
          { value: "Organisationnel", label: t('domains.organisationnel', 'Organisationnel') },
          { value: "Humain", label: t('domains.humain', 'Humain') },
          { value: "Physique", label: t('domains.physique', 'Physique') },
          { value: "Technologique", label: t('domains.technologique', 'Technologique') }
        ]}
      ],
      sort: {
        options: [
          { key: "reference", label: t('sort.reference', 'Réf.'), default: true, dir: "asc" },
          { key: "applicability", label: t('soa.applicability', 'Applicabilité') },
          { key: "implementation", label: t('sort.implementation', 'Implémentation') }
        ]
      },
      onUpdate: function() { loadSoaTable(); }
    });

    // 12. Document Review (filters + sort)
    SectionToolbar.init("docReview", {
      filters: [
        { key: "status", label: t('sort.status', 'Statut'), options: [
          { value: "conforme", label: t('docReview.conforming', 'Conformes') },
          { value: "partiel", label: t('docReview.partial', 'Partiels') },
          { value: "non_conforme", label: t('docReview.nonConforming', 'Non conformes') },
          { value: "na", label: t('soa.notApplicable', 'Non applicable') }
        ]}
      ],
      sort: {
        options: [
          { key: "clause", label: t('sort.clause', 'Clause'), default: true, dir: "asc" },
          { key: "status", label: t('sort.status', 'Statut') },
          { key: "lastReview", label: t('sort.lastReview', 'Dernière revue') }
        ]
      },
      onUpdate: function() { loadDocReviewTable(); }
    });

    // 13. Actions (filters + sort)
    SectionToolbar.init("actions", {
      search: true,
      filters: [
        { key: "filterType", label: t('sort.type', 'Type'), options: [
          { value: "risk", label: t('actionLinkTypes.risk', 'Risque') },
          { value: "nc", label: t('actionLinkTypes.nc', 'Non-conformité') },
          { value: "audit", label: t('actionLinkTypes.audit', 'Audit') },
          { value: "threat", label: t('actionLinkTypes.threat', 'Menace') },
          { value: "owner", label: t('sort.owner', 'Responsable') },
          { value: "continuous", label: t('actionLinkTypes.continuous', 'Amélioration continue') }
        ]},
        { key: "filterValue", label: t('sort.value', 'Valeur'), options: [] },
        { key: "status", label: t('sort.status', 'Statut'), options: [
          { value: "planifie", label: t('actionStatuses.planifie', 'Planifié') },
          { value: "en_cours", label: t('actionStatuses.enCours', 'En cours') },
          { value: "termine", label: t('actionStatuses.termine', 'Terminé') },
          { value: "annule", label: t('actionStatuses.annule', 'Annulé') }
        ]}
      ],
      sort: {
        options: [
          { key: "priority", label: t('sort.priority', 'Priorité'), default: true, dir: "asc" },
          { key: "id", label: t('sort.id', 'ID') },
          { key: "status", label: t('sort.status', 'Statut') },
          { key: "dueDate", label: t('sort.dueDate', 'Échéance') },
          { key: "progress", label: t('sort.progress', 'Progression') },
          { key: "link", label: t('sort.link', 'Lien') }
        ]
      },
      onUpdate: function() { loadActionsTable(); }
    });

    // 14. Non-conformities (filters + sort)
    SectionToolbar.init("nonconformities", {
      search: true,
      filters: [
        { key: "audit", label: t('sort.audit', 'Audit'), options: [] },
        { key: "owner", label: t('sort.owner', 'Responsable'), options: [] }
      ],
      sort: {
        options: [
          { key: "type", label: t('sort.type', 'Type'), default: true, dir: "asc" },
          { key: "status", label: t('sort.status', 'Statut') },
          { key: "date", label: t('sort.date', 'Date') },
          { key: "audit", label: t('sort.audit', 'Audit') },
          { key: "owner", label: t('sort.owner', 'Responsable') }
        ]
      },
      onUpdate: function() { loadNonConformitiesTable(); }
    });

    // 15. Critical Assets (filters + sort)
    SectionToolbar.init("critical-assets", {
      search: true,
      filters: [
        { key: "beneficiary", label: t('criticalAssets.filterBeneficiary', 'Bénéficiaire'), options: [] },
        { key: "availability", label: t('criticalAssets.filterAvailability', 'Disponibilité'), options: [] },
        { key: "impact", label: t('criticalAssets.filterImpact', 'Impact'), options: [] }
      ],
      sort: {
        options: [
          { key: "priority", label: t('sort.priority', 'Priorité'), default: true, dir: "asc" },
          { key: "name", label: t('sort.name', 'Nom') }
        ]
      },
      onUpdate: function() { loadCriticalAssetsTable(); }
    });
  }

  // ── Refonte header 2026-07 : menu utilisateur ─────────────────────────────
  // Clone du pattern accessible de _buildScopeSwitcher (modules/entity-ui.js) :
  // aria-haspopup/expanded, role=menu/menuitem, Échap + refocus, flèches haut/bas,
  // fermeture au clic externe (mousedown capture). Le déclencheur est un avatar
  // (initiales du user courant en mode serveur, icône « ⋯ » en local sans login).
  function _userInitials(name) {
    var parts = String(name || "").trim().split(/\s+/).filter(Boolean);
    var ini = parts.length >= 2
      ? parts[0].charAt(0) + parts[1].charAt(0)
      : String(name || "").trim().slice(0, 2);
    return ini.toUpperCase();
  }

  function refreshUserMenuIdentity() {
    var avatar = document.getElementById("userMenuAvatar");
    var nameEl = document.getElementById("userMenuName");
    var head = document.getElementById("userMenuHead");
    var headName = document.getElementById("userMenuHeadName");
    var headRole = document.getElementById("userMenuHeadRole");
    var trigger = document.getElementById("userMenuBtn");
    var user = null;
    if (typeof isServerModeUnlocked === "function" && isServerModeUnlocked()
        && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn && window.CSI_AuthManager.isLoggedIn()) {
      user = window.CSI_AuthManager.getCurrentUser();
    }
    if (user && user.name) {
      // textContent uniquement : le nom vient du serveur, jamais interprété en HTML.
      if (avatar) avatar.textContent = _userInitials(user.name);
      if (nameEl) nameEl.textContent = user.name;
      var roleKey = user.role === "admin" ? "users.roleAdmin"
        : (user.role === "viewer" ? "users.roleViewer" : "users.roleEditor");
      if (head) head.style.display = "";
      if (headName) headName.textContent = user.name;
      if (headRole) headRole.textContent = t(roleKey);
      if (trigger) trigger.title = user.name;
    } else {
      if (avatar) avatar.innerHTML = '<i class="fas fa-ellipsis" aria-hidden="true"></i>';
      if (nameEl) nameEl.textContent = "";
      if (head) head.style.display = "none";
      if (trigger) trigger.title = "";
    }
  }

  function initUserMenu() {
    var wrap = document.getElementById("csiUserMenu");
    if (!wrap || wrap._csiUserMenuInit) { refreshUserMenuIdentity(); return; }
    wrap._csiUserMenuInit = true;
    var trigger = document.getElementById("userMenuBtn");
    var menu = wrap.querySelector(".csi-usermenu__menu");
    if (!trigger || !menu) return;
    var isOpen = false;
    function onDocDown(ev) {
      if (!wrap.contains(ev.target)) close(false);
    }
    function close(refocus) {
      if (!isOpen) return;
      isOpen = false;
      wrap.classList.remove("csi-usermenu--open");
      trigger.setAttribute("aria-expanded", "false");
      document.removeEventListener("mousedown", onDocDown, true);
      if (refocus) { try { trigger.focus(); } catch (e) { /* environnement sans focus */ } }
    }
    function openMenu() {
      if (isOpen) return;
      isOpen = true;
      wrap.classList.add("csi-usermenu--open");
      trigger.setAttribute("aria-expanded", "true");
      document.addEventListener("mousedown", onDocDown, true);
    }
    function _navItems() {
      var all = menu.querySelectorAll("button.csi-usermenu__item");
      var out = [];
      for (var i = 0; i < all.length; i++) {
        if (all[i].offsetParent !== null || all[i].getClientRects().length) out.push(all[i]);
      }
      return out;
    }
    trigger.addEventListener("click", function() { if (isOpen) close(true); else openMenu(); });
    // Un clic sur un item ferme le menu — SAUF le thème : il cycle 4 thèmes, on laisse
    // le menu ouvert pour enchaîner (le libellé « Thème : … » se met à jour en direct).
    menu.addEventListener("click", function(ev) {
      var node = ev.target;
      while (node && node !== menu && !(node.classList && node.classList.contains("csi-usermenu__item"))) {
        node = node.parentNode;
      }
      if (!node || node === menu) return;
      if (node.id === "themeToggleBtn") return;
      close(true);
    });
    wrap.addEventListener("keydown", function(ev) {
      if (ev.key === "Escape") {
        if (isOpen) { ev.preventDefault(); ev.stopPropagation(); close(true); }
        return;
      }
      if (ev.key !== "ArrowDown" && ev.key !== "ArrowUp") return;
      if (!isOpen) { if (ev.key === "ArrowDown") { ev.preventDefault(); openMenu(); } return; }
      var items = _navItems();
      if (!items.length) return;
      ev.preventDefault();
      var idx = -1;
      for (var i = 0; i < items.length; i++) { if (items[i] === document.activeElement) { idx = i; break; } }
      var next = (ev.key === "ArrowDown") ? (idx + 1) % items.length : (idx <= 0 ? items.length - 1 : idx - 1);
      try { items[next].focus(); } catch (e) { /* environnement sans focus */ }
    });
    refreshUserMenuIdentity();
  }

  function setupEventListeners() {
    const mapping = [
      { id: "addControlBtn", handler: addControl },
      { id: "addActionBtn", handler: addAction },
      { id: "addCriticalAssetBtn", handler: () => openCriticalAssetModal() },
      { id: "editCriticalAssetsTitlesBtn", handler: openCriticalAssetsTitlesModal },
      { id: "addProjectRiskBtn", handler: addProjectRisk },
      { id: "manageBudgetCategoriesBtn", handler: () => openBudgetCategoriesModal() },
      { id: "manageBudgetYearsBtn", handler: () => openBudgetYearsModal() },
      { id: "addBudgetLineBtn", handler: () => openBudgetLineModal() },
      { id: "addRiskBtn", handler: addRisk },
      { id: "addThreatBtn", handler: () => showThreatForm() },
      { id: "addNcBtn", handler: addNC },
      { id: "addAuditBtn", handler: addAudit },
      { id: "addReviewBtn", handler: addReview },
      { id: "addStakeholderBtn", handler: addStakeholder },
      { id: "addDocumentBtn", handler: addDocument },
      { id: "addKpiBtn", handler: addKpi },
      { id: "manualBtn", handler: openManual },
      { id: "settingsBtn", handler: openSettings },
      { id: "serverSyncBtn", handler: handleServerSyncButton },
      { id: "editModeBtn", handler: handleEditModeButton },
      { id: "modeBannerTakeoverBtn", handler: handleEditLockTakeover },
      { id: "auditLogBtn", handler: openAuditLogModule },
      { id: "dashboardActivity", handler: openAuditLogModule },
      { id: "themeToggleBtn", handler: toggleTheme },
      { id: "exportSoaPdfPublic", handler: () => exportSoaPdf("public") },
      { id: "exportSoaPdfAudit", handler: () => exportSoaPdf("audit") },
      { id: "exportDocReviewPdfAudit", handler: exportDocReviewPdf }
    ];
    mapping.forEach(({ id, handler }) => {
      const el = document.getElementById(id);
      if (el) el.addEventListener("click", handler);
    });
    const nis2PhaseFilter = document.getElementById("nis2PhaseFilter");
    const nis2PillarFilter = document.getElementById("nis2PillarFilter");
    [nis2PhaseFilter, nis2PillarFilter].forEach((el) => {
      if (el) el.addEventListener("change", renderNis2Plan);
    });
    // CyFun level selection is now rendered dynamically in renderFrameworksSection()
    // CyFun level popup - confirm button
    // Prépare la modale NIS2 (1ʳᵉ config) : ajoute le choix du référentiel Belgique/France
    // si ReCyF est disponible. Sans ReCyF → modale CyFun pure (comportement inchangé).
    function prepareNis2LevelModal() {
      var modal = document.getElementById('cyfunLevelModal');
      if (!modal) return;
      // ReCyF est proposé dès qu'il est DISPONIBLE (chargé), pas seulement « activé » : dans le
      // modèle mutuellement exclusif, recyf2025 n'est dans enabledFrameworks que si la France est
      // déjà active — utiliser isEnabled empêcherait un utilisateur Belgique de choisir la France.
      var recyfEnabled = !!(window._csiFrameworks && window._csiFrameworks['recyf2025']);
      var refBlock = document.getElementById('cyfunModalRefBlock');
      var beBlock = document.getElementById('cyfunModalBeBlock');
      var frBlock = document.getElementById('cyfunModalFrBlock');
      var titleEl = document.getElementById('cyfunLevelModalTitle');
      if (!recyfEnabled) {
        if (refBlock) refBlock.style.display = 'none';
        if (frBlock) frBlock.style.display = 'none';
        if (beBlock) beBlock.style.display = '';
        return;
      }
      if (refBlock) refBlock.style.display = '';
      if (titleEl) titleEl.textContent = t('wizard.nis2SetupTitle', 'Référentiel NIS2');
      var curRef = (typeof appData !== 'undefined' && appData.nis2Referential) || 'cyfun2025';
      var refRadio = modal.querySelector('input[name="cyfunModalRefChoice"][value="' + curRef + '"]');
      if (refRadio) refRadio.checked = true;
      var degRadio = modal.querySelector('input[name="cyfunModalDegreeChoice"][value="' + ((typeof appData !== 'undefined' && appData.recyfLevel) || 'importante') + '"]');
      if (degRadio) degRadio.checked = true;
      function syncBlocks() {
        var sel = modal.querySelector('input[name="cyfunModalRefChoice"]:checked');
        var v = sel ? sel.value : 'cyfun2025';
        if (beBlock) beBlock.style.display = (v === 'cyfun2025') ? '' : 'none';
        if (frBlock) frBlock.style.display = (v === 'recyf2025') ? '' : 'none';
      }
      if (!modal._nis2RefWired) {
        var radios = modal.querySelectorAll('input[name="cyfunModalRefChoice"]');
        for (var i = 0; i < radios.length; i++) radios[i].addEventListener('change', syncBlocks);
        modal._nis2RefWired = true;
      }
      syncBlocks();
    }
    window.prepareNis2LevelModal = prepareNis2LevelModal;

    const cyfunLevelConfirmBtn = document.getElementById("cyfunLevelConfirmBtn");
    if (cyfunLevelConfirmBtn) {
      cyfunLevelConfirmBtn.addEventListener("click", function() {
        // Référentiel NIS2 : France (ReCyF) si choisi dans la modale, sinon Belgique (CyFun).
        var _refBlock = document.getElementById('cyfunModalRefBlock');
        var _refChoiceEl = (_refBlock && _refBlock.style.display !== 'none')
          ? document.querySelector('input[name="cyfunModalRefChoice"]:checked') : null;
        if (_refChoiceEl && _refChoiceEl.value === 'recyf2025') {
          var _degEl = document.querySelector('input[name="cyfunModalDegreeChoice"]:checked');
          var _degree = _degEl ? _degEl.value : 'importante';
          appData.nis2Referential = 'recyf2025';
          appData._nis2ReferentialLocked = true; // 1er choix confirmé -> référentiel figé (Reset pour changer)
          if (typeof setRecyfDegree === 'function') setRecyfDegree(_degree);
          appData._cyfunLevelChosen = true;
          if (typeof reconcileNis2Frameworks === 'function') reconcileNis2Frameworks();
          if (window.CSI_FrameworkMapper && typeof CSI_FrameworkMapper.applyFrameworkVisibility === 'function') CSI_FrameworkMapper.applyFrameworkVisibility();
          else if (typeof applyNis2Referential === 'function') applyNis2Referential(false);
          if (typeof saveData === 'function') saveData();
          // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
          // FIX-CONFIG-PERSIST-LOCK (revue prod 2026-06-11)
          _persistConfigWithLock(['nis2Referential', '_nis2ReferentialLocked', 'recyfLevel', 'recyfControls', '_cyfunLevelChosen', 'frameworkMapper']);
          if (typeof updateDashboard === 'function') updateDashboard();
          var _m = document.getElementById("cyfunLevelModal");
          if (_m) _m.classList.remove("modal-overlay--visible");
          showToast(t('toasts.recyfConfigured', 'Référentiel NIS2 : France (ReCyF) — {{deg}}').replace('{{deg}}', _degree === 'essentielle' ? 'EE' : 'EI'), 'success');
          return;
        }
        appData.nis2Referential = 'cyfun2025';
        appData._nis2ReferentialLocked = true; // 1er choix confirmé -> référentiel figé (Reset pour changer)
        var selected = document.querySelector('input[name="cyfunLevelChoice"]:checked');
        var level = selected ? selected.value : 'basic';
        // Language choice
        var langSelected = document.querySelector('input[name="cyfunLangChoice"]:checked');
        var lang = langSelected ? langSelected.value : 'en';
        appData.cyfunLang = lang;
        setCyfunAssuranceLevel(level);
        recalcNis2Maturity();
        appData._cyfunLevelChosen = true;
        if (typeof reconcileNis2Frameworks === 'function') reconcileNis2Frameworks();
        if (window.CSI_FrameworkMapper && typeof CSI_FrameworkMapper.applyFrameworkVisibility === 'function') CSI_FrameworkMapper.applyFrameworkVisibility();
        else if (typeof applyNis2Referential === 'function') applyNis2Referential(false);
        // Sync settings dropdown
        var _s = document.getElementById('cyfunLevelSelect');
        if (_s) _s.value = level;
        // Reset filters via SectionToolbar
        SectionToolbar.updateFilterOptions('nis2-controls', 'function', []);
        SectionToolbar.updateFilterOptions('nis2-controls', 'category', []);
        loadNis2ControlsTable();
        if (typeof updateNis2RadarChart === 'function') updateNis2RadarChart();
        if (typeof renderNis2ProgramRadar === 'function') renderNis2ProgramRadar();
        if (typeof renderNis2Plan === 'function') renderNis2Plan();
        saveData();
        // FIX-GLOBAL-SYNC: Push cyfunLevel choice to server for other users
        // FIX-FULLSYNC-STALE: clés déclarées — ne pas écraser l'état serveur complet
        if (window.CSI_SyncManager && window.CSI_SyncManager.forceServerSync) {
          window.CSI_SyncManager.forceServerSync(['nis2Referential', '_nis2ReferentialLocked', 'cyfunLevel', 'cyfunLang', '_cyfunLevelChosen', 'nis2Controls', '_cyfunEvalArchive', 'targetMaturity', 'frameworkMapper']).catch(function() {});
        }
        if (typeof updateDashboard === 'function') updateDashboard();
        // Hide modal
        var modal = document.getElementById("cyfunLevelModal");
        if (modal) { modal.classList.remove("modal-overlay--visible"); }
        showToast(t('toasts.nis2LevelConfigured', 'Niveau NIS2 configuré : {{level}} ({{lang}})').replace('{{level}}', level.toUpperCase()).replace('{{lang}}', (lang === 'fr' ? 'Fran\u00e7ais' : (lang === 'nl' ? 'Nederlands' : 'English'))), 'success');
      });
    }
    // CyFun level modal — add Skip button and Escape key handler
    (function() {
      var cyfunModal = document.getElementById("cyfunLevelModal");
      if (!cyfunModal) return;
      // Add "Passer" (Skip) button next to confirm button
      var footer = cyfunModal.querySelector(".modal__footer");
      if (footer && !document.getElementById("cyfunLevelSkipBtn")) {
        var skipBtn = document.createElement("button");
        skipBtn.id = "cyfunLevelSkipBtn";
        skipBtn.className = "btn btn--secondary";
        skipBtn.type = "button";
        skipBtn.textContent = t('common.skip', 'Passer');
        skipBtn.style.marginRight = "8px";
        footer.insertBefore(skipBtn, footer.firstChild);
        skipBtn.addEventListener("click", function() {
          cyfunModal.classList.remove("modal-overlay--visible");
        });
      }
      // Escape key handler for cyfunLevelModal
      cyfunModal._cyfunEscHandler = function(evt) {
        if (evt.key === "Escape" && cyfunModal.classList.contains("modal-overlay--visible")) {
          cyfunModal.classList.remove("modal-overlay--visible");
          evt.preventDefault();
          evt.stopPropagation();
        }
      };
      document.addEventListener("keydown", cyfunModal._cyfunEscHandler);
    })();
    // Toolbar v2 : la barre legacy Risques projets a été retirée — recherche,
    // filtres, tri et réinitialisation sont gérés par la SectionToolbar.
    setupExcelExportButtons();
    setupKpiCardNavigation();
    setupNis2PlanInteractions();
    setupDashboardInteractions();
    initSectionToolbars();
    // beforeunload handler is registered in initAutoSave() - no duplicate needed here
  }

  // Interactions Dashboard (filtres, export, refresh)
  function setupDashboardInteractions() {
    // Dashboard time filters (30j, 90j, 1an) — masqués tant que l'échantillonnage
    // historique par période n'est pas implémenté (ils ne filtraient rien et
    // paraissaient cassés grisés). À ré-afficher le jour où les périodes seront gérées.
    const chartFilters = document.getElementById('chartFilters');
    if (chartFilters) {
      chartFilters.style.display = 'none';
    }

    // Bouton export PDF
    const exportBtn = document.getElementById('exportDashboardBtn');
    if (exportBtn) {
      exportBtn.addEventListener('click', exportDashboardPDF);
    }
  }

  // Export Dashboard en PDF
  function exportDashboardPDF() {
    if (DEMO_MODE) {
      showDemoBlockedMessage('L\'export Dashboard');
      return;
    }
    if (_exportBusy) return;
    _exportBusy = true;
    showToast2(t('exportLabels.preparingPdf'), 'info');
    var _dashBtn = document.getElementById('exportDashboardBtn');
    if (_dashBtn) _dashBtn.disabled = true;

    // (audit 2026-07-10) html2canvas n'est PAS chargé par index.html : il l'était seulement en
    // lazy par l'export NIS2, donc sur une session fraîche le bouton retombait silencieusement sur
    // l'impression au lieu d'un vrai PDF. On le charge à la demande (comme nis2-pdf-client.js) et on
    // ne bascule sur fallbackPrintPDF que si le chargement échoue réellement.
    var _runDashCapture = function() {
      const dashboard = document.getElementById('dashboard');
      // (audit global 2026-07-03, thème×impression) html2canvas capture le DOM VIVANT — @media
      // print ne s'y applique pas. On neutralise le thème sombre le temps de la capture (fond
      // blanc, texte foncé) puis on restaure, pour un PDF de dashboard lisible et sobre.
      var _prevTheme = document.body.className.match(/theme-[a-z-]+/g) || [];
      var _restoreTheme = function() {
        document.body.classList.remove('theme-light');
        _prevTheme.forEach(function(c) { document.body.classList.add(c); });
      };
      _prevTheme.forEach(function(c) { document.body.classList.remove(c); });
      document.body.classList.add('theme-light');
      html2canvas(dashboard, {
        scale: 2,
        useCORS: true,
        logging: false,
        backgroundColor: '#ffffff'
      }).then(canvas => {
        _restoreTheme();
        const dateStamp = new Date().toISOString().split('T')[0];
        const { jsPDF } = window.jspdf || {};
        if (jsPDF) {
          // Génerer un vrai PDF avec l'image du dashboard
          const imgData = canvas.toDataURL('image/png');
          const pxW = canvas.width;
          const pxH = canvas.height;
          const orientation = pxW > pxH ? 'landscape' : 'portrait';
          const pdf = new jsPDF({ orientation, unit: 'mm', format: 'a4' });
          const pageW = pdf.internal.pageSize.getWidth();
          const pageH = pdf.internal.pageSize.getHeight();
          const margin = 10;
          const maxW = pageW - 2 * margin;
          const maxH = pageH - 2 * margin;
          const ratio = Math.min(maxW / pxW, maxH / pxH);
          const imgW = pxW * ratio;
          const imgH = pxH * ratio;
          const x = (pageW - imgW) / 2;
          const y = (pageH - imgH) / 2;
          pdf.addImage(imgData, 'PNG', x, y, imgW, imgH);
          pdf.save('dashboard-smsi' + _csiExportScopeSlug() + '-' + dateStamp + '.pdf');
        } else {
          // Fallback PNG si jsPDF non disponible
          const link = document.createElement('a');
          link.download = 'dashboard-smsi' + _csiExportScopeSlug() + '-' + dateStamp + '.png';
          link.href = canvas.toDataURL('image/png');
          link.click();
        }
        showToast2(t('toasts.dashboardExported', 'Dashboard exporté'), 'success');
        _exportBusy = false;
        if (_dashBtn) _dashBtn.disabled = false;
      }).catch(err => {
        _restoreTheme(); // (audit 2026-07-03) restaurer le thème même si la capture échoue
        void 0;
        _exportBusy = false;
        if (_dashBtn) _dashBtn.disabled = false;
        fallbackPrintPDF();
      });
    };

    var _giveUpToPrint = function() {
      _exportBusy = false;
      if (_dashBtn) _dashBtn.disabled = false;
      fallbackPrintPDF();
    };

    if (typeof html2canvas !== 'undefined') {
      _runDashCapture();
    } else {
      var _h2cScript = document.createElement('script');
      _h2cScript.src = 'vendor/html2canvas.min.js';
      _h2cScript.onload = function() {
        if (typeof html2canvas !== 'undefined') _runDashCapture();
        else _giveUpToPrint();
      };
      _h2cScript.onerror = _giveUpToPrint;
      document.head.appendChild(_h2cScript);
    }
  }

  function fallbackPrintPDF() {
    // Créer une version imprimable
    const dashboard = document.getElementById('dashboard');
    if (!dashboard) return;
    const printContent = dashboard.cloneNode(true);

    // Créer une fenêtre d'impression
    const printWindow = window.open('', '_blank');
    if (!printWindow) {
      showToast2(t('toasts.popupBlocked', 'Veuillez autoriser les popups pour exporter en PDF.'), 'error');
      return;
    }
    printWindow.document.write(`
      <!DOCTYPE html>
      <html>
      <head>
        <title>Dashboard SMSI - Export</title>
        <link rel="stylesheet" href="style.css">
        <style>
          body { background: white; padding: 20px; }
          .dashboard-footer { display: none; }
          @media print {
            body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
          }
        </style>
      </head>
      <body class="${document.body.className}">
        ${printContent.outerHTML}
      </body>
      </html>
    `);
    printWindow.document.close();

    setTimeout(() => {
      printWindow.print();
      showToast2(t('toasts.useSaveAsPdf', 'Utilisez "Enregistrer en PDF" dans la boîte de dialogue'), 'info');
    }, 500);
  }

  // ============================================================
  // EXPORT PDF - SOA (Déclaration d'applicabilité) - ISO 27001:2022 Clause 6.1.3
  // ============================================================

  function exportSoaPdf(type = "public") {
    if (DEMO_MODE) {
      showDemoBlockedMessage('L\'export SOA PDF');
      return;
    }
    if (_exportBusy) return;
    _exportBusy = true;
    // FIX-EXPORT-CURRENTLANG (audit 2026-06-10) : currentLang n'existait nulle part en
    // portée → ReferenceError au clic, et _exportBusy restait bloqué à true (tous les
    // exports PDF devenaient des no-ops jusqu'au rechargement).
    const currentLang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : "fr";
    const isAudit = type === "audit";
    const orgName = appData.settings?.organizationName || "Organisation";
    const today = new Date().toLocaleDateString(getExportLocale());
    const docVersion = appData.settings?.soaVersion || "1.0";
    const docDate = new Date().toISOString().split('T')[0];

    // Préparer les données
    const sortedControls = [...appData.controls].sort((a, b) => SortUtils.reference(a.reference, b.reference));

    // Stats avancées
    let applicable = 0, notApplicable = 0, implemented = 0, verified = 0;
    let withOwner = 0, withEvidence = 0, withRisks = 0, reviewedLast6Months = 0;
    const domainStats = {};
    const sourceStats = {};
    const sixMonthsAgo = new Date();
    sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);

    sortedControls.forEach(control => {
      const entry = appData.soa.find(e => e.controlId === control.id) || {};
      const domain = getControlDomain(control.reference);
      if (!domainStats[domain]) domainStats[domain] = { total: 0, applicable: 0, implemented: 0, verified: 0 };
      domainStats[domain].total++;

      if (entry.applicable !== false) {
        applicable++;
        domainStats[domain].applicable++;
        var _effImplPdf = _soaEffectiveImplStatus(entry, control); // (audit 2026-07-03) fallback control.status
        if (_effImplPdf === "implemented" || _effImplPdf === "verified") {
          implemented++;
          domainStats[domain].implemented++;
          if (_effImplPdf === "verified") {
            verified++;
            domainStats[domain].verified++;
          }
        }
        if (entry.owner) withOwner++;
        if (entry.evidenceRef) withEvidence++;
        if (entry.linkedRiskIds && entry.linkedRiskIds.length > 0) withRisks++;
        if (entry.lastReviewDate && new Date(entry.lastReviewDate) >= sixMonthsAgo) reviewedLast6Months++;
        if (entry.source) {
          sourceStats[entry.source] = (sourceStats[entry.source] || 0) + 1;
        }
      } else {
        notApplicable++;
      }
    });

    const implPercent = applicable > 0 ? Math.round((implemented / applicable) * 100) : 0;
    const verifiedPercent = applicable > 0 ? Math.round((verified / applicable) * 100) : 0;
    const ownerPercent = applicable > 0 ? Math.round((withOwner / applicable) * 100) : 0;
    const evidencePercent = applicable > 0 ? Math.round((withEvidence / applicable) * 100) : 0;

    // Labels
    const sourceLabels = {
      risk: t('soaSourceLabels.risk'), legal: t('soaSourceLabels.legal'), contractual: t('soaSourceLabels.contractual'),
      best_practice: t('soaSourceLabels.bestPractice'), regulatory: t('soaSourceLabels.regulatory'), business: t('soaSourceLabels.business')
    };
    const implLabels = {
      not_started: t('soaImplStatuses.notStarted'), planned: t('soaImplStatuses.planned'), in_progress: t('soaImplStatuses.inProgress'),
      implemented: t('soaImplStatuses.implemented'), verified: t('soaImplStatuses.verified')
    };

    // Construire le HTML
    let html = `
      <!DOCTYPE html>
      <html lang="${currentLang || 'fr'}">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:">
        <title>${escapeHtml(t('pdfSoa.title', 'Déclaration d\'applicabilité (SoA)'))} - ${escapeHtml(orgName)}</title>
        <style>
          * { box-sizing: border-box; margin: 0; padding: 0; }
          body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 9pt; line-height: 1.4; color: #333; padding: 15mm; }
          .header { text-align: center; margin-bottom: 15px; padding-bottom: 12px; border-bottom: 3px solid #0f9b7a; }
          .header h1 { font-size: 20pt; color: #0f9b7a; margin-bottom: 3px; }
          .header .subtitle { font-size: 11pt; color: #666; }
          .header .org { font-size: 16pt; font-weight: 700; margin: 8px 0; color: #1a1a1a; }
          .header .date { font-size: 9pt; color: #888; }
          .header .version { font-size: 10pt; color: #0f9b7a; font-weight: 600; margin-top: 5px; }

          /* Document Info Box */
          .doc-info { background: linear-gradient(135deg, #f8fffe 0%, #f0fdf4 100%); border: 1px solid #d1fae5; padding: 12px; border-radius: 8px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 15px; }
          .doc-info-item { flex: 1; min-width: 150px; }
          .doc-info-item .label { font-size: 7pt; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
          .doc-info-item .value { font-size: 10pt; font-weight: 600; color: #166534; }

          /* Executive Summary */
          .exec-summary { background: #1e3a5f; color: white; padding: 15px; border-radius: 8px; margin-bottom: 15px; }
          .exec-summary h2 { font-size: 12pt; margin-bottom: 10px; color: #93c5fd; }
          .exec-summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
          .exec-summary-item { text-align: center; }
          .exec-summary-item .number { font-size: 24pt; font-weight: 700; line-height: 1; }
          .exec-summary-item .number.good { color: #4ade80; }
          .exec-summary-item .number.medium { color: #fbbf24; }
          .exec-summary-item .number.low { color: #f87171; }
          .exec-summary-item .label { font-size: 8pt; color: #bfdbfe; margin-top: 3px; }

          /* Stats boxes */
          .stats-row { display: flex; gap: 10px; margin-bottom: 15px; }
          .stat-box { flex: 1; background: #f8f9fa; padding: 10px; border-radius: 6px; border-left: 3px solid #0f9b7a; }
          .stat-box h3 { font-size: 9pt; color: #666; margin-bottom: 8px; }
          .stat-box .items { display: flex; flex-wrap: wrap; gap: 5px; }
          .stat-item { background: white; padding: 4px 8px; border-radius: 4px; font-size: 8pt; }
          .stat-item strong { color: #0f9b7a; }

          /* Domain coverage matrix */
          .domain-matrix { margin-bottom: 15px; }
          .domain-matrix h3 { font-size: 10pt; margin-bottom: 8px; color: #1e3a5f; }
          .domain-matrix table { width: 100%; border-collapse: collapse; font-size: 8pt; }
          .domain-matrix th, .domain-matrix td { padding: 6px 8px; text-align: center; border: 1px solid #e5e7eb; }
          .domain-matrix th { background: #f3f4f6; font-weight: 600; }
          .domain-matrix .domain-name { text-align: left; font-weight: 600; }
          .progress-bar { width: 60px; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; display: inline-block; }
          .progress-bar .fill { height: 100%; border-radius: 4px; }
          .progress-bar .fill.good { background: #22c55e; }
          .progress-bar .fill.medium { background: #f59e0b; }
          .progress-bar .fill.low { background: #ef4444; }

          /* Main table */
          table.soa-table { width: 100%; border-collapse: collapse; font-size: 8pt; margin-top: 10px; }
          table.soa-table th { background: #0f9b7a; color: white; padding: 6px 4px; text-align: left; font-weight: 600; font-size: 7pt; }
          table.soa-table td { padding: 5px 4px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
          table.soa-table tr:nth-child(even) { background: #fafafa; }
          table.soa-table tr.not-applicable { background: #f5f5f5; color: #888; }

          .badge { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 7pt; font-weight: 600; }
          .badge-applicable { background: #dcfce7; color: #166534; }
          .badge-not-applicable { background: #f1f5f9; color: #64748b; }
          .badge-org { background: #dbeafe; color: #1e40af; }
          .badge-hum { background: #f3e8ff; color: #7c3aed; }
          .badge-phy { background: #dcfce7; color: #166534; }
          .badge-tech { background: #ffedd5; color: #c2410c; }

          .impl-status { font-size: 7pt; padding: 1px 4px; border-radius: 2px; }
          .impl-verified { background: #22c55e; color: white; }
          .impl-implemented { background: #3b82f6; color: white; }
          .impl-in_progress { background: #f59e0b; color: white; }
          .impl-planned { background: #8b5cf6; color: white; }
          .impl-not_started { background: #94a3b8; color: white; }

          .source-badge { font-size: 6pt; background: #e0e7ff; color: #3730a3; padding: 1px 3px; border-radius: 2px; }
          .risk-badge { display: inline-block; padding: 1px 3px; border-radius: 2px; margin: 1px; font-size: 6pt; }
          .risk-critique { background: #fee2e2; color: #991b1b; }
          .risk-grave { background: #ffedd5; color: #c2410c; }
          .risk-significatif { background: #fef9c3; color: #854d0e; }
          .risk-mineur { background: #dcfce7; color: #166534; }

          /* Signatures section */
          .signatures { margin-top: 25px; padding-top: 15px; border-top: 2px solid #e5e7eb; }
          .signatures h3 { font-size: 10pt; margin-bottom: 15px; color: #1e3a5f; }
          .signatures-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
          .signature-box { border: 1px solid #d1d5db; padding: 15px; border-radius: 6px; min-height: 100px; }
          .signature-box .role { font-size: 9pt; font-weight: 600; color: #374151; margin-bottom: 5px; }
          .signature-box .line { border-bottom: 1px solid #9ca3af; margin: 30px 0 5px 0; }
          .signature-box .fields { font-size: 8pt; color: #6b7280; }

          /* Version history */
          .version-history { margin-top: 20px; padding: 12px; background: #f9fafb; border-radius: 6px; }
          .version-history h3 { font-size: 9pt; margin-bottom: 8px; color: #374151; }
          .version-history table { width: 100%; font-size: 8pt; border-collapse: collapse; }
          .version-history th, .version-history td { padding: 4px 8px; text-align: left; border-bottom: 1px solid #e5e7eb; }
          .version-history th { background: #e5e7eb; font-weight: 600; }

          .footer { margin-top: 20px; padding-top: 12px; border-top: 1px solid #ddd; font-size: 8pt; color: #888; text-align: center; }
          .footer .ref { font-weight: 600; color: #0f9b7a; }
          .footer .confidential { color: #dc2626; font-weight: 700; margin-top: 5px; }

          .page-break { page-break-after: always; }
          @media print {
            body { padding: 10mm; }
            .no-print { display: none; }
            .page-break { page-break-after: always; }
          }
        </style>
      </head>
      <body>
        <!-- Page 1: Header + Executive Summary -->
        <div class="header">
          <h1>${escapeHtml(t('pdfSoa.title', 'Déclaration d\'applicabilité (SoA)'))}</h1>
          <div class="subtitle">${escapeHtml((() => { var _fw = _getActiveManagementFramework(); return _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) + ' - Clause 6.1.3' : t('pdfSoa.subtitle', 'ISO 27001:2022 - Clause 6.1.3 - Annexe A'); })())}</div>
          <div class="org">${escapeHtml(orgName)}</div>
          <div class="version">Version ${escapeHtml(docVersion)} | ${isAudit ? escapeHtml(t('pdfSoa.fullAuditVersion', 'DOCUMENT AUDIT COMPLET')) : escapeHtml(t('pdfSoa.publicVersion', 'VERSION PUBLIQUE'))}</div>
          <div class="date">${escapeHtml(t('pdfSoa.issueDate', "Date d'émission:"))} ${today}</div>
        </div>

        <!-- Document Information -->
        <div class="doc-info">
          <div class="doc-info-item">
            <div class="label">${escapeHtml(t('pdfSoa.docRef', 'Référence document'))}</div>
            <div class="value">SOA-${docDate.replace(/-/g, '')}</div>
          </div>
          <div class="doc-info-item">
            <div class="label">${escapeHtml(t('pdfSoa.scope', 'Périmètre'))}</div>
            <div class="value">SMSI ${escapeHtml(orgName)}</div>
          </div>
          <div class="doc-info-item">
            <div class="label">${escapeHtml(t('pdfSoa.refStandard', 'Norme de référence'))}</div>
            <div class="value">ISO/IEC 27001:2022</div>
          </div>
          <div class="doc-info-item">
            <div class="label">${escapeHtml(t('pdfSoa.classification', 'Classification'))}</div>
            <div class="value">${isAudit ? escapeHtml(t('pdfSoa.confidential', 'Confidentiel')) : escapeHtml(t('pdfSoa.internal', 'Interne'))}</div>
          </div>
        </div>

        <!-- Executive Summary -->
        <div class="exec-summary">
          <h2>${escapeHtml(t('pdfSoa.execSummary', 'Synthèse exécutive'))}</h2>
          <div class="exec-summary-grid">
            <div class="exec-summary-item">
              <div class="number">${applicable}/${sortedControls.length}</div>
              <div class="label">${escapeHtml(t('pdfSoa.applicableControls', 'Contrôles applicables'))}</div>
            </div>
            <div class="exec-summary-item">
              <div class="number ${implPercent >= 80 ? 'good' : implPercent >= 50 ? 'medium' : 'low'}">${implPercent}%</div>
              <div class="label">${escapeHtml(t('pdfSoa.implRate', "Taux d'implémentation"))}</div>
            </div>
            <div class="exec-summary-item">
              <div class="number ${verifiedPercent >= 70 ? 'good' : verifiedPercent >= 40 ? 'medium' : 'low'}">${verifiedPercent}%</div>
              <div class="label">${escapeHtml(t('pdfSoa.verifiedControls', 'Contrôles vérifiés'))}</div>
            </div>
            <div class="exec-summary-item">
              <div class="number">${notApplicable}</div>
              <div class="label">${escapeHtml(t('pdfSoa.notApplicable', 'Non applicables'))}</div>
            </div>
          </div>
        </div>

        ${isAudit ? `
        <!-- Detailed Stats for Audit -->
        <div class="stats-row">
          <div class="stat-box">
            <h3>${escapeHtml(t('pdfSoa.docQuality', 'Qualité de la documentation'))}</h3>
            <div class="items">
              <div class="stat-item"><strong>${ownerPercent}%</strong> ${escapeHtml(t('pdfSoa.withOwner', 'avec propriétaire'))}</div>
              <div class="stat-item"><strong>${evidencePercent}%</strong> ${escapeHtml(t('pdfSoa.withEvidence', 'avec preuves'))}</div>
              <div class="stat-item"><strong>${withRisks}</strong> ${escapeHtml(t('pdfSoa.linkedToRisks', 'liés à des risques'))}</div>
              <div class="stat-item"><strong>${reviewedLast6Months}</strong> ${escapeHtml(t('pdfSoa.reviewedLast6m', 'revus <6 mois'))}</div>
            </div>
          </div>
          <div class="stat-box">
            <h3>${escapeHtml(t('pdfSoa.selectionSources', 'Sources de sélection'))}</h3>
            <div class="items">
              ${Object.entries(sourceStats).map(([src, count]) =>
                `<div class="stat-item"><strong>${count}</strong> ${sourceLabels[src] || escapeHtml(src)}</div>`
              ).join('')}
            </div>
          </div>
        </div>
        ` : ''}

        <!-- Domain Coverage Matrix -->
        <div class="domain-matrix">
          <h3>${escapeHtml((() => { var _fw = _getActiveControlsFramework(); var _n = _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) : 'ISO 27002:2022'; return t('pdfSoa.domainCoverage', 'Couverture par domaine ' + _n); })())}</h3>
          <table>
            <thead>
              <tr>
                <th class="domain-name">${escapeHtml(t('pdfSoa.domain', 'Domaine'))}</th>
                <th>${escapeHtml(t('pdfSoa.total', 'Total'))}</th>
                <th>${escapeHtml(t('pdfSoa.applicable', 'Applicables'))}</th>
                <th>${escapeHtml(t('pdfSoa.implemented', 'Implémentés'))}</th>
                <th>${escapeHtml(t('pdfSoa.verified', 'Vérifiés'))}</th>
                <th>${escapeHtml(t('pdfSoa.progression', 'Progression'))}</th>
              </tr>
            </thead>
            <tbody>
              ${Object.entries(domainStats).map(([domain, stats]) => {
                const pct = stats.applicable > 0 ? Math.round((stats.implemented / stats.applicable) * 100) : 0;
                return `
                  <tr>
                    <td class="domain-name">${domain}</td>
                    <td>${stats.total}</td>
                    <td>${stats.applicable}</td>
                    <td>${stats.implemented}</td>
                    <td>${stats.verified || 0}</td>
                    <td>
                      <div class="progress-bar">
                        <div class="fill ${pct >= 80 ? 'good' : pct >= 50 ? 'medium' : 'low'}" style="width:${pct}%"></div>
                      </div>
                      ${pct}%
                    </td>
                  </tr>
                `;
              }).join('')}
            </tbody>
          </table>
        </div>

        <div class="page-break"></div>

        <!-- Main SOA Table -->
        <h3 style="font-size:11pt;color:#1e3a5f;margin-bottom:10px;">${escapeHtml((() => { var _fw = _getActiveControlsFramework(); var _n = _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) : 'ISO 27002:2022'; return t('pdfSoa.controlDetails', 'Détail des contrôles ' + _n); })())}</h3>
        <table class="soa-table">
          <thead>
            <tr>
              <th style="width:40px;">${escapeHtml(t('pdfSoa.ref', 'Réf.'))}</th>
              <th style="width:22%;">${escapeHtml(t('pdfSoa.control', 'Contrôle'))}</th>
              <th style="width:55px;">${escapeHtml(t('pdfSoa.domain', 'Domaine'))}</th>
              <th style="width:50px;">${escapeHtml(t('pdfSoa.applic', 'Applic.'))}</th>
              ${isAudit ? '<th style="width:60px;">' + escapeHtml(t('pdfSoa.source', 'Source')) + '</th>' : ''}
              <th style="width:60px;">${escapeHtml(t('pdfSoa.state', 'État'))}</th>
              ${isAudit ? '<th style="width:70px;">' + escapeHtml(t('pdfSoa.owner', 'Propriétaire')) + '</th>' : ''}
              ${isAudit ? '<th style="width:70px;">' + escapeHtml(t('pdfSoa.evidence', 'Preuves')) + '</th>' : ''}
              ${isAudit ? '<th style="width:60px;">' + escapeHtml(t('pdfSoa.risks', 'Risques')) + '</th>' : ''}
              <th>${escapeHtml(t('pdfSoa.justification', 'Justification'))}</th>
            </tr>
          </thead>
          <tbody>
    `;

    sortedControls.forEach(control => {
      const entry = appData.soa.find(e => e.controlId === control.id) || {};
      const domain = getControlDomain(control.reference);
      const isApplicable = entry.applicable !== false;

      // Risques liés
      let risksHtml = "";
      if (isAudit && entry.linkedRiskIds && entry.linkedRiskIds.length > 0) {
        risksHtml = entry.linkedRiskIds.slice(0, 3).map(riskId => {
          const risk = (appData.risks || []).find(r => r.id === riskId);
          if (!risk) return "";
          const rawLevel = risk.level ? risk.level.toLowerCase() : "";
          const levelClass = rawLevel && /^[a-zA-Z0-9-]+$/.test(rawLevel) ? `risk-${rawLevel}` : "";
          return `<span class="risk-badge ${levelClass}">${escapeHtml((risk.title || risk.id).substring(0, 15))}</span>`;
        }).join("");
        if (entry.linkedRiskIds.length > 3) risksHtml += `<span style="font-size:6pt;">+${entry.linkedRiskIds.length - 3}</span>`;
      }

      html += `
        <tr class="${isApplicable ? '' : 'not-applicable'}">
          <td><strong>${escapeHtml(control.reference)}</strong></td>
          <td>${escapeHtml(isoCtrlTitle(control))}</td>
          <td><span class="badge badge-${domain.substring(0,3).toLowerCase()}">${domain.substring(0,4)}</span></td>
          <td><span class="badge ${isApplicable ? 'badge-applicable' : 'badge-not-applicable'}">${isApplicable ? '✓' : '✗'}</span></td>
          ${isAudit ? `<td>${entry.source ? `<span class="source-badge">${escapeHtml(String(sourceLabels[entry.source] || entry.source).substring(0, 10))}</span>` : '-'}</td>` : ''}
          <td>${(function(){var _e=_soaEffectiveImplStatus(entry, control);var _cls=/^[a-z_]+$/.test(_e)?_e:'';return _e ? `<span class="impl-status impl-${_cls}">${escapeHtml((implLabels[_e] || '').substring(0, 8))}</span>` : '-';})()}</td>
          ${isAudit ? `<td style="font-size:7pt;">${entry.owner ? escapeHtml(entry.owner.substring(0, 15)) : '-'}</td>` : ''}
          ${isAudit ? `<td style="font-size:7pt;">${entry.evidenceRef ? escapeHtml(entry.evidenceRef.substring(0, 15)) : '-'}</td>` : ''}
          ${isAudit ? `<td>${risksHtml || '-'}</td>` : ''}
          <td style="font-size:7pt;">
            ${entry.justification ? escapeHtml(entry.justification.substring(0, 80)) + (entry.justification.length > 80 ? '...' : '') : '-'}
            ${!isApplicable && entry.exclusionReason ? `<div style="color:#dc2626;font-style:italic;">${escapeHtml(t('pdfSoa.excluded', 'Exclu:'))} ${escapeHtml(entry.exclusionReason.substring(0, 50))}</div>` : ''}
            ${Array.isArray(entry.comments) && entry.comments.length > 0 ? '<div style="color:#3b82f6;font-size:7pt;">💬 ' + entry.comments.length + '</div>' : ''}
          </td>
        </tr>
      `;
    });

    html += `
          </tbody>
        </table>

        ${isAudit ? `
        <div class="page-break"></div>

        <!-- Signatures Section -->
        <div class="signatures">
          <h3>${escapeHtml(t('pdfSoa.approvals', 'Approbations et signatures'))}</h3>
          <div class="signatures-grid">
            <div class="signature-box">
              <div class="role">${escapeHtml(t('pdfSoa.ismsManager', 'Responsable SMSI'))}</div>
              <div class="line"></div>
              <div class="fields">
                ${escapeHtml(t('pdfSoa.nameField', 'Nom: ________________________________'))}<br>
                ${escapeHtml(t('pdfSoa.dateField', 'Date: ________________________________'))}<br>
                ${escapeHtml(t('pdfSoa.signatureField', 'Signature:'))}
              </div>
            </div>
            <div class="signature-box">
              <div class="role">${escapeHtml(t('pdfSoa.generalManagement', 'Direction Générale'))}</div>
              <div class="line"></div>
              <div class="fields">
                ${escapeHtml(t('pdfSoa.nameField', 'Nom: ________________________________'))}<br>
                ${escapeHtml(t('pdfSoa.dateField', 'Date: ________________________________'))}<br>
                ${escapeHtml(t('pdfSoa.signatureField', 'Signature:'))}
              </div>
            </div>
            <div class="signature-box">
              <div class="role">${escapeHtml(t('pdfSoa.securityOfficer', 'Responsable Sécurité'))}</div>
              <div class="line"></div>
              <div class="fields">
                ${escapeHtml(t('pdfSoa.nameField', 'Nom: ________________________________'))}<br>
                ${escapeHtml(t('pdfSoa.dateField', 'Date: ________________________________'))}<br>
                ${escapeHtml(t('pdfSoa.signatureField', 'Signature:'))}
              </div>
            </div>
          </div>
        </div>

        <!-- Version History -->
        <div class="version-history">
          <h3>${escapeHtml(t('pdfSoa.versionHistory', 'Historique des versions'))}</h3>
          <table>
            <thead>
              <tr>
                <th>${escapeHtml(t('pdfSoa.version', 'Version'))}</th>
                <th>${escapeHtml(t('pdfSoa.date', 'Date'))}</th>
                <th>${escapeHtml(t('pdfSoa.author', 'Auteur'))}</th>
                <th>${escapeHtml(t('pdfSoa.changes', 'Modifications'))}</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>${escapeHtml(docVersion)}</td>
                <td>${today}</td>
                <td>RSSI</td>
                <td>${escapeHtml(t('pdfSoa.currentVersionAuto', 'Version actuelle - Génération automatique'))}</td>
              </tr>
              <tr>
                <td>0.9</td>
                <td>-</td>
                <td>-</td>
                <td>${escapeHtml(t('pdfSoa.initialRevision', 'Révision initiale'))}</td>
              </tr>
            </tbody>
          </table>
        </div>
        ` : ''}

        <div class="footer">
          <div class="ref">${escapeHtml(t('pdfSoa.footerRef', 'Référence:'))} SOA-${escapeHtml(orgName.replace(/\s+/g, '-').toUpperCase())}-${docDate}</div>
          <div>${escapeHtml((() => { var _fw = _getActiveManagementFramework(); var _fwName = _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) : 'ISO 27001:2022'; return t('pdfSoa.footerGenerated', 'Document généré par Cyber-Assistant | Conforme ' + _fwName + ' Clause 6.1.3'); })())}</div>
          ${isAudit ? '<div class="confidential">' + escapeHtml(t('pdfSoa.footerConfidential', 'DOCUMENT CONFIDENTIEL - DIFFUSION RESTREINTE - USAGE AUDIT UNIQUEMENT')) + '</div>' : '<div>© ' + new Date().getFullYear() + ' ' + escapeHtml(orgName) + '</div>'}
        </div>
      </body>
      </html>
    `;

    // Ouvrir dans une nouvelle fenêtre pour impression/PDF
    const printWindow = window.open('', '_blank');
    if (!printWindow) {
      showToast2(t('toasts.popupBlocked', 'Veuillez autoriser les popups pour exporter en PDF.'), 'error');
      _exportBusy = false;
      return;
    }
    printWindow.document.write(html);
    printWindow.document.close();

    setTimeout(() => {
      try {
        printWindow.print();
        showToast2(t('toasts.soaPdfReady', 'SOA prête — Utilisez "Enregistrer en PDF"'), 'success');
      } catch (e) {
        void 0;
      }
      _exportBusy = false;
    }, 300);
  }

  // ============================================================
  // EXPORT PDF - Revue Documentaire (Pack Audit ISO 27001)
  // ============================================================

  function exportDocReviewPdf() {
    if (DEMO_MODE) {
      showDemoBlockedMessage('L\'export PDF');
      return;
    }
    if (_exportBusy) return;
    _exportBusy = true;
    // FIX-EXPORT-CURRENTLANG (audit 2026-06-10) : voir exportSoaPdf — même ReferenceError.
    const currentLang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : "fr";
    const orgName = appData.settings?.organizationName || "Organisation";
    const today = new Date().toLocaleDateString(getExportLocale());

    // Stats
    let conforme = 0, partiel = 0, nonConforme = 0, total = 0;
    DOCUMENT_REVIEW_ITEMS.filter(i => i.reference).forEach(item => {
      const entry = appData.documentReview.find(e => e.reference === item.reference) || {};
      total++;
      if (entry.status === "conforme") conforme++;
      else if (entry.status === "partiel") partiel++;
      else if (entry.status === "non_conforme") nonConforme++;
    });
    const conformePercent = total > 0 ? Math.round((conforme / total) * 100) : 0;

    let html = `
      <!DOCTYPE html>
      <html lang="${currentLang || 'fr'}">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:">
        <title>${escapeHtml(t('pdfDocReview.title', 'Revue documentaire SMSI'))} - ${escapeHtml(orgName)}</title>
        <style>
          * { box-sizing: border-box; margin: 0; padding: 0; }
          body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 10pt; line-height: 1.4; color: #333; padding: 20mm; }
          .header { text-align: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #3b82f6; }
          .header h1 { font-size: 18pt; color: #3b82f6; margin-bottom: 5px; }
          .header .subtitle { font-size: 11pt; color: #666; }
          .header .org { font-size: 14pt; font-weight: 600; margin: 10px 0; }
          .header .date { font-size: 9pt; color: #888; }
          .header .audit-label { background: #dc2626; color: white; padding: 4px 12px; border-radius: 4px; display: inline-block; margin-top: 10px; font-weight: 600; }
          .meta-box { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; display: flex; flex-wrap: wrap; gap: 20px; }
          .meta-item { flex: 1; min-width: 100px; text-align: center; }
          .meta-item .label { font-size: 8pt; color: #666; text-transform: uppercase; }
          .meta-item .value { font-size: 18pt; font-weight: 700; }
          .meta-item .value.good { color: #22c55e; }
          .meta-item .value.warning { color: #f59e0b; }
          .meta-item .value.danger { color: #ef4444; }
          .chapter { background: #1e40af; color: white; padding: 8px 12px; margin: 15px 0 10px 0; font-weight: 600; font-size: 11pt; border-radius: 4px; }
          table { width: 100%; border-collapse: collapse; font-size: 9pt; }
          th { background: #3b82f6; color: white; padding: 8px 6px; text-align: left; font-weight: 600; }
          td { padding: 6px; border-bottom: 1px solid #e0e0e0; vertical-align: top; }
          tr:nth-child(even) { background: #fafafa; }
          tr.conforme { background: rgba(34, 197, 94, 0.08); }
          tr.partiel { background: rgba(245, 158, 11, 0.08); }
          tr.non-conforme { background: rgba(239, 68, 68, 0.08); }
          tr.na { background: #f5f5f5; opacity: 0.7; }
          .status { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 8pt; font-weight: 600; }
          .status-conforme { background: #22c55e; color: white; }
          .status-partiel { background: #f59e0b; color: white; }
          .status-non-conforme { background: #ef4444; color: white; }
          .status-na { background: #94a3b8; color: white; }
          .status-empty { background: #e5e7eb; color: #6b7280; }
          .ref-col { width: 80px; font-weight: 600; }
          .desc-col { width: 30%; }
          .dates { font-size: 8pt; color: #666; }
          .footer { margin-top: 30px; padding-top: 15px; border-top: 1px solid #ddd; font-size: 8pt; color: #888; text-align: center; }
          .checklist { margin-top: 30px; page-break-before: always; }
          .checklist h2 { color: #1e40af; margin-bottom: 15px; }
          .checklist-item { display: flex; align-items: flex-start; padding: 8px 0; border-bottom: 1px solid #eee; }
          .checklist-box { width: 18px; height: 18px; border: 2px solid #3b82f6; border-radius: 3px; margin-right: 10px; flex-shrink: 0; }
          .checklist-text { flex: 1; }
          .checklist-status { font-size: 9pt; padding: 2px 6px; border-radius: 3px; }
          @media print {
            body { padding: 10mm; }
          }
        </style>
      </head>
      <body>
        <div class="header">
          <h1>${escapeHtml(t('pdfDocReview.title', 'Revue documentaire SMSI'))}</h1>
          <div class="subtitle">${escapeHtml((() => { var _fw = _getActiveManagementFramework(); return _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) + ' - Clauses 4-10' : t('pdfDocReview.subtitle', 'ISO 27001:2022 - Clauses 4 à 10'); })())}</div>
          <div class="org">${escapeHtml(orgName)}</div>
          <div class="date">${escapeHtml(t('pdfDocReview.generatedOn', 'Généré le'))} ${today}</div>
          <div class="audit-label">${escapeHtml(t('pdfDocReview.auditLabel', 'PACK AUDIT CERTIFICATION'))}</div>
        </div>

        <div class="meta-box">
          <div class="meta-item">
            <div class="label">${escapeHtml(t('pdfDocReview.conforming', 'Conformes'))}</div>
            <div class="value good">${conforme}</div>
          </div>
          <div class="meta-item">
            <div class="label">${escapeHtml(t('pdfDocReview.partial', 'Partiels'))}</div>
            <div class="value warning">${partiel}</div>
          </div>
          <div class="meta-item">
            <div class="label">${escapeHtml(t('pdfDocReview.nonConforming', 'Non conformes'))}</div>
            <div class="value danger">${nonConforme}</div>
          </div>
          <div class="meta-item">
            <div class="label">${escapeHtml(t('pdfDocReview.conformityRate', 'Taux de conformité'))}</div>
            <div class="value ${conformePercent >= 80 ? 'good' : conformePercent >= 50 ? 'warning' : 'danger'}">${conformePercent}%</div>
          </div>
        </div>

        <table>
          <thead>
            <tr>
              <th class="ref-col">${escapeHtml(t('pdfDocReview.clause', 'Clause'))}</th>
              <th class="desc-col">${escapeHtml(t('pdfDocReview.requirement', 'Exigence'))}</th>
              <th style="width:80px;">${escapeHtml(t('pdfDocReview.status', 'Statut'))}</th>
              <th style="width:80px;">${escapeHtml(t('pdfDocReview.responsible', 'Responsable'))}</th>
              <th>${escapeHtml(t('pdfDocReview.justificationEvidence', 'Justification / Preuve'))}</th>
              <th style="width:90px;">${escapeHtml(t('pdfDocReview.review', 'Revue'))}</th>
            </tr>
          </thead>
          <tbody>
    `;

    let currentChapter = "";
    var _pdfMgmtFw = _getActiveManagementFramework();
    var _pdfIsEn = (typeof currentLang !== 'undefined' && currentLang === 'en') || (document.documentElement.lang === 'en');
    DOCUMENT_REVIEW_ITEMS.forEach(item => {
      if (item.type === "chapter") {
        // Resolve bilingual chapter title from framework data
        var _pdfChTitle = item.title;
        if (_pdfMgmtFw && _pdfMgmtFw.chapters) {
          var _pdfChId = item.title.match(/^(\d+)/);
          if (_pdfChId) {
            var _pdfCh = _pdfMgmtFw.chapters.find(function(c) { return c.id === _pdfChId[1]; });
            if (_pdfCh) _pdfChTitle = (_pdfIsEn && _pdfCh.titleEn) ? _pdfCh.titleEn : _pdfCh.title;
          }
        }
        currentChapter = _pdfChTitle;
        html += '<tr><td colspan="6" class="chapter">' + escapeHtml(_pdfChTitle) + '</td></tr>';
      } else {
        const entry = appData.documentReview.find(e => e.reference === item.reference) || {};
        const statusLabels = {
          conforme: t('docReview.conforming', 'Conformes'), partiel: t('docReview.partial', 'Partiels'), non_conforme: t('docReview.nonConforming', 'Non conformes'), na: "N/A"
        };
        const safeStatus = /^[a-zA-Z0-9_-]+$/.test(entry.status) ? entry.status : '';
        const statusClass = safeStatus || "empty";
        var _pdfClauseNum = item.reference.split(' ')[0] || '';
        var _pdfFwClause = _pdfMgmtFw ? (_pdfMgmtFw.clauses || []).find(function(c) { return c.reference === _pdfClauseNum; }) : null;
        var _pdfRef = (_pdfFwClause && _pdfIsEn && _pdfFwClause.titleEn) ? (_pdfClauseNum + ' ' + _pdfFwClause.titleEn) : (_pdfFwClause ? (_pdfClauseNum + ' ' + _pdfFwClause.title) : item.reference);
        var _pdfDesc = (_pdfFwClause && _pdfIsEn && _pdfFwClause.descriptionEn) ? _pdfFwClause.descriptionEn : (_pdfFwClause ? _pdfFwClause.description : item.description);

        html += `
          <tr class="${safeStatus}">
            <td class="ref-col">${escapeHtml(_pdfRef)}</td>
            <td class="desc-col">${escapeHtml(_pdfDesc)}</td>
            <td><span class="status status-${statusClass}">${statusLabels[entry.status] || escapeHtml(t('pdfDocReview.toEvaluate', 'À évaluer'))}</span></td>
            <td>${escapeHtml(entry.responsible || '-')}</td>
            <td>
              ${entry.justification ? escapeHtml(entry.justification) : '-'}
              ${entry.tool ? `<br><em style="color:#666;">${escapeHtml(t('pdfDocReview.evidence', 'Preuve:'))} ${escapeHtml(entry.tool)}</em>` : ''}
              ${entry.documentLink && isSafeUrl(entry.documentLink) ? `<br><a href="${escapeHtml(entry.documentLink)}" target="_blank" rel="noopener noreferrer" style="color:#3b82f6;font-size:8pt;">${escapeHtml(t('pdfDocReview.document', 'Document'))}</a>` : ''}
              ${Array.isArray(entry.comments) && entry.comments.length > 0 ? '<br><em style="color:#3b82f6;font-size:8pt;">' + entry.comments.length + ' ' + escapeHtml(t('pdfDocReview.comments', 'commentaire(s)')) + '</em>' : ''}
            </td>
            <td class="dates">
              ${entry.lastReview ? escapeHtml(t('pdfDocReview.lastReview', 'Dernière:')) + ' ' + escapeHtml(entry.lastReview) : '-'}
              ${entry.nextReview ? '<br>' + escapeHtml(t('pdfDocReview.nextReview', 'Prochaine:')) + ' ' + escapeHtml(entry.nextReview) : ''}
            </td>
          </tr>
        `;
      }
    });

    html += `
          </tbody>
        </table>

        <!-- Checklist Auditeur -->
        <div class="checklist">
          <h2>${escapeHtml((() => { var _fw = _getActiveManagementFramework(); var _n = _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) : 'ISO 27001:2022'; return t('pdfDocReview.checklistTitle', 'Checklist Préparation Audit ' + _n); })())}</h2>
          <p style="margin-bottom:15px;color:#666;">${escapeHtml(t('pdfDocReview.checklistSubtitle', "Points de contrôle pour l'auditeur externe"))}</p>
    `;

    const auditChecklist = [
      { ref: "4", label: t('pdfDocReview.checkContext', "Contexte de l'organisation"), items: t('pdfDocReview.checkContextItems', "Enjeux internes/externes documentés|Parties intéressées identifiées|Domaine d'application défini").split('|') },
      { ref: "5", label: t('pdfDocReview.checkLeadership', 'Leadership'), items: t('pdfDocReview.checkLeadershipItems', "Politique SSI signée et communiquée|Rôles et responsabilités attribués|Engagement de la direction documenté").split('|') },
      { ref: "6", label: t('pdfDocReview.checkPlanning', 'Planification'), items: t('pdfDocReview.checkPlanningItems', 'Processus d\'appréciation des risques|Plan de traitement des risques|Déclaration d\'applicabilité (SoA)|Objectifs SSI mesurables').split('|') },
      { ref: "7", label: t('pdfDocReview.checkSupport', 'Supports'), items: t('pdfDocReview.checkSupportItems', "Ressources allouées|Compétences documentées|Programme de sensibilisation|Plan de communication|Maîtrise documentaire").split('|') },
      { ref: "8", label: t('pdfDocReview.checkOperation', 'Fonctionnement'), items: t('pdfDocReview.checkOperationItems', "Processus opérationnels documentés|Appréciations des risques à jour|Traitement des risques en cours").split('|') },
      { ref: "9", label: t('pdfDocReview.checkPerformance', 'Évaluation de la performance'), items: t('pdfDocReview.checkPerformanceItems', "Programme d'audit interne|Indicateurs de performance (KPI)|Revue de direction planifiée|Procès-verbaux de revue").split('|') },
      { ref: "10", label: t('pdfDocReview.checkImprovement', 'Amélioration'), items: t('pdfDocReview.checkImprovementItems', "Processus d'amélioration continue|Gestion des non-conformités|Actions correctives tracées").split('|') }
    ];

    auditChecklist.forEach(section => {
      html += `<div style="margin-top:15px;"><strong style="color:#1e40af;">Clause ${section.ref} - ${section.label}</strong></div>`;
      section.items.forEach(item => {
        // Vérifier si on a des données pour cet item
        const relatedEntries = appData.documentReview.filter(e =>
          e.reference && e.reference.startsWith(section.ref + ".")
        );
        const hasConforme = relatedEntries.some(e => e.status === "conforme");

        html += `
          <div class="checklist-item">
            <div class="checklist-box"></div>
            <div class="checklist-text">${item}</div>
            ${hasConforme ? '<span class="checklist-status" style="background:#dcfce7;color:#166534;">' + escapeHtml(t('pdfDocReview.documented', 'Documenté')) + '</span>' : ''}
          </div>
        `;
      });
    });

    html += `
        </div>

        <div class="footer">
          <div style="font-weight:600;">${escapeHtml((() => { var _fw = _getActiveManagementFramework(); var _n = _fw && _fw.meta ? (_fw.meta.shortName || _fw.meta.name) : 'ISO 27001:2022'; return t('pdfDocReview.footerTitle', 'Pack Audit Certification ' + _n); })())}</div>
          <div>${escapeHtml(t('pdfDocReview.footerRef', 'Référence:'))} AUDIT-${escapeHtml(orgName.replace(/\s+/g, '-').toUpperCase())}-${new Date().toISOString().split('T')[0]}</div>
          <div>${escapeHtml(t('pdfDocReview.footerGenerated', 'Document généré par Cyber-Assistant'))}</div>
          <div style="color:#dc2626;font-weight:600;margin-top:5px;">${escapeHtml(t('pdfDocReview.footerConfidential', 'DOCUMENT CONFIDENTIEL - PRÉPARATION AUDIT'))}</div>
        </div>
      </body>
      </html>
    `;

    const printWindow = window.open('', '_blank');
    if (!printWindow) {
      showToast2(t('toasts.popupBlocked', 'Veuillez autoriser les popups pour exporter en PDF.'), "error");
      _exportBusy = false;
      return;
    }
    printWindow.document.write(html);
    printWindow.document.close();

    setTimeout(() => {
      try {
        printWindow.print();
        showToast2(t('toasts.auditPackReady', 'Pack Audit Doc Review prêt — Utilisez "Enregistrer en PDF"'), 'success');
      } catch (e) {
        void 0;
      }
      _exportBusy = false;
    }, 300);
  }

  // Exports des fonctions PDF après leur définition
  window.exportSoaPdf = exportSoaPdf;
  window.exportDocReviewPdf = exportDocReviewPdf;

  // Track current active tab for lock management
  var _currentActiveTab = 'dashboard';
  window._currentActiveTab = _currentActiveTab;
  // AUDIT-CO-H1: Mutex pour sérialiser les navigations de tab (anti-race condition)
  var _tabSwitchInProgress = false;
  // FIX-TABNAV-QUEUE (audit 2026-06-10, bundle.js:21075) : dernière navigation demandée
  // pendant qu'un changement d'onglet est en cours — rejouée à la fin au lieu d'être ignorée.
  var _queuedTabSwitch = null;

  // AUDIT-CO-C3: Re-render le tab courant après un pull de données fraîches
  // Les noms de fonctions sont alignés sur le switch de loadModuleContent (L18962+)
  function _rerenderCurrentTab(moduleId) {
    switch (moduleId) {
      case 'controls': if (typeof loadControlsTable === 'function') loadControlsTable(); break;
      case 'nis2-controls': if (typeof loadNis2ControlsTable === 'function') loadNis2ControlsTable(); break;
      case 'recyf-controls': if (typeof loadRecyfControlsTable === 'function') loadRecyfControlsTable(); break;
      case 'nis2-program': if (typeof loadNis2Program === 'function') loadNis2Program(); break;
      case 'actions': if (typeof loadActionsTable === 'function') loadActionsTable(); break;
      case 'risks':
        if (typeof loadRisksTable === 'function') loadRisksTable();
        if (typeof updateRiskMatrix === 'function') updateRiskMatrix();
        break;
      case 'threats-section': if (typeof loadThreatsTable === 'function') loadThreatsTable(); break;
      case 'nonconformities': if (typeof loadNonConformitiesTable === 'function') loadNonConformitiesTable(); break;
      case 'soa': if (typeof loadSoaTable === 'function') loadSoaTable(); break;
      case 'docReview': if (typeof loadDocReviewTable === 'function') loadDocReviewTable(); break;
      case 'audits': if (typeof loadAuditsTable === 'function') loadAuditsTable(); break;
      case 'managementReviews': if (typeof loadReviewsTable === 'function') loadReviewsTable(); break;
      case 'stakeholders': if (typeof loadStakeholdersTable === 'function') loadStakeholdersTable(); break;
      case 'documents': if (typeof loadDocumentsTable === 'function') loadDocumentsTable(); break;
      case 'kpis': if (typeof loadKpiTable === 'function') loadKpiTable(); break;
      case 'critical-assets': if (typeof loadCriticalAssetsTable === 'function') loadCriticalAssetsTable(); break;
      case 'project-risks': if (typeof loadProjectRisksTable === 'function') loadProjectRisksTable(); break;
      case 'nis2-socle': if (typeof renderSocleGrids === 'function') renderSocleGrids(); break;
      case 'risk-analysis': if (typeof renderRiskAnalysisModule === 'function') renderRiskAnalysisModule(); break;
      case 'impact-matrices': if (typeof renderImpactMatricesModule === 'function') renderImpactMatricesModule(); break;
      case 'framework-mapping': if (typeof loadFrameworkMappingView === 'function') loadFrameworkMappingView(); break;
      case 'auditLog': if (typeof renderAuditLogModule === 'function') renderAuditLogModule(); break;
      default: break;
    }
  }

  // FIX-READONLY-REFRESH (2026-06-30) : pull + applique l'état serveur d'un onglet SANS push ni
  // opération de verrou. Utilisé par les utilisateurs en LECTURE SEULE (navigation + tick
  // périodique) pour voir les modifications des autres — sinon ils restent figés sur l'instantané
  // de leur login. Réutilise l'endpoint data-pull existant (aucun pré-requis serveur → portable).
  // onApplied() n'est appelé que si une révision RÉELLEMENT plus récente a été appliquée.
  function _pullAndApplyTab(tabId, onApplied) {
    if (!tabId || tabId === 'dashboard' || tabId === 'settings') return;
    var SyncMgr = window.CSI_SyncManager;
    var PresenceMgr = window.CSI_PresenceManager || window.CSI_LockManager;
    if (!(SyncMgr && SyncMgr.pullTabFromServer)) return;
    SyncMgr.pullTabFromServer(tabId).then(function(tabData) {
      if (_currentActiveTab !== tabId) return;            // l'utilisateur a navigué ailleurs entre-temps
      if (_csiDirtyTabs[tabId]) return;                   // ne JAMAIS écraser des modifs locales en attente
      if (!tabData || typeof tabData !== 'object') return;
      if (typeof tabData._rev === 'number') {
        if (_tabRevisions[tabId] === tabData._rev) return; // rien de neuf → pas de re-render inutile
        _tabRevisions[tabId] = tabData._rev;
      }
      var tabDataKeys = PresenceMgr && PresenceMgr.getTabDataKeys ? PresenceMgr.getTabDataKeys()[tabId] : null;
      if (tabDataKeys && tabData.data) {
        tabDataKeys.forEach(function(key) { if (tabData.data[key] !== undefined) appData[key] = tabData.data[key]; });
        if (window._csiInvalidateSearchIndex) window._csiInvalidateSearchIndex(); // (audit 2026-07-03) appData muté par pull serveur → index recherche périmé
        _rerenderCurrentTab(tabId);
        if (typeof onApplied === 'function') onApplied();
      }
    }).catch(function(err) { void 0; });
  }

  // FIX-READONLY-REFRESH (2026-06-30) : rafraîchissement périodique de l'onglet courant pour un
  // viewer en LECTURE SEULE. Branché sur le tick d'autosave (qui ne pousse rien en lecture seule)
  // → pas de timer supplémentaire. Très conservateur : seulement si l'onglet navigateur est
  // visible, aucune modale ouverte, et un onglet de données actif.
  function _readOnlyRefreshTick() {
    if (!(typeof isServerModeUnlocked === 'function' && isServerModeUnlocked())) return;
    if (!(window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn())) return;
    if (document.hidden) return;
    var ov = document.getElementById('modalOverlay');
    if (ov && ov.offsetParent !== null) return;          // ne pas perturber une modale ouverte
    var tab = _currentActiveTab;
    if (!tab || tab === 'dashboard' || tab === 'settings') return;
    _pullAndApplyTab(tab, function() {
      showToast2(t('toasts.dataRefreshed', 'Données actualisées (modifiées par un autre utilisateur)'), 'info');
    });
  }

  function loadModuleContent(moduleId) {
    // === CSI-ME === (audit rôles 2026-07-03, MAJEUR) section en droit 'read' pour un
    // compte de sous-entité : avertir à l'OUVERTURE (une fois par section et par session).
    // L'UI d'édition reste globale ; le serveur refuse l'écriture (403 SECTION_READONLY,
    // branche dédiée au push) — sans cet avertissement l'utilisateur découvrait la
    // lecture seule après avoir saisi ses modifications.
    try {
      var _emRo = window.CSI_EntityManager;
      if (_emRo && _emRo.isMultiEntity() && _emRo.isSubEntityUser() && _emRo.sectionRight(moduleId) === 'read') {
        window._csiRoWarnedTabs = window._csiRoWarnedTabs || {};
        if (!window._csiRoWarnedTabs[moduleId] && typeof showToast2 === 'function') {
          window._csiRoWarnedTabs[moduleId] = true;
          showToast2(t('entities.readonlySectionNote', 'Section en lecture seule pour votre périmètre — les modifications n\'y seront pas enregistrées.'), 'info', 7000);
        }
      }
    } catch (e) { /* informatif */ }
    // Tab switch with data sync (server mode) or local save
    var AuthMgr = window.CSI_AuthManager;
    if (isServerModeUnlocked() && AuthMgr && AuthMgr.isLoggedIn() && !isReadOnlyMode()) {
      // AUDIT-CO-H1: Sérialiser les navigations (empêche les release/acquire entrelacés)
      // FIX-TABNAV-QUEUE : la navigation n'est plus perdue — la dernière destination
      // demandée est mémorisée et rejouée dès la fin du changement en cours.
      if (_tabSwitchInProgress) {
        _queuedTabSwitch = moduleId;
        void 0;
        return;
      }
      _tabSwitchInProgress = true;
      var prevTab = _currentActiveTab;
      var PresenceMgr = window.CSI_PresenceManager || window.CSI_LockManager;

      // Push données du tab précédent avant navigation
      function _proceedToNewTab() {
       try {
        _currentActiveTab = moduleId;
        window._currentActiveTab = moduleId;
        _tabSwitchInProgress = false;

        // FIX-TABNAV-QUEUE : rejouer la dernière navigation demandée pendant ce changement
        if (_queuedTabSwitch && _queuedTabSwitch !== moduleId) {
          var _nextTab = _queuedTabSwitch;
          _queuedTabSwitch = null;
          setTimeout(function() { loadModuleContent(_nextTab); }, 0);
          return;
        }
        _queuedTabSwitch = null;

        // Notifier le presence manager du changement d'onglet
        if (PresenceMgr && PresenceMgr.notifyTabChange) {
          PresenceMgr.notifyTabChange(moduleId);
        }

        // === CSI-CO === garantir la grappe du nouvel onglet (acquisition silencieuse si
        // libre ; refus → écran en lecture seule + bandeau nominatif). No-op hors co-édition.
        _csiEnsureClusterForTab(moduleId);

        // Pull données fraîches en mode serveur (sauf dashboard/settings)
        if (moduleId !== 'dashboard' && moduleId !== 'settings') {
          var SyncMgr = window.CSI_SyncManager;
          if (SyncMgr && SyncMgr.pullTabFromServer) {
            var _pullTargetTab = moduleId;
            SyncMgr.pullTabFromServer(_pullTargetTab).then(function(tabData) {
              if (_currentActiveTab !== _pullTargetTab) return;
              // FIX-PUSH-QUEUE (audit 2026-06-10) : ne JAMAIS écraser les clés d'un onglet
              // dont des modifications locales attendent encore d'être poussées — c'était
              // le 2e étage de la perte de données (push jeté puis pull écraseur).
              if (_csiDirtyTabs[_pullTargetTab]) {
                void 0;
                _retryDirtyServerPushes();
                return;
              }
              if (tabData && typeof tabData === 'object') {
                // FIX-OPTIMISTIC-REV (2026-06-11) : mémoriser la révision serveur de l'onglet
                // pour que le prochain push parte de la bonne base (détection de conflit).
                if (typeof tabData._rev === 'number') _tabRevisions[moduleId] = tabData._rev;
                var tabDataKeys = PresenceMgr && PresenceMgr.getTabDataKeys ? PresenceMgr.getTabDataKeys()[moduleId] : null;
                if (tabDataKeys && tabData.data) {
                  tabDataKeys.forEach(function(key) {
                    if (tabData.data[key] !== undefined) {
                      appData[key] = tabData.data[key];
                    }
                  });
                  if (window._csiInvalidateSearchIndex) window._csiInvalidateSearchIndex(); // (audit 2026-07-03) appData muté par pull serveur → index recherche périmé
                  _rerenderCurrentTab(moduleId);
                }
              }
            }).catch(function(err) {
              void 0;
            });
          }
        }
       } catch(err) {
        void 0;
        _tabSwitchInProgress = false;
       }
      }

      // Push données du tab précédent avant de changer
      if (prevTab && prevTab !== 'dashboard' && prevTab !== 'settings') {
        var _isServerPush = typeof isServerModeUnlocked === 'function' && isServerModeUnlocked()
            && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn();
        if (_isServerPush) {
          // Mode serveur : push les données de l'ancien onglet
          var _tabKeys = PresenceMgr && PresenceMgr.getTabDataKeys ? PresenceMgr.getTabDataKeys()[prevTab] : null;
          var _pushPromise = Promise.resolve();
          if (_tabKeys && window.appData) {
            var _tabData = {};
            _tabKeys.forEach(function(key) { _tabData[key] = window.appData[key]; });
            _pushPromise = _serverDataPushWithRetry(prevTab, _tabData, PresenceMgr, 3);
          }
          _pushPromise.then(_proceedToNewTab).catch(_proceedToNewTab);
        } else {
          // Mode local : saveData synchrone
          try { saveData(); } catch(e) { void 0; }
          _proceedToNewTab();
        }
      } else {
        _proceedToNewTab();
      }
    } else {
      _currentActiveTab = moduleId;
      window._currentActiveTab = moduleId;
      // FIX-READONLY-REFRESH (2026-06-30) : en LECTURE SEULE + mode serveur, rafraîchir aussi les
      // données de l'onglet au changement (le bloc d'édition ci-dessus est gardé par !isReadOnlyMode).
      // Aucun push, aucune opération de verrou. Sans ceci, le viewer ne voit jamais les modifs des autres.
      if (isServerModeUnlocked() && AuthMgr && AuthMgr.isLoggedIn()) {
        _pullAndApplyTab(moduleId, null);
      }
      // === CSI-CO === on passe ici quand l'onglet PRÉCÉDENT était en lecture seule de grappe
      // (_csiClusterReadOnly=true → isReadOnlyMode()=true). Réévaluer pour le NOUVEL onglet :
      // retour sur une grappe détenue/libre → redevient éditable. No-op si editModeEnabled=false.
      _csiEnsureClusterForTab(moduleId);
    }

    // Cleanup chart instances when navigating away from risk-analysis
    if (moduleId !== 'risk-analysis' && typeof window.cleanupRiskAnalysisModule === 'function') {
      window.cleanupRiskAnalysisModule();
    }

    switch (moduleId) {
      case "dashboard":
        updateDashboard();
        break;
      case "controls":
        loadControlsTable();
        break;
      case "nis2-controls":
        loadNis2ControlsTable();
        break;
      case "recyf-controls":
        loadRecyfControlsTable();
        break;
      case "nis2-program":
        loadNis2Program();
        break;
      case "actions":
        loadActionsTable();
        break;
      case "risks":
        loadRisksTable();
        updateRiskMatrix();
        break;
      case "threats-section":
        loadThreatsTable();
        break;
      case "nonconformities":
        loadNonConformitiesTable();
        break;
      case "soa":
        loadSoaTable();
        break;
      case "docReview":
        loadDocReviewTable();
        break;
      case "audits":
        loadAuditsTable();
        break;
      case "managementReviews":
        loadReviewsTable();
        break;
      case "stakeholders":
        loadStakeholdersTable();
        break;
      case "documents":
        loadDocumentsTable();
        break;
      case "kpis":
        loadKpiTable();
        break;
      case "critical-assets":
        loadCriticalAssetsTable();
        break;
      case "project-risks":
        loadProjectRisksTable();
        break;
      case "nis2-socle":
        renderSocleGrids();
        break;
      case "risk-analysis":
        if (typeof renderRiskAnalysisModule === 'function') renderRiskAnalysisModule();
        break;
      case "impact-matrices":
        if (typeof renderImpactMatricesModule === 'function') renderImpactMatricesModule();
        break;
      case "framework-mapping":
        if (typeof loadFrameworkMappingView === 'function') loadFrameworkMappingView();
        break;
      case "auditLog":
        renderAuditLogModule();
        break;
      case "manual":
        break;
      case "settings":
        renderSettingsPage();
        break;
    }
    refreshReadOnlyLocks();
  }

  // ========================================
  // RACCOURCIS CLAVIER GLOBAUX
  // ========================================
  var _keyboardShortcutsSetup = false;
  function setupKeyboardShortcuts() {
    if (_keyboardShortcutsSetup) return;
    _keyboardShortcutsSetup = true;
    document.addEventListener("keydown", (e) => {
      // Ignorer si on est dans un champ de saisie
      const target = e.target;
      const isInput = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable;

      // Escape - Fermer les modals (sauf modales obligatoires)
      if (e.key === "Escape") {
        const openModal = document.querySelector(".modal-overlay--visible, .modal.show, .modal--visible");
        if (openModal) {
          // Ne pas fermer si la modale est obligatoire (hideCancel + hideClose)
          if (_modalMandatory) {
            e.preventDefault();
            return;
          }
          closeModal();
          e.preventDefault();
          return;
        }
      }

      // Ctrl/Cmd + S - Sauvegarder
      if ((e.ctrlKey || e.metaKey) && e.key === "s") {
        e.preventDefault();
        if (typeof saveData === "function") {
          const success = saveData();
          if (success) {
            showToast2(t('toasts.dataSaved'), "success");
          }
        }
        return;
      }

      // Ctrl/Cmd + E - Export rapide (JSON)
      if ((e.ctrlKey || e.metaKey) && e.key === "e" && !isInput) {
        e.preventDefault();
        if (typeof exportData === "function") {
          exportData();
        }
        return;
      }

      // Ctrl/Cmd + P - Export PDF
      if ((e.ctrlKey || e.metaKey) && e.key === "p" && !isInput) {
        e.preventDefault();
        if (typeof generatePdfReportWithFeedback === "function") {
          generatePdfReportWithFeedback();
        }
        return;
      }

      // Ctrl/Cmd + , - Ouvrir les paramètres
      if ((e.ctrlKey || e.metaKey) && e.key === ",") {
        e.preventDefault();
        if (typeof openSettings === "function") {
          openSettings();
        }
        return;
      }

      // Ctrl/Cmd + D - Aller au Dashboard
      if ((e.ctrlKey || e.metaKey) && e.key === "d" && !isInput) {
        e.preventDefault();
        const dashNav = document.querySelector('[data-tab="dashboard"]');
        if (dashNav) dashNav.click();
        return;
      }
    });

    void 0;
  }

  // app.js
  // Système de sauvegarde automatique
  let _autoSaveInterval = null;
  let _lastSavedHash = null;
  let _autoSavePaused = false;

  function computeDataHash() {
    // Optimized hash: no per-char variable allocation, bitwise OR coerces to int
    var str = JSON.stringify(appData);
    var hash = 0;
    var len = str.length;
    for (var i = 0; i < len; i++) {
      hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
    }
    return hash;
  }

  function autoSave() {
    if (_autoSavePaused) return;
    if (typeof isReadOnlyMode === 'function' && isReadOnlyMode()) {
      // FIX-READONLY-REFRESH (2026-06-30) : en lecture seule, l'autosave ne pousse rien — on
      // réutilise son créneau pour rafraîchir l'onglet courant (voir les modifs des autres).
      _readOnlyRefreshTick();
      return;
    }

    const currentHash = computeDataHash();
    if (currentHash !== _lastSavedHash) {
      const success = saveData();
      if (success) {
        _lastSavedHash = currentHash;
        // Refonte header 2026-07 : en mode serveur, saveData() pilote lui-même la pastille
        // (« saving » pendant le push asynchrone, puis HH:MM ou erreur dans le .then) —
        // un appel synchrone ici écraserait l'état « saving » avant tout rendu.
        if (!(typeof isServerModeUnlocked === 'function' && isServerModeUnlocked())) {
          updateAutoSaveIndicator();
        }
      }
    }
    // FIX-PUSH-QUEUE (audit 2026-06-10) : filet de l'autosave — re-pousser les onglets
    // dont un push précédent a échoué (réseau, mutex) même si le hash global n'a pas
    // changé depuis (les données sont sinon bloquées en mémoire jusqu'au rechargement).
    if (typeof _retryDirtyServerPushes === 'function') _retryDirtyServerPushes();
  }

  // Refonte header 2026-07 : pastille de synchronisation (.csi-sync-dot).
  // États : neutre/synchronisé (point vert), --saving (point warning + pulse),
  // --error (point danger). Le texte reste court : « Synchronisé » au démarrage,
  // puis « HH:MM » de la dernière sauvegarde ; le détail complet vit dans title.
  function _syncDotSetState(state, text) {
    var indicator = document.getElementById("autoSaveIndicator");
    if (!indicator) return null;
    indicator.classList.remove("csi-sync-dot--saving", "csi-sync-dot--error");
    if (state) indicator.classList.add("csi-sync-dot--" + state);
    if (text != null) {
      var textEl = indicator.querySelector(".csi-sync-dot__text");
      if (textEl) {
        textEl.removeAttribute("data-i18n");
        textEl.textContent = text;
      } else {
        indicator.textContent = text;
      }
    }
    return indicator;
  }

  function updateAutoSaveIndicator() {
    const now = new Date();
    let shortTime;
    try {
      shortTime = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
    } catch (e) {
      shortTime = now.toLocaleTimeString();
    }
    const indicator = _syncDotSetState(null, shortTime);
    if (!indicator) return;
    indicator.title = `${t('common.savedAt')} ${now.toLocaleTimeString()}`;
    indicator.removeAttribute("data-i18n-title");
    indicator.classList.add("autosave-flash");
    setTimeout(() => indicator.classList.remove("autosave-flash"), 1000);
  }

  function initAutoSave(intervalMs = 30000) {
    // Calcul du hash initial
    _lastSavedHash = computeDataHash();

    // Démarrer l'intervalle d'autosave
    if (_autoSaveInterval) clearInterval(_autoSaveInterval);
    _autoSaveInterval = setInterval(autoSave, intervalMs);

    // Sauvegarder aussi avant de quitter la page
    window.addEventListener("beforeunload", () => {
      autoSave();
      _autoSavePaused = true; // Éviter double sauvegarde après
    });

    // Sauvegarder quand la page perd le focus (changement d'onglet)
    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "hidden") {
        autoSave();
      }
    });

    void 0;
  }

  function pauseAutoSave() {
    _autoSavePaused = true;
  }

  function resumeAutoSave() {
    _autoSavePaused = false;
  }

  window.initAutoSave = initAutoSave;
  window.pauseAutoSave = pauseAutoSave;
  window.resumeAutoSave = resumeAutoSave;

  /**
   * Crée et affiche le bandeau DEMO discret en haut de page
   */
  function initDemoBanner() {
    if (!DEMO_MODE) return;

    const banner = document.createElement('div');
    banner.id = 'demoBanner';
    banner.className = 'demo-banner';
    const _t = window.CSI_i18n ? window.CSI_i18n.t.bind(window.CSI_i18n) : (k) => k;
    banner.innerHTML = `
      <div class="demo-banner__content">
        <span class="demo-banner__text">
          <i class="fas fa-flask"></i>
          <span data-i18n="demo.banner.text">${_t('demo.banner.text')}</span>
        </span>
        <a href="${DEMO_WEBSITE_URL}" target="_blank" rel="noopener" class="demo-banner__link">
          <i class="fas fa-arrow-left"></i>
          <span data-i18n="demo.banner.backWebsite">${_t('demo.banner.backWebsite')}</span>
        </a>
        <a href="mailto:${DEMO_CONTACT_EMAIL}" class="demo-banner__link">
          <i class="fas fa-envelope"></i>
          <span data-i18n="demo.banner.contact">${_t('demo.banner.contact')}</span>
        </a>
      </div>
    `;

    // Insérer au tout début du body
    document.body.insertBefore(banner, document.body.firstChild);

    // Ajouter les styles inline pour le bandeau
    const style = document.createElement('style');
    style.textContent = `
      .demo-banner {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 100000;
        background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
        color: #e8e8e8;
        padding: 8px 16px;
        font-size: 0.85rem;
        box-shadow: 0 2px 8px rgba(0,0,0,0.15);
      }
      .demo-banner__content {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 24px;
        max-width: 1200px;
        margin: 0 auto;
      }
      .demo-banner__text {
        display: flex;
        align-items: center;
        gap: 8px;
        opacity: 0.9;
      }
      .demo-banner__text i {
        color: #64b5f6;
      }
      .demo-banner__link {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        background: rgba(255,255,255,0.1);
        color: #90caf9;
        padding: 6px 14px;
        border-radius: 20px;
        text-decoration: none;
        font-weight: 500;
        transition: all 0.2s ease;
        border: 1px solid rgba(144,202,249,0.3);
      }
      .demo-banner__link:hover {
        background: rgba(255,255,255,0.15);
        color: #fff;
        border-color: rgba(255,255,255,0.4);
        transform: translateY(-1px);
      }
      .demo-banner__link .fa-external-link-alt {
        font-size: 0.7em;
        opacity: 0.7;
      }
      /* Décaler le contenu principal pour le bandeau */
      body.demo-mode {
        padding-top: 44px !important;
      }
      body.demo-mode .app-header {
        top: 44px;
      }
      body.demo-mode .sidebar {
        top: calc(var(--header-height, 60px) + 44px);
        height: calc(100vh - var(--header-height, 60px) - 44px);
      }
      @media (max-width: 768px) {
        .demo-banner__content {
          flex-direction: column;
          gap: 8px;
        }
        .demo-banner {
          padding: 10px 12px;
        }
      }
    `;
    document.head.appendChild(style);

    // Ajouter classe au body
    document.body.classList.add('demo-mode');
  }

  function loadAppVersion() {
    fetchWithTimeout('version.json', {}, 5000)
      .then(function(r) { return r.ok ? r.json() : null; })
      .then(function(data) {
        if (data && data.version) {
          var v = data.version;
          if (window.CSI) window.CSI.version = v;
          var el = document.getElementById('appVersionFooter');
          if (el) el.textContent = 'V' + v;
          var badge = document.getElementById('appVersionBadge');
          if (badge) badge.textContent = 'V' + String(v).split('.')[0];
        }
      })
      .catch(function() { /* version.json optionnel */ });
  }

  async function initApp() {
    try {
      // Mode DEMO: toujours charger les données démo (écrase le cache localStorage)
      if (DEMO_MODE) {
        void 0;
        loadDemoDataSilent();

        // Activer le mode édition par défaut en DEMO (pas de mot de passe)
        editModeEnabled = true;
      }

      // Login wall: In server mode, require JWT authentication before showing any data
      // Ensure LicenseManager detection is complete before checking server mode
      // (detect() is async and may not have resolved yet from server-setup.js's setTimeout)
      if (!DEMO_MODE && window.location.protocol !== 'file:') {
        var _lm = window.CSI_LicenseManager;
        if (_lm && !_lm.isDetected()) {
          await _lm.detect().catch(function() {});
        }
        // Ensure TrialManager detection is complete before UI rendering (wizard + license section)
        var _tm = window.CSI_TrialManager;
        if (_tm && !_tm.isDetected()) {
          await _tm.detect().catch(function() {});
        }
      }
      // Détecter LocalTrialManager + TrialManager pour Corporate Local
      var _edInit = window._csiEdition;
      if (!DEMO_MODE && _edInit && _edInit.id === 'corporate-local') {
        var _ltmInit = window.CSI_LocalTrialManager;
        if (_ltmInit) {
          // Détecter d'abord le LocalTrialManager (licence/trial local)
          if (!_ltmInit.isDetected()) {
            await _ltmInit.detect().catch(function() {});
          }
          // Puis le TrialManager global (qui délègue au LTM en corporate-local)
          var _tmLocal = window.CSI_TrialManager;
          if (_tmLocal && !_tmLocal.isDetected()) {
            await _tmLocal.detect().catch(function() {});
          }
        }
      }
      if (!DEMO_MODE && isServerModeUnlocked()) {
        var _authMgr = window.CSI_AuthManager;
        // FIX-SSO-BOOT-RACE: attendre que l'init auth ait fini (sso-redeem async notamment)
        // avant de tester isLoggedIn(). Sinon, en arrivant avec #sso-code=XXX dans l'URL,
        // le boot appelle validateToken() AVANT que sso-redeem ait stocké le token →
        // showLoginModal apparaît à tort, l'utilisateur voit la page de login malgré
        // une authentification SSO réussie. onAuthReady attend la fin du flow d'init.
        if (_authMgr && typeof _authMgr.onAuthReady === 'function') {
          await new Promise(function(resolve) { _authMgr.onAuthReady(resolve); });
        }
        if (_authMgr && !_authMgr.isLoggedIn()) {
          // Try to validate existing token first
          var _validUser = await _authMgr.validateToken();
          if (!_validUser) {
            // No valid token — show login modal (opaque background, non-dismissible)
            await new Promise(function(resolve) {
              _authMgr.showLoginModal(function(loggedIn) {
                if (!loggedIn) return; // modal is non-dismissible, should not happen
                if (_authMgr.mustChangePassword()) {
                  _authMgr.showForcePasswordChangeModal(function() { resolve(); });
                } else {
                  resolve();
                }
              });
            });
          } else if (_authMgr.mustChangePassword()) {
            await new Promise(function(resolve) {
              _authMgr.showForcePasswordChangeModal(function() { resolve(); });
            });
          }
        } else if (_authMgr && _authMgr.mustChangePassword()) {
          await new Promise(function(resolve) {
            _authMgr.showForcePasswordChangeModal(function() { resolve(); });
          });
        }
      }

      const hasCachedData = (() => {
        // En mode serveur, les données viendront du serveur (pas de cache local)
        if (window.CSI_Storage && window.CSI_Storage.isServerMode()) return true;
        try {
          return !!localStorage.getItem("smsi_data");
        } catch (e) {
          return false;
        }
      })();
      const showWelcome = shouldShowWelcome(hasCachedData);
      await loadData();

      // Étape 6: Blocage si serveur injoignable au chargement (mode serveur uniquement)
      if (window._csiServerLoadFailed && window.CSI_Storage && window.CSI_Storage.isServerMode()) {
        var overlay = document.createElement('div');
        overlay.id = 'csi-server-unreachable';
        overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px;color:white;font-family:system-ui;';

        var icon = document.createElement('i');
        icon.className = 'fas fa-server';
        icon.style.cssText = 'font-size:3rem;color:#ef4444;';
        overlay.appendChild(icon);

        var title = document.createElement('h2');
        title.style.cssText = 'margin:0;font-size:1.5rem;';
        title.textContent = t('errors.serverUnreachable', 'Serveur inaccessible');
        overlay.appendChild(title);

        var desc = document.createElement('p');
        desc.style.cssText = 'margin:0;max-width:400px;text-align:center;opacity:0.8;';
        desc.textContent = t('errors.serverUnreachableDesc', 'Impossible de charger les données depuis le serveur. Vérifiez votre connexion réseau et que le serveur est en cours d\'exécution.');
        overlay.appendChild(desc);

        var retryBtn = document.createElement('button');
        retryBtn.style.cssText = 'margin-top:8px;padding:10px 24px;background:#3b82f6;color:white;border:none;border-radius:6px;cursor:pointer;font-size:14px;';
        retryBtn.textContent = t('common.retry', 'Réessayer');
        retryBtn.addEventListener('click', function() { location.reload(); });
        overlay.appendChild(retryBtn);

        document.body.appendChild(overlay);
        return; // Bloquer le reste de l'initialisation
      }

      initThemeToggle();
      // Restore compact mode
      try {
        if (localStorage.getItem("smsi_compact_mode") === "1") {
          document.body.classList.add("compact-mode");
        }
      } catch (e) { /* ignore */ }

      // Mode DEMO: initialiser le bandeau et skip les popups de sécurité
      if (DEMO_MODE) {
        initDemoBanner();
        // Démarrer le rechargement automatique des données (toutes les heures)
        startDemoAutoReload();
        // Marquer le setup serveur comme fait (mais pas le welcome)
        try {
          localStorage.setItem('smsi_setup_completed', '1');
          localStorage.setItem('smsi_deployment_mode', 'local');
          // Note: Ne pas marquer WELCOME_STORAGE_KEY ici, le wizard le fera
        } catch (e) { void 0; }
      }

      // Global unhandled promise rejection handler
      window.addEventListener('unhandledrejection', function(event) {
        void 0;
      });

      initReadOnlyBanner();
      initTrialBanner();
      updateAppTitle();
      setupNavigation();
      if (window.CSI_FrameworkMapper) CSI_FrameworkMapper.applyFrameworkVisibility();
      if (typeof applyNis2Referential === 'function') applyNis2Referential(false);
      setupEventListeners();
      setupKeyboardShortcuts();
      setupGlobalSearch();
      initDashboard();
      loadAppVersion();
      initAutoSave(30000); // Autosave toutes les 30 secondes

      // Start PresenceManager in server mode
      if (window.CSI_PresenceManager && typeof CSI_PresenceManager.start === 'function' && isServerModeUnlocked()) {
        CSI_PresenceManager.start();
      }

      // Start EditLockManager (verrou d'édition global) + abonnement aux events
      if (window.CSI_EditLockManager && typeof CSI_EditLockManager.start === 'function' && isServerModeUnlocked()) {
        CSI_EditLockManager.start();
        CSI_EditLockManager.subscribe(function (event, payload) {
          if (event === 'lock-lost') {
            // Le serveur nous a retiré le verrou (expiration ou takeover admin).
            // (audit co-édition 2026-07-03, MAJEUR) distinguer les DEUX causes : si personne
            // ne détient le verrou (payload.remoteLock absent), c'est une EXPIRATION (veille,
            // perte réseau > TTL) — accuser « l'administrateur a repris la main » était faux
            // et source de conflits d'équipe. La clé lockExpired existait sans être utilisée.
            if (!isReadOnlyMode()) {
              setEditMode(false, { silent: true });
              var _lockHolderNow = payload && payload.remoteLock;
              showToast2(_lockHolderNow
                ? t('modeBanner.lockStolen', "L'administrateur a repris la main. Vous êtes en lecture seule.")
                : t('modeBanner.lockExpired', "Votre verrou d'édition a expiré (perte de connexion). Repassez en mode édition pour continuer."), 'warning');
            }
            updateEditModeButtonUi();
            updateModeBannerUi();
          } else if (event === 'inactivity-auto-quit') {
            // Auto-quit après 5 min sans modification
            if (!isReadOnlyMode()) {
              setEditMode(false, { silent: true });
              if (CSI_EditLockManager.release) { try { CSI_EditLockManager.release(); } catch (e) {} }
              showToast2(t('modeBanner.autoQuitInactivity', 'Mode édition désactivé après 5 min sans modification'), 'info');
            }
            updateEditModeButtonUi();
            updateModeBannerUi();
          } else if (event === 'cluster-lost') {
            // === CSI-CO === une grappe (pas toutes) a été reprise (takeover ciblé). Si c'est
            // celle de l'écran affiché → lecture seule immédiate + bandeau ; sinon simple
            // rafraîchissement (l'écran courant reste éditable sur sa propre grappe).
            if (editModeEnabled && CSI_EditLockManager.isCoedit && CSI_EditLockManager.isCoedit()
                && CSI_EditLockManager.isHeldByMeFor && !CSI_EditLockManager.isHeldByMeFor(_currentActiveTab)) {
              window._csiClusterReadOnly = true;
              refreshReadOnlyLocks();
              showToast2(t('modeBanner.lockStolen', "L'administrateur a repris la main. Vous êtes en lecture seule."), 'warning');
            }
            updateEditModeButtonUi();
            updateModeBannerUi();
          } else if (event === 'holder-changed' || event === 'state-updated') {
            updateEditModeButtonUi();
            updateModeBannerUi();
          }
        });
        // Document-wide listener pour notifier les éditions à EditLockManager (anti auto-quit)
        document.addEventListener('input', function () {
          if (CSI_EditLockManager.isHeldByMe && CSI_EditLockManager.isHeldByMe()) {
            CSI_EditLockManager.notifyEdit();
          }
        }, true);
        document.addEventListener('change', function () {
          if (CSI_EditLockManager.isHeldByMe && CSI_EditLockManager.isHeldByMe()) {
            CSI_EditLockManager.notifyEdit();
          }
        }, true);
      }

      // Mode local : la pastille ne « synchronise » rien — libellé neutre de
      // sauvegarde automatique (l'heure remplace le texte dès la 1re autosave).
      if (!isServerModeUnlocked()) {
        var _syncTextEl = document.querySelector('#autoSaveIndicator .csi-sync-dot__text');
        if (_syncTextEl) {
          _syncTextEl.setAttribute('data-i18n', 'header.syncLocal');
          _syncTextEl.textContent = t('header.syncLocal', 'Sauvegarde auto');
        }
      }

      // Server mode: show user indicator + logout button in header
      if (isServerModeUnlocked() && window.CSI_AuthManager && window.CSI_AuthManager.isLoggedIn()) {
        var _sui = document.getElementById('serverUserIndicator');
        if (_sui) {
          var _currentUsr = window.CSI_AuthManager.getCurrentUser();
          var _nameSpan = _sui.querySelector('.server-user-indicator__name');
          if (_nameSpan && _currentUsr) _nameSpan.textContent = _currentUsr.name;
          _sui.style.display = '';
        }
        var _logoutBtn = document.getElementById('serverLogoutBtn');
        if (_logoutBtn) {
          _logoutBtn.addEventListener('click', function() {
            _logoutBtn.disabled = true;
            window.CSI_AuthManager.logout().then(function() {
              location.reload();
            }).catch(function() {
              location.reload();
            });
          });
        }
      }

      // Refonte header 2026-07 : menu utilisateur (avatar + items) — après l'init auth
      // ci-dessus pour connaître l'utilisateur courant (mode serveur) ou son absence (local).
      initUserMenu();
      // Palier étroit ≤1120px : le bouton édition devient icône seule → reposer title/aria
      // quand on franchit le seuil (updateEditModeButtonUi n'est sinon rappelé que sur
      // changement d'état). addListener = repli vieux moteurs (file://).
      try {
        var _mqNarrowHeader = window.matchMedia("(max-width: 1120px)");
        var _mqNarrowHandler = function() { updateEditModeButtonUi(); };
        if (_mqNarrowHeader.addEventListener) _mqNarrowHeader.addEventListener("change", _mqNarrowHandler);
        else if (_mqNarrowHeader.addListener) _mqNarrowHeader.addListener(_mqNarrowHandler);
      } catch (e) { /* matchMedia absent : titles posés au prochain changement d'état */ }

      // i18n: translate static HTML elements (data-i18n attributes) on initial load
      if (window.CSI_i18n && typeof window.CSI_i18n.translatePage === 'function') {
        window.CSI_i18n.translatePage();
      }

      // Toggle manual language on initial load
      var _initLang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : 'fr';
      var _frManual = document.getElementById('manualContentFR');
      var _enManual = document.getElementById('manualContentEN');
      if (_frManual) _frManual.style.display = (_initLang === 'fr') ? '' : 'none';
      if (_enManual) _enManual.style.display = (_initLang !== 'fr') ? '' : 'none';

      hideLoadingSpinner();
      showToast(t('toasts.appLoaded'), "success");

      // Show setup wizard on first launch (combines language + welcome + demo)
      // Chain modals sequentially to prevent stacking (BUG-701)
      function _showCyfunModalIfNeeded() {
        var _fwEnabled = (appData.frameworkMapper && appData.frameworkMapper.enabledFrameworks) || [];
        // m-RC4 (audit fond 2026-07-11) : inclure recyf2025 — la modale fallback de choix de
        // niveau/degré ne se déclenchait jamais pour un utilisateur ReCyF (garde CyFun-only).
        var _nis2Enabled = _fwEnabled.indexOf('nis2') >= 0 || _fwEnabled.indexOf('cyfun2025') >= 0 || _fwEnabled.indexOf('recyf2025') >= 0;
        // FIX: If cyfunLevel is already set (configured by admin or another user),
        // treat _cyfunLevelChosen as true to prevent showing the wizard again.
        // m-RC4 (revu) : NE PAS étendre ce court-circuit à recyfLevel — il reçoit un défaut
        // 'importante' à CHAQUE chargement (normalisation), donc « défini » ≠ « choisi » :
        // l'étendre supprimait la modale de choix de degré pour un import ReCyF legacy.
        // cyfunLevel, lui, reste '' tant qu'aucun choix n'a été fait (marqueur fiable).
        if (_nis2Enabled && appData.cyfunLevel && !appData._cyfunLevelChosen) {
          appData._cyfunLevelChosen = true;
        }
        if (_nis2Enabled && !appData._cyfunLevelChosen) {
          setTimeout(function() {
            var modal = document.getElementById("cyfunLevelModal");
            if (modal) {
              var curLang = appData.cyfunLang || 'en';
              var langRadio = modal.querySelector('input[name="cyfunLangChoice"][value="' + curLang + '"]');
              if (langRadio) langRadio.checked = true;
              if (window.prepareNis2LevelModal) window.prepareNis2LevelModal();
              modal.classList.add("modal-overlay--visible");
            }
          }, 300);
        }
      }

      function _showPasswordSetupThenCyfun() {
        if (typeof queueEditPasswordSetup === 'function' && typeof shouldPromptEditPasswordSetup === 'function' && shouldPromptEditPasswordSetup()) {
          // Wait for modalOverlay to be dismissed before showing CyFun
          // Uses MutationObserver on the actual modalOverlay element (not phantom ID)
          var _overlay = document.getElementById('modalOverlay');
          if (_overlay) {
            var _pwdObserver = new MutationObserver(function(mutations) {
              if (!_overlay.classList.contains('modal-overlay--visible')) {
                _pwdObserver.disconnect();
                setTimeout(_showCyfunModalIfNeeded, 300);
              }
            });
            _pwdObserver.observe(_overlay, { attributes: true, attributeFilter: ['class'] });
            queueEditPasswordSetup();
            // Timeout fallback in case observer never fires
            setTimeout(function() { _pwdObserver.disconnect(); _showCyfunModalIfNeeded(); }, 30000);
          } else {
            queueEditPasswordSetup();
            setTimeout(_showCyfunModalIfNeeded, 2000);
          }
        } else {
          _showCyfunModalIfNeeded();
        }
      }

      if (DEMO_MODE) {
        showDemoWelcomeWizard();
      } else if (showWelcome) {
        // Chain: wizard → password setup → CyFun modal (sequential, no stacking)
        showSetupWizard(function() {
          setTimeout(_showPasswordSetupThenCyfun, 300);
        });
        // If wizard was skipped (not first launch), still check password + CyFun
        // The wizard calls onComplete immediately if skipped
      } else {
        // No wizard needed, proceed directly
        setTimeout(_showPasswordSetupThenCyfun, 300);
      }
      // Sync settings dropdown with current level
      var _cyfLvlSel = document.getElementById("cyfunLevelSelect");
      if (_cyfLvlSel) _cyfLvlSel.value = appData.cyfunLevel || 'basic';
    } catch (error) {
      void 0;
      hideLoadingSpinner();
      showToast(t('toasts.appLoadError', 'Erreur lors du chargement : {{message}}').replace('{{message}}', error.message), "error");
    }
  }

  /**
   * Wizard de bienvenue simplifié pour le mode DEMO
   * Affiche uniquement le choix de langue puis une page d'accueil
   */
  function showDemoWelcomeWizard() {
    // Vérifier si déjà vu (utiliser une clé spécifique pour la démo)
    const DEMO_WELCOME_KEY = 'smsi_demo_welcome_seen';
    try {
      if (localStorage.getItem(DEMO_WELCOME_KEY) === '1') return;
    } catch (e) {}

    if (document.getElementById("demoWelcomeWizard")) return;

    // Force English for initial wizard render (first slide always in English)
    if (window.CSI_i18n) window.CSI_i18n.setLanguage('en');
    const _t = window.CSI_i18n ? window.CSI_i18n.t.bind(window.CSI_i18n) : (k) => k;
    const currentLang = 'en';
    const overlay = document.createElement("div");
    overlay.id = "demoWelcomeWizard";
    overlay.className = "wizard-overlay";
    overlay.innerHTML = `
      <div class="wizard-modal" role="dialog" aria-modal="true" aria-labelledby="demoWizardTitle" style="max-width: 480px; overflow-y: auto;">
        <div style="padding: 28px;">
          <div style="text-align: center; margin-bottom: 20px;">
            <div style="font-size: 2.5rem; margin-bottom: 12px;">
              <i class="fas fa-shield-alt" style="color: var(--color-primary, #3b82f6);"></i>
            </div>
            <h2 id="demoWizardTitle" style="margin: 0 0 6px 0; font-size: 1.35rem; color: var(--color-text);">
              ${_t('demo.welcome.title')}
            </h2>
            <p id="demoWizardSubtitle" style="margin: 0; color: var(--color-text-secondary); font-size: 0.9rem;">
              ${_t('demo.welcome.subtitle')}
            </p>
          </div>

          <div style="margin-bottom: 20px;">
            <label id="demoWizardLangLabel" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--color-text); font-size: 0.9rem;">
              <i class="fas fa-globe"></i> ${_t('demo.welcome.languageLabel')}
            </label>
            <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
              ${window.CSI_i18n ? Object.entries(window.CSI_i18n.getAvailableLanguages()).map(([code, info]) => `
                <button class="wizard-lang-btn ${code === currentLang ? 'wizard-lang-btn--selected' : ''}" type="button" data-lang="${code}" style="padding: 10px 8px; border: 2px solid var(--color-border); border-radius: 8px; background: var(--color-surface); cursor: pointer; transition: all 0.2s;">
                  <span style="font-size: 1.3rem; display: block;">${info.flag}</span>
                  <span style="font-size: 0.8rem; color: var(--color-text);">${info.name}</span>
                </button>
              `).join("") : ''}
            </div>
          </div>

          <div style="background: var(--color-surface-hover, #f8f9fa); border-radius: 8px; padding: 14px; margin-bottom: 16px;">
            <p id="demoWizardInfo" style="margin: 0; color: var(--color-text); font-size: 0.85rem;">
              <i class="fas fa-info-circle" style="color: var(--color-primary);"></i>
              <strong>${_t('demo.welcome.infoLabel')}</strong> — ${_t('demo.welcome.info')}
            </p>
          </div>

          <div style="background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(139, 92, 246, 0.08)); border-radius: 8px; padding: 14px; margin-bottom: 20px; border: 1px solid rgba(59, 130, 246, 0.2);">
            <p id="demoWizardVersions" style="margin: 0 0 10px 0; color: var(--color-text); font-size: 0.85rem; font-weight: 500;">
              <i class="fas fa-download" style="color: var(--color-primary);"></i> ${_t('demo.welcome.versions')}
            </p>
            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
              <div style="background: var(--color-surface); border-radius: 6px; padding: 8px; text-align: center;">
                <i class="fas fa-laptop" style="color: var(--color-primary); font-size: 1rem;"></i>
                <p id="demoWizardLocal" style="margin: 4px 0 0 0; font-size: 0.75rem; color: var(--color-text);"><strong>${_t('demo.welcome.local')}</strong></p>
              </div>
              <div style="background: var(--color-surface); border-radius: 6px; padding: 8px; text-align: center;">
                <i class="fas fa-cloud" style="color: var(--color-success, #22c55e); font-size: 1rem;"></i>
                <p id="demoWizardServer" style="margin: 4px 0 0 0; font-size: 0.75rem; color: var(--color-text);"><strong>${_t('demo.welcome.server')}</strong></p>
              </div>
            </div>
          </div>

          <div style="text-align: center;">
            <button id="demoWizardStart" type="button" style="background: var(--color-primary, #3b82f6); color: white; border: none; padding: 12px 28px; border-radius: 8px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s;">
              <i class="fas fa-rocket"></i> <span id="demoWizardStartText">${_t('demo.welcome.start')}</span>
            </button>
          </div>
        </div>
      </div>
    `;

    document.body.appendChild(overlay);

    // Afficher l'overlay avec animation
    requestAnimationFrame(() => {
      overlay.classList.add('is-visible');
    });

    // Gestion du choix de langue
    const langBtns = overlay.querySelectorAll('.wizard-lang-btn');
    langBtns.forEach(btn => {
      btn.addEventListener('click', () => {
        langBtns.forEach(b => {
          b.classList.remove('wizard-lang-btn--selected');
          b.style.borderColor = 'var(--color-border)';
          b.style.background = 'var(--color-surface)';
        });
        btn.classList.add('wizard-lang-btn--selected');
        btn.style.borderColor = 'var(--color-primary)';
        btn.style.background = 'var(--color-primary-light, #eff6ff)';

        // Appliquer la langue
        if (window.CSI_i18n) {
          window.CSI_i18n.setLanguage(btn.dataset.lang);
          window.CSI_i18n.markLanguageChosen();
          window.CSI_i18n.translatePage();
          // Re-translate wizard popup
          const _t2 = window.CSI_i18n.t.bind(window.CSI_i18n);
          const titleEl = document.getElementById('demoWizardTitle');
          if (titleEl) titleEl.textContent = _t2('demo.welcome.title');
          const subtitleEl = document.getElementById('demoWizardSubtitle');
          if (subtitleEl) subtitleEl.textContent = _t2('demo.welcome.subtitle');
          const infoEl = document.getElementById('demoWizardInfo');
          if (infoEl) infoEl.innerHTML = '<i class="fas fa-info-circle" style="color: var(--color-primary);"></i> <strong>' + _t2('demo.welcome.infoLabel') + '</strong> — ' + _t2('demo.welcome.info');
          const versionsEl = document.getElementById('demoWizardVersions');
          if (versionsEl) versionsEl.innerHTML = '<i class="fas fa-download" style="color: var(--color-primary);"></i> ' + _t2('demo.welcome.versions');
          const localEl = document.getElementById('demoWizardLocal');
          if (localEl) localEl.innerHTML = '<strong>' + _t2('demo.welcome.local') + '</strong>';
          const serverEl = document.getElementById('demoWizardServer');
          if (serverEl) serverEl.innerHTML = '<strong>' + _t2('demo.welcome.server') + '</strong>';
          const startEl = document.getElementById('demoWizardStartText');
          if (startEl) startEl.textContent = _t2('demo.welcome.start');
        }
      });
    });

    // Bouton démarrer
    document.getElementById('demoWizardStart').addEventListener('click', () => {
      // Marquer comme vu
      try {
        localStorage.setItem(DEMO_WELCOME_KEY, '1');
        if (window.CSI_i18n) {
          window.CSI_i18n.markLanguageChosen();
        }
      } catch (e) { void 0; }
      overlay.remove();
      // Rafraîchir le dashboard
      if (typeof initDashboard === 'function') {
        initDashboard();
      }
    });
  }
  // Shared CyFun scoring engine used by nis2-pdf-client.js, nis2-export-html.js, nis2-print.js
  window.computeCyfunScoringForExport = function() {
    var ad = window.appData;
    if (!ad) return null;
    var level = ad.cyfunLevel || 'basic';
    var controls = window.CSI_FrameworkMapper ? CSI_FrameworkMapper.getAppDataControls('cyfun2025') : (ad.nis2Controls || []);
    var fw = (window._csiFrameworks || {})['cyfun2025'];
    if (!fw) return null;
    var levelConfig = (fw.meta.assuranceLevels || []).find(function(l) { return l.id === level; });
    if (!levelConfig) return null;

    var totalDoc = 0, totalImpl = 0, scoredCount = 0, totalCount = controls.length;
    var keyDoc = 0, keyImpl = 0, keyCount = 0, keyScoredCount = 0;
    var categoryScores = {};

    controls.forEach(function(c) {
      var doc = c.docMaturity || 0;
      var impl = c.implMaturity || 0;
      if (doc > 0 || impl > 0) { totalDoc += doc; totalImpl += impl; scoredCount++; }
      if (c.isKeyMeasure) {
        keyCount++;
        if (doc > 0 || impl > 0) { keyDoc += doc; keyImpl += impl; keyScoredCount++; }
      }
      var catId = c.categoryId;
      if (!categoryScores[catId]) categoryScores[catId] = { docSum: 0, implSum: 0, count: 0, scored: 0 };
      categoryScores[catId].count++;
      if (doc > 0 || impl > 0) {
        categoryScores[catId].docSum += doc;
        categoryScores[catId].implSum += impl;
        categoryScores[catId].scored++;
      }
    });

    // FIX-EXPORT-SCORING (audit 2026-06-10) : aligner les dénominateurs sur la convention
    // de l'écran (recalcNis2Maturity / FrameworkMapper.getCategoryMaturity : moyennes sur les
    // SEULES exigences évaluées, doc>0||impl>0). Avant, avgTotal/avgDoc/avgImpl divisaient
    // par le total (non-évaluées = 0) alors que avgKeyTotal divisait par les évaluées :
    // verdict, moyenne globale et radar embarqué dans le même export se contredisaient.
    var avgDoc = scoredCount > 0 ? +((totalDoc / scoredCount).toFixed(2)) : 0;
    var avgImpl = scoredCount > 0 ? +((totalImpl / scoredCount).toFixed(2)) : 0;
    var avgTotal = scoredCount > 0 ? +(((totalDoc + totalImpl) / (2 * scoredCount)).toFixed(2)) : 0;
    var avgKeyTotal = keyScoredCount > 0 ? +(((keyDoc + keyImpl) / (2 * keyScoredCount)).toFixed(2)) : 0;

    var thresholds = levelConfig.thresholds || {};
    var passes = {
      keyMeasures: avgKeyTotal >= (thresholds.keyMeasures || 0),
      total: avgTotal >= (thresholds.total || 0),
      categories: true
    };

    var catDetails = {};
    Object.keys(categoryScores).forEach(function(catId) {
      var cs = categoryScores[catId];
      // FIX-EXPORT-SCORING (audit 2026-06-10) : moyenne par catégorie sur les exigences
      // évaluées (cs.scored), comme le radar affiché à l'écran (getCategoryMaturity).
      var catAvg = cs.scored > 0 ? +(((cs.docSum + cs.implSum) / (2 * cs.scored)).toFixed(2)) : 0;
      catDetails[catId] = { avg: catAvg, count: cs.count, scored: cs.scored };
      if (thresholds.categoryMin && catAvg < thresholds.categoryMin && cs.count > 0) {
        passes.categories = false;
      }
    });

    var overallPass = passes.keyMeasures && passes.total && passes.categories;

    return {
      level: level,
      levelLabel: levelConfig.label,
      totalRequirements: totalCount,
      scoredRequirements: scoredCount,
      avgDoc: avgDoc,
      avgImpl: avgImpl,
      avgTotal: avgTotal,
      keyMeasureCount: keyCount,
      keyScoredCount: keyScoredCount,
      avgKeyTotal: avgKeyTotal,
      categoryScores: catDetails,
      thresholds: thresholds,
      passes: passes,
      overallPass: overallPass
    };
  };

  document.addEventListener("DOMContentLoaded", initApp);

  // i18n: Refresh all translated constants and re-render UI on language change
  window.addEventListener('languageChange', function() {
    if (window._csiInvalidateSearchIndex) window._csiInvalidateSearchIndex(); // (audit 2026-07-03) libellés i18n de l'index recherche
    // Refresh all option arrays that use t()
    CONTROL_STATUS_OPTIONS = getControlStatusOptions();
    DOMAIN_OPTIONS = getDomainOptions();
    CMM_LEVEL_OPTIONS = getCmmLevelOptions();
    ACTION_PRIORITY_OPTIONS = getActionPriorityOptions();
    ACTION_STATUS_OPTIONS = getActionStatusOptions();
    TREATMENT_OPTIONS = getTreatmentOptions();
    THREAT_STATUS_LABELS = getThreatStatusLabels();
    THREAT_STATUS_OPTIONS = getThreatStatusOptions();
    STATUS_OPTIONS = getNcStatusOptions();
    NC_STATUS_LABELS = {};
    STATUS_OPTIONS.forEach(function(opt) { NC_STATUS_LABELS[opt.value] = opt.label; });
    NC_TYPE_OPTIONS = getNcTypeOptions();
    AUDIT_STATUS_OPTIONS = getAuditStatusOptions();
    AUDIT_STATUS_LABELS = {};
    AUDIT_STATUS_OPTIONS.forEach(function(opt) { AUDIT_STATUS_LABELS[opt.value] = opt.label; });
    AUDIT_TYPE_OPTIONS = getAuditTypeOptions();
    AUDIT_TYPE_LABELS = {};
    AUDIT_TYPE_OPTIONS.forEach(function(opt) { AUDIT_TYPE_LABELS[opt.value] = opt.label; });
    CONTROL_REVIEW_FREQ_OPTIONS = getControlReviewFreqOptions();
    OPERATIONAL_CAPABILITIES = getOperationalCapabilities();
    IMPACT_LEVEL_OPTIONS = getImpactLevelOptions();
    IMPACT_LEVEL_LABELS = getImpactLevelLabels();
    PROBABILITY_LEVEL_OPTIONS = getProbabilityLevelOptions();
    PROBABILITY_LEVEL_LABELS = getProbabilityLevelLabels();
    PROJECT_RISK_STATUS_OPTIONS = getProjectRiskStatusOptions();
    PROJECT_RISK_STATUS_LABELS = getProjectRiskStatusLabels();
    // FIX-I18N-NIS2-ENUMS (audit 2026-07-10) : badges statut/urgence/type du plan NIS2
    NIS2_STATUS_META = getNis2StatusMeta();
    NIS2_URGENCY_LABELS = getNis2UrgencyLabels();
    NIS2_ACTION_TYPE_OPTIONS = getNis2ActionTypeOptions();
    NIS2_ACTION_TYPE_LABELS = getNis2ActionTypeLabels();

    // FIX-I18N-TOOLBARS (audit 2026-06-10, bundle.js:22082) : re-init de TOUTES les barres
    // d'outils (SectionToolbar.init est documenté ré-entrant : destroy + rebuild) — must run
    // BEFORE translatePage so the new DOM elements get their data-i18n attributes translated.
    // L'ancien code ne ré-initialisait que controls et soa : les toolbars des ~14 autres
    // modules (risks, threats, audits, kpis, nis2-controls, recyf-controls…) gardaient les
    // libellés de l'ancienne langue jusqu'au rechargement de la page. Les options de filtre
    // dynamiques (fonctions/catégories NIS2, objectifs ReCyF…) sont repeuplées par les
    // load*Table() au prochain affichage de chaque module, comme au démarrage.
    if (typeof initSectionToolbars === 'function') {
      initSectionToolbars();
    }

    // Translate the page (data-i18n attributes) — runs after SectionToolbar rebuild
    if (window.CSI_i18n && typeof window.CSI_i18n.translatePage === 'function') {
      window.CSI_i18n.translatePage();
    }

    // Refonte header 2026-07 : libellés dynamiques du header (hors data-i18n) —
    // item « Thème : … », rôle du menu utilisateur, title/aria du bouton édition.
    updateThemeToggleUi();
    refreshUserMenuIdentity();
    updateEditModeButtonUi();

    // Toggle manual language (FR / EN)
    var manualLang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : 'fr';
    var frManual = document.getElementById('manualContentFR');
    var enManual = document.getElementById('manualContentEN');
    if (frManual) frManual.style.display = (manualLang === 'fr') ? '' : 'none';
    if (enManual) enManual.style.display = (manualLang !== 'fr') ? '' : 'none';

    // Re-render visible module
    // FIX-I18N-RERENDER-DEFER (revue audit 2026-06-11) : différer d'un tick. setLanguage()
    // dispatche 'languageChange' de façon SYNCHRONE ; quand la langue est changée via le
    // select de Paramètres > Général, renderSettingsPage() reconstruirait la page (et
    // détruirait le select) AU MILIEU du handler 'change' en cours — réentrance fragile et
    // flash de succès du bouton « Enregistrer » perdu. Le report laisse le handler courant
    // se terminer ; translatePage() ci-dessus a déjà traité les attributs data-i18n.
    setTimeout(function() {
    try {
      const activeTab = document.querySelector('.module--active');
      if (activeTab) {
        const tabId = activeTab.id;
        if (tabId === 'dashboard' && typeof updateDashboard === 'function') updateDashboard();
        else if (tabId === 'controls' && typeof loadControlsTable === 'function') loadControlsTable();
        else if (tabId === 'nis2-controls' && typeof loadNis2ControlsTable === 'function') loadNis2ControlsTable();
        else if (tabId === 'recyf-controls' && typeof loadRecyfControlsTable === 'function') loadRecyfControlsTable();
        else if (tabId === 'soa' && typeof loadSoaTable === 'function') loadSoaTable();
        else if (tabId === 'docReview' && typeof loadDocReviewTable === 'function') loadDocReviewTable();
        else if (tabId === 'actions' && typeof loadActionsTable === 'function') loadActionsTable();
        else if (tabId === 'risks' && typeof loadRisksTable === 'function') loadRisksTable();
        else if (tabId === 'threats-section' && typeof loadThreatsTable === 'function') loadThreatsTable();
        else if (tabId === 'nonconformities' && typeof loadNonConformitiesTable === 'function') loadNonConformitiesTable();
        else if (tabId === 'audits' && typeof loadAuditsTable === 'function') loadAuditsTable();
        else if (tabId === 'managementReviews' && typeof loadReviewsTable === 'function') loadReviewsTable();
        else if (tabId === 'stakeholders' && typeof loadStakeholdersTable === 'function') loadStakeholdersTable();
        else if (tabId === 'documents' && typeof loadDocumentsTable === 'function') loadDocumentsTable();
        else if (tabId === 'kpis' && typeof loadKpiTable === 'function') loadKpiTable();
        else if (tabId === 'nis2-program' && typeof loadNis2Program === 'function') loadNis2Program();
        else if (tabId === 'critical-assets' && typeof loadCriticalAssetsTable === 'function') loadCriticalAssetsTable();
        else if (tabId === 'project-risks' && typeof loadProjectRisksTable === 'function') loadProjectRisksTable();
        // FIX-I18N-SETTINGS-RERENDER (audit 2026-06-10, bundle.js:22142) : la page Paramètres
        // (et les onglets ci-dessous, absents de la liste) est construite via t() en
        // textContent sans data-i18n — translatePage() ne peut pas la retraduire. Re-rendu
        // explicite au changement de langue (mêmes fonctions que le switch de switchTab).
        else if (tabId === 'settings' && typeof renderSettingsPage === 'function') renderSettingsPage();
        else if (tabId === 'nis2-socle' && typeof renderSocleGrids === 'function') renderSocleGrids();
        else if (tabId === 'risk-analysis' && typeof renderRiskAnalysisModule === 'function') renderRiskAnalysisModule();
        else if (tabId === 'impact-matrices' && typeof renderImpactMatricesModule === 'function') renderImpactMatricesModule();
        else if (tabId === 'framework-mapping' && typeof loadFrameworkMappingView === 'function') loadFrameworkMappingView();
        else if (tabId === 'auditLog' && typeof renderAuditLogModule === 'function') renderAuditLogModule();
      }
    } catch (e) {
      void 0;
    }
    }, 0);
  });
})();
