/**
 * Cyber-Assistant - Module Matrices d'Impacts
 * Gestion de grilles d'impact personnalisables (niveaux × dimensions)
 * Réutilisables dans les analyses de risques
 */
(function() {
  'use strict';

  // ============================================================
  // HELPERS
  // ============================================================
  function _t(key, fallback) {
    return typeof window.t === 'function' ? window.t(key) : (fallback || key);
  }
  var _esc = window.escapeHtml;
  function _id() {
    return 'imx-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
  }
  function _now() { return new Date().toISOString(); }
  function _deepClone(o) { try { return JSON.parse(JSON.stringify(o)); } catch(e) { void 0; return o; } }

  // === RA-UX (A2) === Couleur par niveau de gravité.
  // `level.color` est une SURCHARGE optionnelle (absente = comportement historique, rendu
  // par le CSS). FIX-REVUE RA-UX : la palette par défaut du picker est celle de
  // `.imx-level-dot` (impact-matrices.css:471+) — le swatch s'affiche À CÔTÉ du dot, les
  // deux doivent montrer la même couleur, sinon confirmer le picker sans rien changer
  // écrivait une surcharge non voulue.
  var DEFAULT_SEVERITY_COLORS = { 1: '#22c55e', 2: '#eab308', 3: '#f97316', 4: '#ef4444', 5: '#dc2626', 6: '#b91c1c', 7: '#7f1d1d' };
  // FIX-REVUE RA-UX : une couleur peut provenir d'un .analyse tiers — seule une couleur hex
  // stricte atteint un attribut style (l'échappement HTML ne couvre pas la grammaire CSS).
  function _safeCssColor(c) {
    return /^#[0-9a-fA-F]{3,8}$/.test(c || '') ? c : '';
  }
  function _levelColor(lv) {
    return _safeCssColor(lv && lv.color) || DEFAULT_SEVERITY_COLORS[lv && lv.level] || '#e5e7eb';
  }
  // Texte lisible sur fond personnalisé (luminance simple — suffit pour des aplats).
  function _textColorFor(hex) {
    var m = /^#?([0-9a-f]{6})$/i.exec(hex || '');
    if (!m) return '#1f2937';
    var n = parseInt(m[1], 16);
    var lum = 0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255);
    return lum > 150 ? '#1f2937' : '#ffffff';
  }
  // Préréglage TLP (FIRST, Traffic Light Protocol 2.0) : on assigne depuis le niveau le
  // plus grave — RED, AMBER, GREEN — et tous les niveaux inférieurs restants en CLEAR.
  var TLP_PALETTE = { clear: '#ffffff', green: '#33a02c', amber: '#ffc000', red: '#ff2b2b' };
  function tlpColorsFor(count) {
    var out = [];
    for (var i = 0; i < count; i++) {
      var fromEnd = count - 1 - i;
      out.push(fromEnd === 0 ? TLP_PALETTE.red : fromEnd === 1 ? TLP_PALETTE.amber : fromEnd === 2 ? TLP_PALETTE.green : TLP_PALETTE.clear);
    }
    return out;
  }

  // ============================================================
  // SEED DATA — Matrice par défaut (5 niveaux, 10 dimensions)
  // ============================================================
  function createDefaultMatrix() {
    return {
      id: 'imx-default-v1',
      name: "Matrice d'impact g\u00e9n\u00e9rique (PESTEL/5M/POTI)",
      description: "Matrice d'impact 5 niveaux, 10 dimensions. Contenu g\u00e9n\u00e9rique bas\u00e9 sur une matrice \u00e9v\u00e9nement/crise.",
      createdAt: '2026-02-08T00:00:00.000Z',
      updatedAt: '2026-02-08T00:00:00.000Z',
      levels: [
        { level: 1, label: 'Insignifiant', band: 'low', responsePriority: 5, responseCapability: "capacit\u00e9\nde gestion courante de l\u2019organisation/\nchef de projet / programme" },
        { level: 2, label: 'Mineur', band: 'low', responsePriority: 4, responseCapability: "capacit\u00e9\nde gestion courante de l\u2019organisation/\nchef de projet / programme" },
        { level: 3, label: 'Significatif', band: 'medium', responsePriority: 3, responseCapability: "capacit\u00e9\nde gestion courante de l\u2019organisation/ le comit\u00e9 de pilotage projet/programme" },
        { level: 4, label: 'Grave', band: 'medium', responsePriority: 2, responseCapability: "Capacit\u00e9 de gestion courante de l\u2019organisation/\ngestion des incidents IT majeurs/gestion des probl\u00e8mes IT majeurs/ le comit\u00e9 op\u00e9rationnel et la direction -> constitution d\u2019une task force ad hoc qui rapporte \u00e0 la direction et aux b\u00e9n\u00e9ficiaires impact\u00e9s" },
        { level: 5, label: 'Critique', band: 'high', responsePriority: 1, responseCapability: "Les capacit\u00e9s gestion courante de l\u2019organisation sont d\u00e9pass\u00e9es -> d\u00e9clenchement gestion de crise (constitution cellule de crise en plus de task forces qui lui rapportent)" }
      ],
      dimensions: [
        { id: 'POL_REPUTATION', label: 'Politique / R\u00e9putation (PESTEL)', sourceLabel: 'Politique (PESTEL) / R\u00e9putation', order: 1 },
        { id: 'ECO_FINANCE', label: '\u00c9conomique / Moyens (PESTEL, 5M)', sourceLabel: 'Economique (PESTEL) / Moyens (5M)', order: 2 },
        { id: 'SOC_PERSONNES', label: 'Sociologique / Personnes (PESTEL, 5M, RGPD)', sourceLabel: "Sociologique (PESTEL) / Main d\u2019\u0153uvre (5M) / Personnes (RGPD)", order: 3 },
        { id: 'TECH_SERVICES', label: 'Technologique / Services & Mat\u00e9riel (PESTEL, POTI, 5M)', sourceLabel: 'Technologique / services (PESTEL & POTI) / mat\u00e9riel (5M)', order: 4 },
        { id: 'ENV_MILIEU', label: 'Environnemental / Milieu (PESTEL, 5M)', sourceLabel: 'Environnemental (PESTEL) / Milieu (5M)', order: 5 },
        { id: 'LEGAL_REG', label: 'L\u00e9gal & R\u00e9glementaire / M\u00e9thodes (PESTEL, 5M)', sourceLabel: 'L\u00e9gal & r\u00e9glementaire (PESTEL) / M\u00e9thodes (5M)', order: 6 },
        { id: 'ORG_PROCESS', label: 'Organisation / Processus (POTI)', sourceLabel: 'Organisation / processus (POTI)', order: 7 },
        { id: 'INFO_CONF', label: 'Information: Confidentialit\u00e9 (POTI)', sourceLabel: 'Information (POTI): Confidentialit\u00e9', order: 8 },
        { id: 'INFO_INTEG', label: 'Information: Int\u00e9grit\u00e9 (POTI)', sourceLabel: 'Information (POTI): Int\u00e9grit\u00e9', order: 9 },
        { id: 'INFO_AVAIL', label: 'Information: Disponibilit\u00e9 (POTI)', sourceLabel: 'Information (POTI): Disponibilit\u00e9', order: 10 }
      ],
      cells: {
        POL_REPUTATION: {
          '1': "La r\u00e9putation ou l\u2019image de l\u2019organisation ou de ses b\u00e9n\u00e9ficiaires n\u2019est pas engag\u00e9e.",
          '2': "Diff\u00e9rentes critiques non r\u00e9currentes sont remont\u00e9es. La situation est le r\u00e9sultat d\u2019un fait mineur ou d\u2019un fait divers.\nImpact sur l\u2019image/visibilit\u00e9: interne",
          '3': "L\u2019organisation est concern\u00e9e de mani\u00e8re temporaire par une atteinte \u00e0 sa r\u00e9putation. L\u2019image de l\u2019organisme ou de ses b\u00e9n\u00e9ficiaires n\u2019est pas impact\u00e9e sur le long terme.\nImpact sur l\u2019image/visibilit\u00e9: local",
          '4': "Critiques s\u00e9rieuses et r\u00e9p\u00e9t\u00e9es dans les m\u00e9dias. La situation n\u2019est pas temporaire et un impact \u00e0 plus long terme est possible pour l\u2019image de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires. la direction et les instances de gouvernance sont impact\u00e9s.\nImpact sur l\u2019image/visibilit\u00e9: m\u00e9dia",
          '5': "L\u2019alt\u00e9ration de l\u2019image de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires est d\u00e9finitive. L\u2019organisation restera longuement touch\u00e9e. Une crise est engag\u00e9e. les autorit\u00e9s / instances de tutelle sont impact\u00e9s.\nImpact sur l\u2019image/visibilit\u00e9: m\u00e9dia"
        },
        ECO_FINANCE: {
          '1': "pas d\u2019impact financier\n\nD\u00e9passement insignifiant du budget du projet/programme",
          '2': "impact financier\nfaible\n\nperte financiere li\u00e9e \u00e0 des tarifs pr\u00e9f\u00e9rentiels dont on ne peut b\u00e9n\u00e9ficier compte-tenu des d\u00e9lais courts\ndouble paiement de factures\n\nD\u00e9passement budg\u00e9taire de 0,5% du budget du projet/programme",
          '3': "impact financier\nmoyen\n\namende et retard de paiement\n\nperte financiere inf\u00e9rieure \u00e0 20 000 euros\n\nPlus de 5% de d\u00e9passement budg\u00e9taire du budget du projet/programme",
          '4': "impact financier\nimportant\n\nd\u00e9passement de la consommation budg\u00e9taire avec impact sur la continuit\u00e9 business\n\nperte financiere sup\u00e9rieure \u00e0 20 000 euros\n\nPlus de 10% de d\u00e9passement budg\u00e9taire du budget du projet/programme",
          '5': "impact financier\ntr\u00e8s important\n\nla position de tr\u00e9sorerie ne permet pas de couvrir au-del\u00e0 d\u2019un mois de salaires du personnel, incapacit\u00e9 de payer les fournisseurs\nd\u00e9tournement de fonds de l\u2019organisation"
        },
        SOC_PERSONNES: {
          '1': "Les personnes concern\u00e9es ne sont pas impact\u00e9es.\nNear-miss/incident/presque-accident: pas de dommage humain",
          '2': "Les personnes concern\u00e9es pourraient connaitre quelques d\u00e9sagr\u00e9ments, qu\u2019elles surmonteront sans difficult\u00e9. Exemples : perte de temps, simple contrariet\u00e9, agacement, \u00e9nervement.\nincapacit\u00e9 temporaire de travail inf\u00e9rieure \u00e0 3 jours",
          '3': "Les personnes concern\u00e9es pourraient connaitre des d\u00e9sagr\u00e9ments significatifs, qu\u2019elles pourront surmonter malgr\u00e9 quelques difficult\u00e9s. Exemples : frais suppl\u00e9mentaires, refus d\u2019acc\u00e8s \u00e0 des services/prestations, peur, incompr\u00e9hension, stress, sentiment d\u2019atteinte \u00e0 la vie priv\u00e9e.\nincapacit\u00e9 temporaire de travail sup\u00e9rieure \u00e0 3 jours",
          '4': "Les personnes concern\u00e9es pourraient connaitre des cons\u00e9quences significatives, qu\u2019elles devraient pouvoir surmonter, mais avec de s\u00e9rieuses difficult\u00e9s. Exemples : difficult\u00e9s financi\u00e8res non temporaires, d\u00e9gradation de biens, perte d\u2019emploi, \u00e9tat de sant\u00e9, assignation en justice, harc\u00e8lement ou chantage.\n\nD\u00e9c\u00e8s d\u2019un membre du personnel dans le cadre priv\u00e9; accident du travail entrainant une incapacit\u00e9 permanente du membre du personnel",
          '5': "Les personnes concern\u00e9es pourraient connaitre des cons\u00e9quences significatives, voire irr\u00e9m\u00e9diables, qu\u2019elles pourraient ne pas surmonter. Exemples : p\u00e9ril financier, affection physique ou psychologique de longue dur\u00e9e ou permanente, d\u00e9c\u00e8s, perte d\u2019acc\u00e8s \u00e0 une infrastructure vitale, sanctions p\u00e9nales.\n\nAccident de travail: d\u00e9c\u00e8s du membre du personnel sur les lieux du travail/sur le chemin du travail"
        },
        TECH_SERVICES: {
          '1': "incident IT sans impact sur le business\n\nmat\u00e9riel: vol/perte d\u2019asset sans impact sur la continuit\u00e9 de service et facilement rempla\u00e7able (ex: perte d\u2019un pc rempla\u00e7able rapidement avec stock disponible)\n\nQualit\u00e9 produit : anomalie mineure permettant de travailler sur le logiciel, sans impact majeur.Ex : faute d\u2019orthographe, d\u00e9faut de couleur.",
          '2': "incident IT 1 utilisateur\nde 1 \u00e0 10 utilisateurs sont impact\u00e9s avec workaround\n\nmat\u00e9riel: vol/perte d\u2019asset de faible valeur, facilement rempla\u00e7able et avec peu d\u2019impact sur la continuit\u00e9 de service (en stock)\n\nQualit\u00e9 produit : anomalie moyenne permettant de travailler sur le logiciel, qui d\u00e9grade la fonctionnalit\u00e9 sans impact majeur. Ex : un contr\u00f4le erron\u00e9 d\u2019un champ courriel.",
          '3': "incident IT Une application est impact\u00e9e avec workaround\nplus de 10 utilisateurs sont impact\u00e9s avec workaround\nde 1 \u00e0 10 utilisateurs sont impact\u00e9s sans workaround\nun VIP est impact\u00e9\n\nmat\u00e9riel: vol/perte d\u2019asset impliquant un d\u00e9lais de remplacement et impactant la continuit\u00e9 de service\n\nQualit\u00e9 produit : anomalie majeure qui d\u00e9grade l\u2019utilisation d\u2019une fonction de l\u2019application et pour laquelle il existe une solution de contournement technique ou organsationnelle \u2013 anomalie ne permettant l\u2019exploitation du logiciel qu\u2019en mode d\u00e9grad\u00e9, non viable sur le long terme. Ex : un bouton \u00ab annuler \u00bb effectue un envoi.",
          '4': "incident IT de s\u00e9curit\u00e9\nUn site complet est impact\u00e9\nplus de 10 utilisateurs sont impact\u00e9s sans workaround\n\nincident IT Une application est impact\u00e9e sans workaround\nmat\u00e9riel: vol/perte/vandalisme/destruction totale d\u2019 asset critique rempla\u00e7able avec impact sur la continuit\u00e9 de service ( en stock et/ou et supplychain d\u00e9lais raisonnables,...)\n\nQualit\u00e9 produit : anomalie bloquante qui conduit \u00e0 rendre inop\u00e9rante toute une application, ou qui bloque l\u2019utilisation ou provoque un r\u00e9sultat erron\u00e9 pour une fonction de l\u2019application et pour laquelle il ne peut exister une solution de contournement technique ou organisationnelle. Ex :l\u2019envoi de donn\u00e9es donne un message d\u2019erreur.",
          '5': "incident de s\u00e9curit\u00e9\nmat\u00e9riel: vol/perte/vandalisme/destruction totale d\u2019 asset critique difficilement rempla\u00e7able (ex:serveur) avec fort impact sur la continuit\u00e9 de service ( pas en stock et supplychain d\u00e9lais importants,...)"
        },
        ENV_MILIEU: {
          '1': "Near miss/incident/presque-accident:\npas de dommage pour l\u2019environnement de travail",
          '2': "environnement mat\u00e9riel facilement rempla\u00e7able",
          '3': "environnement mat\u00e9riel difficilement rempla\u00e7able",
          '4': "Destruction partielle de\nl\u2019environnement de travail dont certaines zones sont encore accessibles",
          '5': "Accident de travail:\ndestruction totale de l\u2019environnement de travail ; environnement de travail rendu totalement inaccessible"
        },
        LEGAL_REG: {
          '1': "pas d\u2019impact r\u00e9glementaire\naux l\u00e9gislations sp\u00e9cifiques \u00e0 la protection des donn\u00e9es\nPas ou peu d\u2019impact juridique",
          '2': "pas d\u2019impact r\u00e9glementaire\naux l\u00e9gislations sp\u00e9cifiques \u00e0 la protection des donn\u00e9es\nimpact juridique: Revendication d\u2019un tiers",
          '3': "blame possible\nformul\u00e9 par l\u2019autorit\u00e9 de protection des donn\u00e9es (publication de la d\u00e9cision)\nimpact juridique:Autorit\u00e9 impliqu\u00e9e",
          '4': "Mesures correctrices :\navertissement, r\u00e9primande, rappel \u00e0 l\u2019ordre, injonction, interdiction temporaire du traitement, astreintes. - Poursuites civiles : suspension du traitement (action en cessation), dommages et int\u00e9r\u00eats (action en r\u00e9paration). - Sanctions p\u00e9nales : amendes de 2.000 EUR \u00e0 120.000 EUR (pour certains types d\u2019infractions seulement, en ce compris les transferts de donn\u00e9es hors de l\u2019EEE non-conformes \u00e0 l\u2019arr\u00eat Schrems II).\nImpact juridique: Annulation ou nullit\u00e9 d\u2019un acte",
          '5': "Mesures correctrices :\navertissement, r\u00e9primande, rappel \u00e0 l\u2019ordre, injonction, interdiction d\u00e9finitive du traitement, astreintes. - Poursuites civiles : suspension du traitement (action en cessation), dommages et int\u00e9r\u00eats (action en r\u00e9paration). - Sanctions p\u00e9nales : amendes de 2.000 EUR \u00e0 120.000 EUR (pour certains types d\u2019infractions seulement, en ce compris les transferts de donn\u00e9es hors de l\u2019EEE non-conformes \u00e0 l\u2019arr\u00eat Schrems II).\nImpact juridique:sanction p\u00e9nale"
        },
        ORG_PROCESS: {
          '1': "L\u2019organisation et ses b\u00e9n\u00e9ficiaires ne sont pas perturb\u00e9s dans leurs activit\u00e9s. La production n\u2019est pas touch\u00e9e.\nProjet/programme: D\u00e9calage d\u2019activit\u00e9s hors chemin critique, Nice-to-have non r\u00e9alis\u00e9",
          '2': "L\u2019organisation est l\u00e9g\u00e8rement perturb\u00e9e mais il n\u2019y a pas d\u2019impact sur les activit\u00e9s ou la production de l\u2019organisme pour ses b\u00e9n\u00e9ficiaires.\n\nObjectifs du processus de l\u2019organisation partiellement atteints; pas d\u2019impact sur d\u2019autres processus de l\u2019organisation; impact op\u00e9rationnel porte \u00e0 cons\u00e9quence mais reste tol\u00e9rable\n\nprojet/programme: D\u00e9calage d\u2019activit\u00e9s sur chemin critique,Must-Have non r\u00e9alis\u00e9",
          '3': "Probl\u00e8mes affectant le fonctionnement interne et engendrant du retard et un impact limit\u00e9 sur la production ou les activit\u00e9s de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires.\n\nobjectifs du processus de l\u2019organisation moyennement atteints; impact sur un autre processus de l\u2019organisation; impact op\u00e9rationnel: ne peut etre tol\u00e9r\u00e9 qu\u2019\u00e0 titre provisoire mais doit \u00eatre trait\u00e9 \u00e0 terme\n\nprojet/programme: Non respect d\u2019un jalon interm\u00e9diaire,Pan complet non r\u00e9alis\u00e9",
          '4': "Paralysie de certains processus de l\u2019organisation affectant les employ\u00e9s internes et les b\u00e9n\u00e9ficiaires de l\u2019organisation. La production et les activit\u00e9s sont fortement perturb\u00e9es pendant plusieurs jours.\n\nObjectifs du processus de l\u2019organisation faiblement atteints; impact sur plusieurs autres processus de l\u2019organisation ou sur le processus de management; impact op\u00e9rationnel: ne met pas l\u2019entit\u00e9 en p\u00e9rile mais reste tr\u00e8s grave et doit imp\u00e9rativement \u00eatre trait\u00e9\n\nprojet/programme:Non respect de l\u2019engagement contractuel/l\u00e9gal,R\u00e9vision int\u00e9grale du scope",
          '5': "La production de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires est \u00e0 l\u2019arr\u00eat et la reprise de l\u2019activit\u00e9 n\u2019est pas garantie dans plus d\u2019une ou plusieurs semaines.\n\nObjectifs du processus non atteints; impact sur l\u2019ensemble des processus de l\u2019organisation; impact op\u00e9rationnel: mets la survie de l\u2019entit\u00e9 en cause"
        },
        INFO_CONF: {
          '1': "La confidentialit\u00e9 n\u2019est pas directement impact\u00e9e et ne constitue pas un \u00e9l\u00e9ment important.",
          '2': "Impact faible ou n\u00e9gligeable mais la perte de confidentialit\u00e9 est d\u00e9favorable pour l\u2019organisation et/ou pour ses b\u00e9n\u00e9ficiaires.",
          '3': "Impact significatif mais limit\u00e9 \u00e0 un certain p\u00e9rim\u00e8tre sur la confidentialit\u00e9 d\u2019une certaine quantit\u00e9 de donn\u00e9es appartenant \u00e0 l\u2019organisation et/ou \u00e0 ses b\u00e9n\u00e9ficiaires.",
          '4': "Impact grave sur la confidentialit\u00e9 conduisant \u00e0 une perturbation cons\u00e9quente des activit\u00e9s de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires.",
          '5': "Impact critique sur la confidentialit\u00e9 conduisant \u00e0 une perturbation d\u00e9finitive des activit\u00e9s de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires."
        },
        INFO_INTEG: {
          '1': "L\u2019int\u00e9grit\u00e9 n\u2019est pas directement impact\u00e9e et ne constitue pas un \u00e9l\u00e9ment important.",
          '2': "Impact faible ou n\u00e9gligeable mais la perte d\u2019int\u00e9grit\u00e9 est d\u00e9favorable pour l\u2019organisation et/ou pour ses b\u00e9n\u00e9ficiaires.",
          '3': "Impact significatif mais limit\u00e9 \u00e0 un certain p\u00e9rim\u00e8tre sur l\u2019int\u00e9grit\u00e9 d\u2019une certaine quantit\u00e9 de donn\u00e9es appartenant \u00e0 l\u2019organisation ou \u00e0 ses b\u00e9n\u00e9ficiaires.",
          '4': "Impact grave sur l\u2019int\u00e9grit\u00e9 conduisant \u00e0 une perturbation cons\u00e9quente des activit\u00e9s de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires.",
          '5': "Impact critique sur l\u2019int\u00e9grit\u00e9 conduisant \u00e0 une perturbation d\u00e9finitive des activit\u00e9s de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires."
        },
        INFO_AVAIL: {
          '1': "La disponibilit\u00e9 n\u2019est pas directement impact\u00e9e et ne constitue pas un \u00e9l\u00e9ment important.",
          '2': "Impact faible ou n\u00e9gligeable mais la perte de disponibilit\u00e9 est d\u00e9favorable pour l\u2019organisation et/ou pour ses b\u00e9n\u00e9ficiaires.",
          '3': "Impact significatif mais limit\u00e9 \u00e0 un certain p\u00e9rim\u00e8tre sur la disponibilit\u00e9 d\u2019une certaine quantit\u00e9 de donn\u00e9es appartenant \u00e0 l\u2019organisation ou \u00e0 ses b\u00e9n\u00e9ficiaires.",
          '4': "Impact grave sur la disponibilit\u00e9 conduisant \u00e0 une perturbation cons\u00e9quente des activit\u00e9s de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires.",
          '5': "Impact critique sur la disponibilit\u00e9 conduisant \u00e0 une perturbation d\u00e9finitive des activit\u00e9s de l\u2019organisation et/ou de ses b\u00e9n\u00e9ficiaires."
        }
      },
      meta: {
        calculation: {
          defaultAggregation: 'max',
          notes: "Pour calculer un impact global \u00e0 partir des dimensions, la r\u00e8gle par d\u00e9faut est le max des niveaux s\u00e9lectionn\u00e9s."
        }
      }
    };
  }

  // ============================================================
  // SEED DATA — NIST SP 800-30 (5 niveaux, 6 dimensions)
  // ============================================================
  function createNistMatrix() {
    return {
      id: 'imx-nist-800-30-v1',
      name: "NIST SP 800-30 \u2014 Risk Impact Assessment",
      description: "Matrice d'impact bas\u00e9e sur le NIST Special Publication 800-30 Rev. 1 (Guide for Conducting Risk Assessments). R\u00e9f\u00e9rence internationale majeure utilis\u00e9e par les agences f\u00e9d\u00e9rales am\u00e9ricaines et adopt\u00e9e mondialement. 5 niveaux de s\u00e9v\u00e9rit\u00e9, 6 dimensions d'\u00e9valuation.",
      createdAt: '2026-03-06T00:00:00.000Z',
      updatedAt: '2026-03-06T00:00:00.000Z',
      levels: [
        { level: 1, label: 'Very Low', band: 'low', responsePriority: 5, responseCapability: "Gestion courante, aucune escalade requise." },
        { level: 2, label: 'Low', band: 'low', responsePriority: 4, responseCapability: "Gestion au niveau op\u00e9rationnel, notification au management de proximit\u00e9." },
        { level: 3, label: 'Moderate', band: 'medium', responsePriority: 3, responseCapability: "Escalade au management interm\u00e9diaire, activation des proc\u00e9dures de r\u00e9ponse." },
        // FIX-AUDIT-B1 : bandes fidèles aux libellés NIST — depuis B1 la bande high/very-high
        // définit le plancher « élevé » de l'échelle CIA ; un niveau nommé « High » qui ne
        // comptait pas comme élevé (et s'imprimait orange) était incohérent.
        { level: 4, label: 'High', band: 'high', responsePriority: 2, responseCapability: "Escalade \u00e0 la direction, mobilisation de ressources d\u00e9di\u00e9es, task force si n\u00e9cessaire." },
        { level: 5, label: 'Very High', band: 'very-high', responsePriority: 1, responseCapability: "Escalade imm\u00e9diate \u00e0 la direction g\u00e9n\u00e9rale, activation du plan de gestion de crise." }
      ],
      dimensions: [
        { id: 'MISSION_BUSINESS', label: 'Mission / Business Impact', sourceLabel: "Impact sur les missions et objectifs m\u00e9tier de l'organisation", order: 1 },
        { id: 'DATA_CONFIDENTIALITY', label: 'Data Confidentiality', sourceLabel: "Perte de confidentialit\u00e9 des donn\u00e9es et informations", order: 2 },
        { id: 'DATA_INTEGRITY', label: 'Data Integrity', sourceLabel: "Alt\u00e9ration ou corruption des donn\u00e9es et informations", order: 3 },
        { id: 'SYSTEM_AVAILABILITY', label: 'System Availability', sourceLabel: "Interruption des syst\u00e8mes et services critiques", order: 4 },
        { id: 'FINANCIAL_LOSS', label: 'Financial Loss', sourceLabel: "Pertes financi\u00e8res directes et indirectes", order: 5 },
        { id: 'REGULATORY_LEGAL', label: 'Regulatory / Legal Compliance', sourceLabel: "Non-conformit\u00e9 r\u00e9glementaire et sanctions juridiques", order: 6 }
      ],
      cells: {
        MISSION_BUSINESS: {
          '1': "Impact n\u00e9gligeable sur les missions. Les objectifs m\u00e9tier restent pleinement atteignables sans d\u00e9gradation mesurable.",
          '2': "Impact limit\u00e9 et temporaire sur certaines missions secondaires. Les objectifs principaux restent atteignables avec des ajustements mineurs.",
          '3': "Impact significatif sur une ou plusieurs missions principales. D\u00e9gradation notable de la capacit\u00e9 \u00e0 atteindre les objectifs dans les d\u00e9lais pr\u00e9vus.",
          '4': "Impact majeur sur les missions critiques. Incapacit\u00e9 temporaire \u00e0 remplir les missions essentielles, n\u00e9cessitant une r\u00e9organisation significative.",
          '5': "Impact catastrophique rendant l'organisation incapable de remplir ses missions fondamentales. Remise en cause de la viabilit\u00e9 de l'organisation."
        },
        DATA_CONFIDENTIALITY: {
          '1': "Divulgation de donn\u00e9es publiques ou non sensibles. Aucun impact sur la vie priv\u00e9e ou les int\u00e9r\u00eats de l'organisation.",
          '2': "Divulgation limit\u00e9e de donn\u00e9es internes non critiques. Impact mineur sur la vie priv\u00e9e d'un petit nombre de personnes.",
          '3': "Divulgation de donn\u00e9es confidentielles affectant un p\u00e9rim\u00e8tre limit\u00e9. Risque de pr\u00e9judice pour les personnes concern\u00e9es (donn\u00e9es personnelles, secrets commerciaux).",
          '4': "Divulgation massive de donn\u00e9es hautement confidentielles (donn\u00e9es sensibles, secrets industriels, donn\u00e9es de sant\u00e9). Pr\u00e9judice grave pour de nombreuses personnes.",
          '5': "Compromission totale de donn\u00e9es classifi\u00e9es ou critiques pour la s\u00e9curit\u00e9 nationale. Cons\u00e9quences irr\u00e9versibles pour les personnes et l'organisation."
        },
        DATA_INTEGRITY: {
          '1': "Alt\u00e9ration n\u00e9gligeable de donn\u00e9es non critiques, facilement d\u00e9tectable et corrigeable. Aucune d\u00e9cision m\u00e9tier impact\u00e9e.",
          '2': "Alt\u00e9ration limit\u00e9e de donn\u00e9es op\u00e9rationnelles. Correction possible sans impact majeur sur les processus m\u00e9tier.",
          '3': "Alt\u00e9ration significative de donn\u00e9es m\u00e9tier importantes. Risque de d\u00e9cisions erron\u00e9es et de perturbation des processus pendant plusieurs jours.",
          '4': "Corruption massive de donn\u00e9es critiques (financi\u00e8res, m\u00e9dicales, contractuelles). Impact grave sur la fiabilit\u00e9 des syst\u00e8mes et la confiance des parties prenantes.",
          '5': "Destruction ou corruption irr\u00e9versible de donn\u00e9es vitales. Perte totale de confiance dans les syst\u00e8mes d'information. Impossibilit\u00e9 de reconstituer les donn\u00e9es."
        },
        SYSTEM_AVAILABILITY: {
          '1': "Interruption br\u00e8ve (< 1h) de syst\u00e8mes non critiques. Aucun impact sur les utilisateurs ou les services m\u00e9tier.",
          '2': "Interruption de quelques heures de syst\u00e8mes secondaires. Impact limit\u00e9, contournement possible via des proc\u00e9dures alternatives.",
          '3': "Interruption prolong\u00e9e (1-3 jours) de syst\u00e8mes importants. D\u00e9gradation significative des services, impact sur les utilisateurs internes et externes.",
          '4': "Interruption majeure (> 3 jours) de syst\u00e8mes critiques. Paralysie de processus m\u00e9tier essentiels, impact financier et r\u00e9putationnel important.",
          '5': "Indisponibilit\u00e9 totale et prolong\u00e9e de l'infrastructure critique. Arr\u00eat complet des activit\u00e9s, risque de non-reprise."
        },
        FINANCIAL_LOSS: {
          '1': "Pertes financi\u00e8res n\u00e9gligeables (< 10 000 \u20ac). Aucun impact sur le budget op\u00e9rationnel.",
          '2': "Pertes financi\u00e8res limit\u00e9es (10 000 - 100 000 \u20ac). Impact absorbable dans le budget courant sans r\u00e9allocation.",
          '3': "Pertes financi\u00e8res significatives (100 000 - 1 M\u20ac). R\u00e9allocation budg\u00e9taire n\u00e9cessaire, impact sur les investissements pr\u00e9vus.",
          '4': "Pertes financi\u00e8res majeures (1 - 10 M\u20ac). Impact sur la rentabilit\u00e9 annuelle, n\u00e9cessit\u00e9 de financement externe ou de restructuration.",
          '5': "Pertes financi\u00e8res catastrophiques (> 10 M\u20ac). Menace sur la viabilit\u00e9 financi\u00e8re de l'organisation, risque de cessation de paiement."
        },
        REGULATORY_LEGAL: {
          '1': "Aucun impact r\u00e9glementaire. Conformit\u00e9 maintenue \u00e0 toutes les obligations l\u00e9gales.",
          '2': "Non-conformit\u00e9 mineure d\u00e9tect\u00e9e en interne. Correction possible avant notification aux autorit\u00e9s. Risque limit\u00e9 de sanction.",
          '3': "Non-conformit\u00e9 significative n\u00e9cessitant une notification aux autorit\u00e9s (ex: notification RGPD dans les 72h). Risque d'avertissement ou de mise en demeure.",
          '4': "Violation grave des obligations r\u00e9glementaires (NIS2, RGPD, sectorielles). Sanctions financi\u00e8res importantes (jusqu'\u00e0 2% du CA), injonctions de mise en conformit\u00e9.",
          '5': "Violation majeure avec sanctions maximales (jusqu'\u00e0 4% du CA mondial ou 20 M\u20ac RGPD / 10 M\u20ac NIS2). Poursuites p\u00e9nales possibles, interdiction temporaire d'activit\u00e9."
        }
      },
      meta: {
        calculation: {
          defaultAggregation: 'max',
          notes: "Agr\u00e9gation par maximum conform\u00e9ment aux recommandations NIST SP 800-30. L'impact global correspond au niveau le plus \u00e9lev\u00e9 parmi toutes les dimensions \u00e9valu\u00e9es."
        }
      }
    };
  }

  // ============================================================
  // SEED DATA — EBIOS RM / ISO 27005 (4 niveaux, 5 dimensions)
  // ============================================================
  function createEbiosMatrix() {
    return {
      id: 'imx-ebios-rm-v1',
      name: "EBIOS RM / ISO 27005 \u2014 Matrice d'impacts",
      description: "Matrice d'impact bas\u00e9e sur la m\u00e9thode EBIOS Risk Manager de l'ANSSI (Agence nationale de la s\u00e9curit\u00e9 des syst\u00e8mes d'information, France), align\u00e9e sur la norme ISO/CEI 27005:2022. R\u00e9f\u00e9rence europ\u00e9enne officielle pour l'analyse de risques cyber. 4 niveaux de gravit\u00e9, 5 dimensions d'\u00e9valuation.",
      createdAt: '2026-03-06T00:00:00.000Z',
      updatedAt: '2026-03-06T00:00:00.000Z',
      levels: [
        { level: 1, label: 'Mineure', band: 'low', responsePriority: 4, responseCapability: "L'organisation peut g\u00e9rer cet impact dans le cadre de ses activit\u00e9s courantes." },
        { level: 2, label: 'Significative', band: 'low', responsePriority: 3, responseCapability: "L'organisation subit un pr\u00e9judice limit\u00e9 mais surmont\u00e9 sans difficult\u00e9 majeure." },
        { level: 3, label: 'Grave', band: 'medium', responsePriority: 2, responseCapability: "L'organisation subit un pr\u00e9judice grave n\u00e9cessitant une mobilisation importante." },
        { level: 4, label: 'Critique', band: 'high', responsePriority: 1, responseCapability: "L'organisation ne peut pas surmonter ce pr\u00e9judice. Sa survie est menac\u00e9e." }
      ],
      dimensions: [
        { id: 'IMPACT_MISSIONS', label: 'Impacts sur les missions', sourceLabel: "Perturbation des activit\u00e9s op\u00e9rationnelles et des missions de l'organisation", order: 1 },
        { id: 'IMPACT_HUMAINS', label: 'Impacts humains', sourceLabel: "Cons\u00e9quences sur la s\u00e9curit\u00e9, la sant\u00e9 et les conditions des personnes", order: 2 },
        { id: 'IMPACT_FINANCIERS', label: 'Impacts financiers', sourceLabel: "Co\u00fbts directs, amendes, pertes de revenus et co\u00fbts de rem\u00e9diation", order: 3 },
        { id: 'IMPACT_JURIDIQUES', label: 'Impacts juridiques', sourceLabel: "Non-conformit\u00e9 l\u00e9gale, sanctions r\u00e9glementaires et litiges", order: 4 },
        { id: 'IMPACT_IMAGE', label: "Impacts sur l'image", sourceLabel: "Atteinte \u00e0 la r\u00e9putation et \u00e0 la confiance des parties prenantes", order: 5 }
      ],
      cells: {
        IMPACT_MISSIONS: {
          '1': "Perturbation mineure n'affectant pas les missions principales. Retards n\u00e9gligeables, facilement rattrapables.",
          '2': "D\u00e9gradation temporaire de certaines missions secondaires. Les missions principales sont maintenues mais avec une efficacit\u00e9 r\u00e9duite pendant quelques jours.",
          '3': "Perturbation grave des missions principales pendant une dur\u00e9e prolong\u00e9e. Incapacit\u00e9 partielle \u00e0 d\u00e9livrer les services essentiels aux b\u00e9n\u00e9ficiaires.",
          '4': "Arr\u00eat total ou quasi-total des missions de l'organisation. Incapacit\u00e9 durable \u00e0 assurer la continuit\u00e9 des services. Mise en p\u00e9ril de la p\u00e9rennit\u00e9 de l'organisation."
        },
        IMPACT_HUMAINS: {
          '1': "Aucun impact sur les personnes. D\u00e9sagr\u00e9ments mineurs facilement surmontables (perte de temps, contrari\u00e9t\u00e9).",
          '2': "Impacts limit\u00e9s sur les personnes : stress, inconfort, difficult\u00e9s administratives temporaires. Pas d'atteinte \u00e0 l'int\u00e9grit\u00e9 physique.",
          '3': "Impacts graves sur les personnes : atteinte \u00e0 la vie priv\u00e9e, pr\u00e9judice financier personnel, d\u00e9gradation des conditions de travail, risques psychosociaux significatifs.",
          '4': "Atteintes irr\u00e9versibles aux personnes : mise en danger de la vie, handicap permanent, cons\u00e9quences psychologiques durables, perte d'autonomie financi\u00e8re."
        },
        IMPACT_FINANCIERS: {
          '1': "Co\u00fbts n\u00e9gligeables absorb\u00e9s dans le budget de fonctionnement courant. Aucune r\u00e9allocation budg\u00e9taire n\u00e9cessaire.",
          '2': "Co\u00fbts limit\u00e9s n\u00e9cessitant une r\u00e9allocation budg\u00e9taire interne. Impact sur les marges mais pas sur la viabilit\u00e9. Amendes mineures possibles.",
          '3': "Co\u00fbts importants : pertes de revenus significatives, amendes substantielles, co\u00fbts de rem\u00e9diation \u00e9lev\u00e9s. Impact sur les r\u00e9sultats annuels et les investissements.",
          '4': "Co\u00fbts catastrophiques mena\u00e7ant la viabilit\u00e9 financi\u00e8re : amendes maximales (% du CA), pertes de contrats majeurs, co\u00fbts de reconstruction, cessation de paiement possible."
        },
        IMPACT_JURIDIQUES: {
          '1': "Aucune cons\u00e9quence juridique. L'organisation reste conforme \u00e0 l'ensemble de ses obligations l\u00e9gales et r\u00e9glementaires.",
          '2': "Non-conformit\u00e9 mineure identifi\u00e9e. Risque de rappel \u00e0 l'ordre ou d'avertissement par les autorit\u00e9s comp\u00e9tentes. Mise en conformit\u00e9 rapide possible.",
          '3': "Non-conformit\u00e9 av\u00e9r\u00e9e entra\u00eenant des sanctions : amendes administratives, injonctions de mise en conformit\u00e9, actions civiles en r\u00e9paration. Atteinte \u00e0 la r\u00e9putation juridique.",
          '4': "Violations graves entra\u00eenant des sanctions maximales : amendes p\u00e9nales, poursuites judiciaires, interdiction d'activit\u00e9, mise sous tutelle. Responsabilit\u00e9 personnelle des dirigeants engag\u00e9e."
        },
        IMPACT_IMAGE: {
          '1': "Aucun impact sur l'image. L'incident reste interne et n'affecte pas la perception des parties prenantes.",
          '2': "Atteinte limit\u00e9e et temporaire \u00e0 la r\u00e9putation. Quelques critiques internes ou de partenaires proches, sans couverture m\u00e9diatique.",
          '3': "Atteinte significative \u00e0 la r\u00e9putation avec couverture m\u00e9diatique locale ou sectorielle. Perte de confiance de certains partenaires et clients. N\u00e9cessit\u00e9 d'une communication de crise.",
          '4': "Atteinte majeure et durable \u00e0 la r\u00e9putation avec couverture m\u00e9diatique nationale/internationale. Perte de confiance g\u00e9n\u00e9ralis\u00e9e des parties prenantes. Remise en cause du leadership. Impact irr\u00e9versible sur l'image de marque."
        }
      },
      meta: {
        calculation: {
          defaultAggregation: 'max',
          notes: "Conform\u00e9ment \u00e0 la m\u00e9thode EBIOS RM, la gravit\u00e9 globale correspond au niveau maximal observ\u00e9 parmi les 5 dimensions d'impact."
        }
      }
    };
  }

  // ============================================================
  // NORMALISATION
  // ============================================================
  // FIX-AUDIT-PROTO-IMX (audit v2, 2026-08-25) : `m.cells[dim.id]` et `[String(lv.level)]`
  // indexent des objets avec des clés venues d'un .analyse importé ou d'un pull serveur
  // (aucune validation PHP). Une dimension nommée `__proto__` créait une propriété sur
  // Object.prototype (visible par `in`, invisible dans Object.keys) ; un niveau nommé
  // `toString` rendait `String({a:1})` fatal — application inutilisable. On RENOMME la clé
  // au point d'entrée (en conservant la valeur) plutôt que de la refuser : une matrice
  // importée reste exploitable, sans jamais toucher au prototype.
  var _IMX_UNSAFE_KEY = /^(?:__proto__|constructor|prototype)$/;
  function _imxSafeKey(k) {
    var str = String(k == null ? '' : k);
    return _IMX_UNSAFE_KEY.test(str) ? ('imx_' + str) : str;
  }
  function _imxOwn(obj, k) { return obj && Object.prototype.hasOwnProperty.call(obj, k); }

  function normalizeImpactMatrices(raw) {
    if (!raw || typeof raw !== 'object') {
      raw = { schemaVersion: 1, activeMatrixId: null, matrices: [] };
    }
    if (!raw.schemaVersion) raw.schemaVersion = 1;
    if (!Array.isArray(raw.matrices)) raw.matrices = [];
    // Seed si aucune matrice
    if (raw.matrices.length === 0) {
      raw.matrices.push(createDefaultMatrix());
      raw.matrices.push(createNistMatrix());
      raw.matrices.push(createEbiosMatrix());
    }
    // Vérifier activeMatrixId
    if (!raw.activeMatrixId || !raw.matrices.find(function(m) { return m.id === raw.activeMatrixId; })) {
      raw.activeMatrixId = raw.matrices[0].id;
    }
    // Normaliser chaque matrice
    raw.matrices.forEach(function(m) {
      if (!m.id) m.id = _id();
      if (!m.name) m.name = 'Sans nom';
      if (!m.description) m.description = '';
      if (!m.createdAt) m.createdAt = _now();
      if (!m.updatedAt) m.updatedAt = _now();
      if (!Array.isArray(m.levels)) m.levels = [{ level: 1, label: 'Niveau 1', band: 'low', responsePriority: 1, responseCapability: '' }];
      if (!Array.isArray(m.dimensions)) m.dimensions = [];
      if (!m.cells || typeof m.cells !== 'object') m.cells = {};
      if (!m.meta) m.meta = { calculation: { defaultAggregation: 'max', notes: '' } };
      if (!m.meta.calculation) m.meta.calculation = { defaultAggregation: 'max', notes: '' };
      // FIX-AUDIT-XSS-IMX (audit v2, 2026-08-25) : INVARIANT DE TYPE au point d'entrée unique.
      // `level` et `responsePriority` d'une matrice importée n'étaient JAMAIS coercés, et
      // `level` est émis BRUT dans des attributs (data-severity=, texte de cellule) sur 7
      // sites : une matrice `.analyse` forgée exécutait du script dès l'ouverture de la
      // section, et _persistAffected la propageait à toute l'entité. Repli i+1 (et non 1)
      // pour ne pas écraser plusieurs niveaux corrompus sur le même rang.
      m.levels.forEach(function(lv, i) {
        if (!lv || typeof lv !== 'object') { m.levels[i] = lv = { label: '', band: 'low', responseCapability: '' }; }
        var n = Number(lv.level);
        lv.level = isFinite(n) ? Math.max(1, Math.round(n)) : (i + 1);
        var pr = Number(lv.responsePriority);
        lv.responsePriority = isFinite(pr) ? Math.max(1, Math.round(pr)) : 1;
      });
      // FIX-AUDIT-PROTO-IMX : neutraliser les clés dangereuses AVANT toute indexation,
      // en emportant la valeur déjà stockée sous l'ancienne clé si elle existait en propre.
      m.dimensions.forEach(function(dim) {
        if (!dim || typeof dim !== 'object') return;
        var sain = _imxSafeKey(dim.id);
        if (sain !== dim.id) {
          if (_imxOwn(m.cells, dim.id)) { m.cells[sain] = m.cells[dim.id]; delete m.cells[dim.id]; }
          dim.id = sain;
        }
      });
      // Compléter cells manquantes (indexation sûre : hasOwnProperty, jamais `in`)
      m.dimensions.forEach(function(dim) {
        if (!_imxOwn(m.cells, dim.id) || !m.cells[dim.id] || typeof m.cells[dim.id] !== 'object') m.cells[dim.id] = {};
        m.levels.forEach(function(lv) {
          var k = String(lv.level);
          if (!_imxOwn(m.cells[dim.id], k)) m.cells[dim.id][k] = '';
        });
      });
    });
    return raw;
  }
  window.normalizeImpactMatrices = normalizeImpactMatrices;

  // ============================================================
  // STATE
  // ============================================================
  var _selectedId = null;
  // FIX-IMX-SECTION-STATE (retour client 2026-08, « very low ») : l'état ouvert/replié des
  // sections de l'éditeur doit SURVIVRE aux re-rendus. Toutes les actions structurantes
  // (Ajouter niveau/dimension, supprimer, déplacer, préréglage TLP) re-rendent le module
  // entier — la section se refermait sous le clic et l'utilisateur croyait l'ajout perdu
  // alors qu'il était bien appliqué. Défauts = état historique (grille ouverte, le reste replié).
  var _openSections = { general: false, levels: false, dimensions: false, grid: true, aggregation: false };
  function _sectionCls(name) {
    return 'imx-editor-section' + (_openSections[name] ? '' : ' imx-editor-section--collapsed');
  }
  var _boundContainer = null;

  // ============================================================
  // DATA ACCESS
  // ============================================================
  // === CSI-ME === FIX-IMPOSED-PUSH (retour client SPF 2026-08-17) : vrai si le périmètre
  // courant est une SOUS-ENTITÉ (compte dédié ou admin transverse basculé). Les matrices
  // d'impact y sont IMPOSÉES par l'entité principale (§8.1) : le serveur strippe toute
  // écriture de cette clé → module en consultation seule, aucun seed, aucune persistance.
  function _scopedSub() {
    if (typeof window._csiScopedSubEntity === 'function') return window._csiScopedSubEntity();
    var EM = window.CSI_EntityManager;
    return !!(EM && EM.isMultiEntity && EM.isMultiEntity()
      && ((EM.isSubEntityUser && EM.isSubEntityUser()) || (EM.activeScope && EM.activeScope())));
  }

  function ensureInitialized() {
    if (!window.appData) return;
    // FIX-IMPOSED-PUSH : en sous-entité, ne JAMAIS seeder ni sauvegarder — on affiche ce que
    // le read-through du principal a servi (pull complet). Normaliser seulement un magasin
    // déjà peuplé (idempotent, sans écriture) ; un magasin absent reste absent.
    if (_scopedSub()) {
      if (window.appData.impactMatrices && Array.isArray(window.appData.impactMatrices.matrices)
          && window.appData.impactMatrices.matrices.length > 0) {
        window.appData.impactMatrices = normalizeImpactMatrices(window.appData.impactMatrices);
      }
      return;
    }
    if (!window.appData.impactMatrices) {
      window.appData.impactMatrices = normalizeImpactMatrices(null);
      // === CSI-CO === (audit co-édition 2026-08-05) ensureInitialized() est appelée à CHAQUE
      // rendu du module : le seed persistait donc depuis un chemin de LECTURE. En consultation
      // le push partait sans verrou (423, onglet gelé) ; on garde le seed en RAM — l'affichage
      // est correct — et la persistance suivra la première écriture légitime.
      var _roIM = (typeof window.isReadOnlyMode === 'function') && window.isReadOnlyMode();
      if (!_roIM) _save();
    } else {
      window.appData.impactMatrices = normalizeImpactMatrices(window.appData.impactMatrices);
    }
  }

  function getStore() {
    return window.appData && window.appData.impactMatrices ? window.appData.impactMatrices : null;
  }

  function getActiveMatrix() {
    var store = getStore();
    if (!store || !Array.isArray(store.matrices)) return null;
    return store.matrices.find(function(m) { return m.id === store.activeMatrixId; }) || store.matrices[0] || null;
  }

  function findMatrix(id) {
    var store = getStore();
    if (!store || !Array.isArray(store.matrices)) return null;
    return store.matrices.find(function(m) { return m.id === id; }) || null;
  }

  // ============================================================
  // SAVE (debounced)
  // ============================================================
  var _saveTimer = null;
  // FIX-AUDIT-JOURNAL (audit v2, 2026-08-25) : la categorie `impactMatrices` etait declaree
  // et traduite dans AUDIT_CAT_KEYS sans le moindre emetteur. Best-effort : le journal ne
  // doit JAMAIS faire echouer une action metier. Traces posees dans les MUTATIONS seulement
  // (jamais dans ensureInitialized, atteignable au rendu = ecriture depuis un chemin de lecture).
  function _audit(action, target, details) {
    try {
      if (typeof window.addAuditEntry === 'function') window.addAuditEntry(action, 'impactMatrices', target, details);
    } catch (e) { /* journal indisponible : sans effet sur l'action en cours */ }
  }

  function _save() {
    if (typeof window.saveData === 'function') window.saveData();
  }
  function debouncedSave() {
    if (_saveTimer) clearTimeout(_saveTimer);
    _saveTimer = setTimeout(function() {
      _save();
    }, 400);
  }

  // ============================================================
  // CRUD
  // ============================================================
  function createMatrix() {
    if (_scopedSub()) return; // FIX-IMPOSED-PUSH : échelles imposées par l'entité principale
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var store = getStore();
    if (!store) return;
    var m = {
      id: _id(),
      name: _t('impactMatrices.newMatrixName', 'Nouvelle matrice'),
      description: '',
      createdAt: _now(),
      updatedAt: _now(),
      levels: [
        { level: 1, label: 'Faible', band: 'low', responsePriority: 3, responseCapability: '' },
        { level: 2, label: 'Moyen', band: 'medium', responsePriority: 2, responseCapability: '' },
        { level: 3, label: 'Fort', band: 'high', responsePriority: 1, responseCapability: '' }
      ],
      dimensions: [
        { id: 'DIM_1', label: 'Dimension 1', sourceLabel: '', order: 1 },
        { id: 'DIM_2', label: 'Dimension 2', sourceLabel: '', order: 2 }
      ],
      cells: {
        DIM_1: { '1': '', '2': '', '3': '' },
        DIM_2: { '1': '', '2': '', '3': '' }
      },
      meta: { calculation: { defaultAggregation: 'max', notes: '' } }
    };
    store.matrices.push(m);
    _selectedId = m.id;
    store.matrices[store.matrices.length - 1].updatedAt = _now();
    _audit('add', m.name, _t('impactMatrices.audit.created', 'Matrice d\'impact creee'));
    _save();
    renderModule();
    if (typeof window.showToast2 === 'function') {
      window.showToast2(_t('impactMatrices.toastCreated', 'Matrice cr\u00e9\u00e9e'), 'success');
    }
  }

  function duplicateMatrix(id) {
    if (_scopedSub()) return; // FIX-IMPOSED-PUSH : échelles imposées par l'entité principale
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var store = getStore();
    if (!store) return;
    var src = findMatrix(id);
    if (!src) return;
    var copy = _deepClone(src);
    copy.id = _id();
    copy.name = src.name + ' (' + _t('impactMatrices.copy', 'copie') + ')';
    copy.createdAt = _now();
    copy.updatedAt = _now();
    store.matrices.push(copy);
    _selectedId = copy.id;
    _audit('add', copy.name, _t('impactMatrices.audit.duplicated', 'Matrice dupliquee depuis') + ' : ' + src.name);
    _save();
    renderModule();
    if (typeof window.showToast2 === 'function') {
      window.showToast2(_t('impactMatrices.toastDuplicated', 'Matrice dupliqu\u00e9e'), 'success');
    }
  }

  function deleteMatrix(id) {
    if (_scopedSub()) return; // FIX-IMPOSED-PUSH : échelles imposées par l'entité principale
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var store = getStore();
    if (!store) return;
    if (store.matrices.length <= 1) {
      if (typeof window.showToast2 === 'function') {
        window.showToast2(_t('impactMatrices.toastCannotDeleteLast', 'Impossible de supprimer la derni\u00e8re matrice'), 'warning');
      }
      return;
    }
    var m = findMatrix(id);
    if (!m) return;
    if (!confirm(_t('impactMatrices.confirmDelete', 'Supprimer cette matrice ?') + '\n' + m.name)) return;
    _audit('delete', m.name, _t('impactMatrices.audit.deleted', 'Matrice d\'impact supprimee'));
    store.matrices = store.matrices.filter(function(mx) { return mx.id !== id; });
    if (store.activeMatrixId === id) {
      store.activeMatrixId = store.matrices[0].id;
    }
    if (_selectedId === id) {
      _selectedId = store.matrices[0].id;
    }
    // FIX-CASCADE-PERSIST (2026-06-29) : \u00e9viter les r\u00e9f\u00e9rences orphelines \u2014 les analyses de
    // risque qui pointaient cette matrice retombent sur la matrice active (impactMatrixId=null),
    // et l'onglet risk-analysis doit \u00eatre persist\u00e9 en mode serveur (sinon l'impactMatrixId
    // orphelin r\u00e9appara\u00eet au prochain pull).
    var _raCleaned = false;
    if (window.appData && Array.isArray(window.appData.riskAnalyses)) {
      window.appData.riskAnalyses.forEach(function(a) {
        if (a && a.impactMatrixId === id) { a.impactMatrixId = null; _raCleaned = true; }
      });
    }
    _save();
    // Persiste impactMatrices (au cas où l'onglet courant ne serait pas 'impact-matrices' :
    // saveData ne pousserait alors pas la suppression) + risk-analysis si des analyses ont été
    // nettoyées. Le helper saute l'onglet courant (déjà poussé par saveData) → pas de double push.
    if (typeof window._csiPersistAffectedTabs === 'function') {
      var _mxKeys = ['impactMatrices'];
      if (_raCleaned) _mxKeys.push('riskAnalyses');
      window._csiPersistAffectedTabs(_mxKeys);
    }
    renderModule();
    if (typeof window.showToast2 === 'function') {
      window.showToast2(_t('impactMatrices.toastDeleted', 'Matrice supprim\u00e9e'), 'success');
    }
  }

  function activateMatrix(id) {
    if (_scopedSub()) return; // FIX-IMPOSED-PUSH : échelles imposées par l'entité principale
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var store = getStore();
    if (!store) return;
    store.activeMatrixId = id;
    var _am = findMatrix(id);
    _audit('config', (_am && _am.name) || id, _t('impactMatrices.audit.activated', 'Matrice d\'impact activee'));
    _save();
    renderModule();
    if (typeof window.showToast2 === 'function') {
      window.showToast2(_t('impactMatrices.toastActivated', 'Matrice activ\u00e9e'), 'info');
    }
  }

  // ============================================================
  // ENTRY POINT
  // ============================================================
  function renderModule() {
    ensureInitialized();
    var container = document.getElementById('impactMatricesContent');
    if (!container) return;
    var store = getStore();
    var scoped = _scopedSub();
    // FIX-IMPOSED-PUSH : sous-entité sans catalogue hérité (le principal n'a pas encore
    // initialisé ses échelles) — état vide explicite, pas de seed local fantôme.
    if (scoped && (!store || !Array.isArray(store.matrices) || store.matrices.length === 0)) {
      container.textContent = '';
      container.appendChild(_imposedBannerEl());
      var emptyBox = document.createElement('div');
      emptyBox.className = 'imx-empty';
      var emptyIcon = document.createElement('i');
      emptyIcon.className = 'fas fa-table-cells';
      var emptyMsg = document.createElement('p');
      emptyMsg.textContent = _t('impactMatrices.imposedEmpty', 'Les échelles n\'ont pas encore été initialisées sur l\'entité principale.');
      emptyBox.appendChild(emptyIcon);
      emptyBox.appendChild(emptyMsg);
      container.appendChild(emptyBox);
      return;
    }
    if (!store) return;
    // Default selection
    if (!_selectedId || !findMatrix(_selectedId)) {
      _selectedId = store.activeMatrixId || (store.matrices[0] && store.matrices[0].id);
    }
    renderLayout(container, store);
    if (scoped) _applyImposedReadOnly(container);
    if (typeof window.refreshReadOnlyLocks === 'function') window.refreshReadOnlyLocks();
  }

  // FIX-IMPOSED-PUSH : bandeau + gel du module en sous-entité. Appliqué APRÈS le rendu
  // (post-traitement : une seule porte, aucun site de génération HTML à ne pas oublier) et
  // AVANT refreshReadOnlyLocks — on retire data-readonly-lock des champs qu'on fige, sinon
  // le verrou vivant les ré-activerait en mode édition.
  function _imposedBannerEl() {
    var banner = document.createElement('div');
    banner.className = 'imx-imposed-banner';
    var icon = document.createElement('i');
    icon.className = 'fas fa-lock';
    var span = document.createElement('span');
    span.textContent = _t('impactMatrices.imposedBanner', 'Échelles imposées par l\'entité principale — consultation seule dans une sous-entité.');
    banner.appendChild(icon);
    banner.appendChild(span);
    return banner;
  }
  function _applyImposedReadOnly(container) {
    container.insertBefore(_imposedBannerEl(), container.firstChild);
    // (audit du lot) data-permanent-readonly plutôt que retirer data-readonly-lock :
    // c'est le mécanisme DÉDIÉ de refreshReadOnlyLocks (« reste désactivé même en mode
    // édition ») — aucun contrat d'ordre d'appel, un futur refresh isolé respecte le gel.
    var actions = container.querySelectorAll('[data-imx-action]');
    for (var i = 0; i < actions.length; i++) {
      if (actions[i].getAttribute('data-imx-action') === 'toggleSection') continue; // navigation de lecture
      actions[i].style.display = 'none';
      actions[i].dataset.permanentReadonly = 'true';
      actions[i].disabled = true;
    }
    var fields = container.querySelectorAll('input, textarea, select');
    for (var j = 0; j < fields.length; j++) {
      fields[j].disabled = true;
      fields[j].dataset.permanentReadonly = 'true';
    }
  }
  window.renderImpactMatricesModule = renderModule;

  // ============================================================
  // RENDER LAYOUT
  // ============================================================
  function renderLayout(container, store) {
    var html = '<div class="imx-layout">';
    html += '<div class="imx-list-panel">';
    html += renderListPanel(store);
    html += '</div>';
    html += '<div class="imx-editor-panel">';
    html += renderEditorPanel(store);
    html += '</div>';
    html += '</div>';
    container.innerHTML = html;
    // Bind events only once per container to avoid listener accumulation
    if (_boundContainer !== container) {
      _boundContainer = container;
      bindEvents(container);
    }
  }

  // ============================================================
  // RENDER LIST PANEL
  // ============================================================
  function renderListPanel(store) {
    var html = '';
    html += '<div class="imx-toolbar">';
    html += '<span class="imx-toolbar__title">' + _esc(_t('impactMatrices.listTitle', 'Matrices')) + '</span>';
    html += '<button class="btn btn--primary btn--sm" data-imx-action="create" data-readonly-lock="true">';
    html += '<i class="fas fa-plus"></i> ' + _esc(_t('impactMatrices.newMatrix', 'Nouvelle'));
    html += '</button>';
    html += '</div>';

    store.matrices.forEach(function(m) {
      var isSelected = m.id === _selectedId;
      var isActive = m.id === store.activeMatrixId;
      var dateStr = m.updatedAt ? new Date(m.updatedAt).toLocaleDateString() : '';
      html += '<div class="imx-card' + (isSelected ? ' imx-card--selected' : '') + '" data-imx-select="' + _esc(m.id) + '">';
      if (isActive) {
        html += '<span class="imx-badge-active">' + _esc(_t('impactMatrices.active', 'Active')) + '</span>';
      }
      html += '<div class="imx-card__name">' + _esc(m.name) + '</div>';
      html += '<div class="imx-card__date">' + _esc(dateStr) + '</div>';
      html += '<div class="imx-card__stats">' + m.levels.length + ' ' + _esc(_t('impactMatrices.cardLevels', 'niveaux')) + ' &middot; ' + m.dimensions.length + ' ' + _esc(_t('impactMatrices.cardDimensions', 'dimensions')) + '</div>';
      html += '<div class="imx-card__colorbar">';
      m.levels.forEach(function(lv) {
        var _segColor = _safeCssColor(lv.color);
        html += '<div class="imx-card__colorbar-seg" data-severity="' + _esc(lv.level) + '"' + (_segColor ? ' style="background:' + _segColor + '"' : '') + '></div>';
      });
      html += '</div>';
      html += '<div class="imx-card__actions">';
      if (!isActive) {
        html += '<button class="btn btn--sm" data-imx-action="activate" data-imx-id="' + _esc(m.id) + '" data-readonly-lock="true" title="' + _esc(_t('impactMatrices.activate', 'Activer')) + '">';
        html += '<i class="fas fa-check-circle"></i>';
        html += '</button>';
      }
      html += '<button class="btn btn--sm" data-imx-action="duplicate" data-imx-id="' + _esc(m.id) + '" data-readonly-lock="true" title="' + _esc(_t('impactMatrices.duplicate', 'Dupliquer')) + '">';
      html += '<i class="fas fa-copy"></i>';
      html += '</button>';
      html += '<button class="btn btn--sm btn--danger" data-imx-action="delete" data-imx-id="' + _esc(m.id) + '" data-readonly-lock="true" title="' + _esc(_t('impactMatrices.delete', 'Supprimer')) + '">';
      html += '<i class="fas fa-trash"></i>';
      html += '</button>';
      html += '</div>';
      html += '</div>';
    });

    return html;
  }

  // ============================================================
  // RENDER EDITOR PANEL
  // ============================================================
  function renderEditorPanel(store) {
    var m = findMatrix(_selectedId);
    if (!m) {
      return '<div class="imx-empty"><i class="fas fa-table-cells"></i><p>' +
        _esc(_t('impactMatrices.selectOrCreate', 'S\u00e9lectionnez ou cr\u00e9ez une matrice')) + '</p></div>';
    }

    var html = '';

    // --- Name & Description ---
    html += '<div class="' + _sectionCls('general') + '" data-imx-section="general">';
    html += '<div class="imx-editor-section__title" data-imx-action="toggleSection" data-imx-section-target="general">';
    html += '<span><i class="fas fa-chevron-down imx-chevron"></i>' + _esc(_t('impactMatrices.generalInfo', 'Informations g\u00e9n\u00e9rales')) + '</span>';
    html += '</div>';
    html += '<div class="imx-editor-section__body">';
    html += '<div class="imx-field">';
    html += '<label>' + _esc(_t('impactMatrices.matrixName', 'Nom')) + '</label>';
    html += '<input type="text" data-imx-field="name" value="' + _esc(m.name) + '" data-readonly-lock="true">';
    html += '</div>';
    html += '<div class="imx-field">';
    html += '<label>' + _esc(_t('impactMatrices.matrixDescription', 'Description')) + '</label>';
    html += '<textarea data-imx-field="description" data-readonly-lock="true" rows="2">' + _esc(m.description) + '</textarea>';
    html += '</div>';
    html += '</div>';
    html += '</div>';

    // --- Levels ---
    html += '<div class="' + _sectionCls('levels') + '" data-imx-section="levels">';
    html += '<div class="imx-editor-section__title" data-imx-action="toggleSection" data-imx-section-target="levels">';
    html += '<span><i class="fas fa-chevron-down imx-chevron"></i>' + _esc(_t('impactMatrices.levelsTitle', 'Niveaux de gravit\u00e9')) + '</span>';
    html += '<span>';
    // === RA-UX (A2) === pr\u00e9r\u00e9glage TLP (blanc/vert/amber/rouge \u2014 standard FIRST).
    html += '<button class="btn btn--sm" data-imx-action="applyTlp" data-readonly-lock="true" title="' + _esc(_t('impactMatrices.tlpPresetHint', 'Appliquer les couleurs du Traffic Light Protocol (blanc, vert, amber, rouge)')) + '">';
    html += '<i class="fas fa-traffic-light"></i> ' + _esc(_t('impactMatrices.tlpPreset', 'Couleurs TLP'));
    html += '</button> ';
    html += '<button class="btn btn--primary btn--sm" data-imx-action="addLevel" data-readonly-lock="true">';
    html += '<i class="fas fa-plus"></i> ' + _esc(_t('impactMatrices.addLevel', 'Ajouter'));
    html += '</button>';
    html += '</span>';
    html += '</div>';
    html += '<div class="imx-editor-section__body">';
    html += '<table class="imx-levels-table">';
    html += '<thead><tr>';
    html += '<th>#</th>';
    html += '<th>' + _esc(_t('impactMatrices.levelColor', 'Couleur')) + '</th>';
    html += '<th>' + _esc(_t('impactMatrices.levelLabel', 'Libell\u00e9')) + '</th>';
    html += '<th>' + _esc(_t('impactMatrices.levelBand', 'Bande')) + '</th>';
    html += '<th>' + _esc(_t('impactMatrices.levelPriority', 'Priorit\u00e9')) + '</th>';
    html += '<th>' + _esc(_t('impactMatrices.levelCapability', 'Capacit\u00e9 de r\u00e9ponse')) + '</th>';
    html += '<th class="imx-col-actions"></th>';
    html += '</tr></thead><tbody>';
    m.levels.forEach(function(lv, idx) {
      html += '<tr data-severity="' + _esc(lv.level) + '">';
      var _dotColor = _safeCssColor(lv.color);
      html += '<td class="imx-level-num"><span class="imx-level-dot" data-severity="' + _esc(lv.level) + '"' + (_dotColor ? ' style="background:' + _dotColor + '"' : '') + '></span>' + _esc(lv.level) + '</td>';
      // === RA-UX (A2) === surcharge de couleur par niveau (défaut = palette historique).
      html += '<td><input type="color" class="imx-level-color" data-imx-level="' + idx + '" data-imx-lfield="color" value="' + _esc(_levelColor(lv)) + '" data-readonly-lock="true"></td>';
      html += '<td><input type="text" data-imx-level="' + idx + '" data-imx-lfield="label" value="' + _esc(lv.label) + '" data-readonly-lock="true"></td>';
      html += '<td><select data-imx-level="' + idx + '" data-imx-lfield="band" data-readonly-lock="true" title="' + _esc(_t('impactMatrices.bandHint', 'Les bandes high et very-high définissent le niveau « élevé » de la classification CIA des analyses liées (majorations de scoring).')) + '">';
      // === RA-UX (A1/B1) === bande `very-high` : FISP 2.0 / FIPS 199 distinguent High de
      // Very High. Depuis B1, la bande est FONCTIONNELLE : le premier niveau à bande
      // high/very-high définit le plancher « élevé » de l'échelle CIA des analyses liées
      // (majorations de scoring + couleur des barres). Documenté dans l'infobulle.
      ['low', 'medium', 'high', 'very-high'].forEach(function(b) {
        html += '<option value="' + b + '"' + (lv.band === b ? ' selected' : '') + '>' + b + '</option>';
      });
      html += '</select></td>';
      html += '<td><input type="number" data-imx-level="' + idx + '" data-imx-lfield="responsePriority" value="' + (lv.responsePriority || 1) + '" min="1" data-readonly-lock="true" style="width:60px"></td>';
      html += '<td><textarea data-imx-level="' + idx + '" data-imx-lfield="responseCapability" data-readonly-lock="true" rows="2">' + _esc(lv.responseCapability || '') + '</textarea></td>';
      html += '<td class="imx-col-actions">';
      if (idx > 0) {
        html += '<button class="btn btn--sm" data-imx-action="moveLevel" data-imx-dir="up" data-imx-idx="' + idx + '" data-readonly-lock="true" title="' + _esc(_t('common.moveUp', 'Monter')) + '"><i class="fas fa-arrow-up"></i></button>';
      }
      if (idx < m.levels.length - 1) {
        html += '<button class="btn btn--sm" data-imx-action="moveLevel" data-imx-dir="down" data-imx-idx="' + idx + '" data-readonly-lock="true" title="' + _esc(_t('common.moveDown', 'Descendre')) + '"><i class="fas fa-arrow-down"></i></button>';
      }
      if (m.levels.length > 1) {
        html += '<button class="btn btn--sm btn--danger" data-imx-action="removeLevel" data-imx-idx="' + idx + '" data-readonly-lock="true" title="' + _esc(_t('common.delete', 'Supprimer')) + '"><i class="fas fa-times"></i></button>';
      }
      html += '</td>';
      html += '</tr>';
    });
    html += '</tbody></table>';
    html += '</div>';
    html += '</div>';

    // --- Dimensions ---
    html += '<div class="' + _sectionCls('dimensions') + '" data-imx-section="dimensions">';
    html += '<div class="imx-editor-section__title" data-imx-action="toggleSection" data-imx-section-target="dimensions">';
    html += '<span><i class="fas fa-chevron-down imx-chevron"></i>' + _esc(_t('impactMatrices.dimensionsTitle', 'Dimensions')) + '</span>';
    html += '<button class="btn btn--primary btn--sm" data-imx-action="addDimension" data-readonly-lock="true">';
    html += '<i class="fas fa-plus"></i> ' + _esc(_t('impactMatrices.addDimension', 'Ajouter'));
    html += '</button>';
    html += '</div>';
    html += '<div class="imx-editor-section__body">';
    html += '<div class="imx-dim-list">';
    m.dimensions.forEach(function(dim, idx) {
      html += '<div class="imx-dim-chip">';
      html += '<input type="text" data-imx-dim="' + idx + '" data-imx-dfield="label" value="' + _esc(dim.label) + '" data-readonly-lock="true">';
      if (m.dimensions.length > 1) {
        html += '<span class="imx-dim-remove" data-imx-action="removeDimension" data-imx-idx="' + idx + '" data-readonly-lock="true" title="' + _esc(_t('common.delete', 'Supprimer')) + '"><i class="fas fa-times"></i></span>';
      }
      html += '</div>';
    });
    html += '</div>';
    html += '</div>';
    html += '</div>';

    // --- Impact Grid ---
    html += '<div class="' + _sectionCls('grid') + '" data-imx-section="grid">';
    html += '<div class="imx-editor-section__title" data-imx-action="toggleSection" data-imx-section-target="grid">';
    html += '<span><i class="fas fa-chevron-down imx-chevron"></i>' + _esc(_t('impactMatrices.gridTitle', 'Grille d\'impacts')) + '</span>';
    html += '</div>';
    html += '<div class="imx-editor-section__body">';
    html += '<div class="imx-grid-wrapper">';
    html += '<table class="imx-grid">';
    html += '<thead><tr>';
    html += '<th class="imx-grid__dim-header">' + _esc(_t('impactMatrices.dimension', 'Dimension')) + '</th>';
    m.levels.forEach(function(lv) {
      // === RA-UX (A2) === la couleur personnalisée prime sur le CSS data-severity.
      var _thColor = _safeCssColor(lv.color);
      var _thStyle = _thColor ? ' style="background:' + _thColor + ';color:' + _textColorFor(_thColor) + ';border-color:' + _thColor + '"' : '';
      html += '<th data-severity="' + _esc(lv.level) + '"' + _thStyle + '>' + _esc(lv.label) + ' (' + _esc(lv.level) + ')</th>';
    });
    html += '</tr></thead><tbody>';
    m.dimensions.forEach(function(dim) {
      html += '<tr>';
      html += '<td class="imx-grid__dim-label">' + _esc(dim.label) + '</td>';
      m.levels.forEach(function(lv) {
        var cellVal = (m.cells[dim.id] && m.cells[dim.id][String(lv.level)]) || '';
        html += '<td data-severity="' + _esc(lv.level) + '"><textarea data-imx-cell-dim="' + _esc(dim.id) + '" data-imx-cell-level="' + _esc(lv.level) + '" data-readonly-lock="true">' + _esc(cellVal) + '</textarea></td>';
      });
      html += '</tr>';
    });
    html += '</tbody></table>';
    html += '</div>';
    html += '</div>';
    html += '</div>';

    // --- Aggregation ---
    html += '<div class="' + _sectionCls('aggregation') + '" data-imx-section="aggregation">';
    html += '<div class="imx-editor-section__title" data-imx-action="toggleSection" data-imx-section-target="aggregation">';
    html += '<span><i class="fas fa-chevron-down imx-chevron"></i>' + _esc(_t('impactMatrices.aggregationTitle', 'M\u00e9thode d\'agr\u00e9gation')) + '</span>';
    html += '</div>';
    html += '<div class="imx-editor-section__body">';
    html += '<div class="imx-aggregation">';
    html += '<label>' + _esc(_t('impactMatrices.aggregationMethod', 'Calcul de l\'impact global')) + '</label>';
    // FIX-AUDIT-AGG (audit 2026-08-25) : le select proposait 'max+override' et 'weighted' en
    // ANGLAIS BRUT (sans _t) alors que defaultAggregation n'est lu par AUCUN chemin de calcul —
    // l'unique point de calcul (_pickerComputeImpact) fait toujours max + normalisation affine.
    // On n'affiche donc plus un réglage inerte : libellé localisé de la règle réellement
    // appliquée, et normalisation à la lecture de toute valeur héritée ≠ 'max'.
    var agg = m.meta.calculation.defaultAggregation || 'max';
    if (agg !== 'max') { agg = 'max'; m.meta.calculation.defaultAggregation = 'max'; }
    html += '<select data-imx-field="aggregation" data-readonly-lock="true">';
    html += '<option value="max" selected>' + _esc(_t('impactMatrices.aggregationMax', 'Niveau maximum parmi les dimensions évaluées')) + '</option>';
    html += '</select>';
    html += '</div>';
    html += '<div class="imx-field" style="margin-top:var(--spacing-sm,0.5rem)">';
    html += '<label>' + _esc(_t('impactMatrices.aggregationNotes', 'Notes')) + '</label>';
    html += '<textarea data-imx-field="aggregationNotes" data-readonly-lock="true" rows="2">' + _esc(m.meta.calculation.notes || '') + '</textarea>';
    html += '</div>';
    html += '</div>';
    html += '</div>';

    return html;
  }

  // ============================================================
  // EVENT BINDING
  // ============================================================
  function bindEvents(container) {
    // Delegation
    container.addEventListener('click', function(e) {
      var target = e.target.closest('[data-imx-action]') || e.target.closest('[data-imx-select]');
      if (!target) return;

      // Card selection
      if (target.dataset.imxSelect) {
        _selectedId = target.dataset.imxSelect;
        renderModule();
        return;
      }

      var action = target.dataset.imxAction;
      var id = target.dataset.imxId;
      var idx = target.dataset.imxIdx !== undefined ? parseInt(target.dataset.imxIdx, 10) : -1;
      var dir = target.dataset.imxDir;

      // FIX-IMPOSED-PUSH : sous-entité = consultation seule (seuls la sélection de carte,
      // gérée plus haut, et le pliage de section restent permis). Un clic forcé sur un
      // bouton masqué ne fait rien.
      if (action !== 'toggleSection' && _scopedSub()) return;

      switch (action) {
        case 'create': createMatrix(); break;
        case 'duplicate': duplicateMatrix(id); break;
        case 'delete': deleteMatrix(id); break;
        case 'activate': activateMatrix(id); break;
        case 'addLevel': addLevel(); break;
        case 'applyTlp': applyTlpPreset(); break;
        case 'removeLevel': removeLevel(idx); break;
        case 'moveLevel': moveLevel(idx, dir); break;
        case 'addDimension': addDimension(); break;
        case 'removeDimension': removeDimension(idx); break;
        case 'toggleSection':
          var sectionTarget = target.dataset.imxSectionTarget;
          var section = container.querySelector('[data-imx-section="' + sectionTarget + '"]');
          if (section) {
            var _nowCollapsed = section.classList.toggle('imx-editor-section--collapsed');
            _openSections[sectionTarget] = !_nowCollapsed; // FIX-IMX-SECTION-STATE
          }
          break;
      }
    });

    // Input changes (debounced)
    container.addEventListener('input', function(e) {
      if (_scopedSub()) return; // FIX-IMPOSED-PUSH : consultation seule en sous-entité
      var el = e.target;
      var m = findMatrix(_selectedId);
      if (!m) return;

      // Top-level fields
      if (el.dataset.imxField) {
        handleFieldChange(m, el);
        return;
      }

      // Level fields
      if (el.dataset.imxLevel !== undefined && el.dataset.imxLfield) {
        handleLevelChange(m, el);
        return;
      }

      // Dimension fields
      if (el.dataset.imxDim !== undefined && el.dataset.imxDfield) {
        handleDimensionChange(m, el);
        return;
      }

      // Cell fields
      if (el.dataset.imxCellDim && el.dataset.imxCellLevel) {
        handleCellChange(m, el);
        return;
      }
    });

    // Select changes
    container.addEventListener('change', function(e) {
      if (_scopedSub()) return; // FIX-IMPOSED-PUSH : consultation seule en sous-entité
      var el = e.target;
      var m = findMatrix(_selectedId);
      if (!m) return;

      if (el.dataset.imxField) {
        handleFieldChange(m, el);
        return;
      }
      if (el.dataset.imxLevel !== undefined && el.dataset.imxLfield) {
        handleLevelChange(m, el);
        return;
      }
    });
  }

  // ============================================================
  // FIELD HANDLERS
  // ============================================================
  function handleFieldChange(m, el) {
    var field = el.dataset.imxField;
    if (field === 'name') {
      m.name = el.value;
    } else if (field === 'description') {
      m.description = el.value;
    } else if (field === 'aggregation') {
      m.meta.calculation.defaultAggregation = el.value;
    } else if (field === 'aggregationNotes') {
      m.meta.calculation.notes = el.value;
    }
    m.updatedAt = _now();
    debouncedSave();
  }

  function handleLevelChange(m, el) {
    var idx = parseInt(el.dataset.imxLevel, 10);
    var field = el.dataset.imxLfield;
    if (!m.levels[idx]) return;
    if (field === 'responsePriority') {
      m.levels[idx][field] = parseInt(el.value, 10) || 1;
    } else {
      m.levels[idx][field] = el.value;
    }
    m.updatedAt = _now();
    debouncedSave();
  }

  function handleDimensionChange(m, el) {
    var idx = parseInt(el.dataset.imxDim, 10);
    var field = el.dataset.imxDfield;
    if (!m.dimensions[idx]) return;
    m.dimensions[idx][field] = el.value;
    m.updatedAt = _now();
    debouncedSave();
  }

  function handleCellChange(m, el) {
    // FIX-AUDIT-PROTO-IMX : ces deux clés reviennent d'attributs data-* — forgeables via un
    // rendu issu d'une matrice importée. On refuse explicitement au lieu d'indexer.
    var dimId = el.dataset.imxCellDim;
    var level = el.dataset.imxCellLevel;
    if (_IMX_UNSAFE_KEY.test(String(dimId)) || _IMX_UNSAFE_KEY.test(String(level))) return;
    if (!_imxOwn(m.cells, dimId) || !m.cells[dimId] || typeof m.cells[dimId] !== 'object') m.cells[dimId] = {};
    m.cells[dimId][level] = el.value;
    m.updatedAt = _now();
    debouncedSave();
  }

  // ============================================================
  // LEVEL / DIMENSION OPERATIONS
  // ============================================================
  // === RA-UX (A2) === applique la palette TLP aux niveaux de la matrice sélectionnée.
  function applyTlpPreset() {
    if (_scopedSub()) return; // échelles imposées par l'entité principale (§8.1)
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var m = findMatrix(_selectedId);
    if (!m || !Array.isArray(m.levels) || !m.levels.length) return;
    var colors = tlpColorsFor(m.levels.length);
    m.levels.forEach(function(lv, i) { lv.color = colors[i]; });
    m.updatedAt = _now();
    debouncedSave();
    renderModule();
  }

  function addLevel() {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var m = findMatrix(_selectedId);
    if (!m) return;
    // (audit 2026-07-03, BLOQUANT) le moteur de cotation des risques est une grille FIXE 5x5 :
    // au-delà de 5 niveaux, l'impact (max des niveaux sélectionnés) sort de l'échelle 1-5 et
    // la cotation retombe en vert (faux-négatif). On plafonne à 5 niveaux, aligné sur l'échelle.
    if (m.levels.length >= 5) {
      if (typeof window.showToast2 === 'function') {
        window.showToast2(_t('impactMatrices.maxLevels', 'Maximum 5 niveaux (aligné sur l’échelle de cotation des risques 1-5).'), 'warning');
      }
      return;
    }
    var newNum = m.levels.length + 1;
    // FIX-AUDIT-B1 : le nouveau SOMMET hérite de la bande high/very-high de l'ancien —
    // sinon un « Ajouter » laissait une bande high isolée en milieu d'échelle (le plancher
    // « élevé » de la classification CIA ignorerait alors le sommet, cf. _ciaHighFloor).
    var _prevTop = m.levels[m.levels.length - 1];
    var _newBand = (_prevTop && (_prevTop.band === 'high' || _prevTop.band === 'very-high')) ? _prevTop.band : 'medium';
    m.levels.push({
      level: newNum,
      label: _t('impactMatrices.newLevelLabel', 'Niveau') + ' ' + newNum,
      band: _newBand,
      responsePriority: 1,
      responseCapability: ''
    });
    // Add empty cells for new level in each dimension
    m.dimensions.forEach(function(dim) {
      if (_IMX_UNSAFE_KEY.test(String(dim.id))) return; // défense en profondeur : cf. handleCellChange
      if (!_imxOwn(m.cells, dim.id) || !m.cells[dim.id]) m.cells[dim.id] = {};
      m.cells[dim.id][String(newNum)] = '';
    });
    m.updatedAt = _now();
    _save();
    renderModule();
  }

  function removeLevel(idx) {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var m = findMatrix(_selectedId);
    if (!m || m.levels.length <= 1) return;
    var removedLevel = m.levels[idx].level;
    m.levels.splice(idx, 1);
    // Renumber levels
    m.levels.forEach(function(lv, i) { lv.level = i + 1; });
    // Rebuild cells: remove old level key, renumber
    m.dimensions.forEach(function(dim) {
      if (_IMX_UNSAFE_KEY.test(String(dim.id))) return; // défense en profondeur
      var oldCells = (_imxOwn(m.cells, dim.id) && m.cells[dim.id]) || {};
      var origKeys = Object.keys(oldCells).sort(function(a, b) { return window.SortUtils.numeric(parseInt(a), parseInt(b)); });
      origKeys = origKeys.filter(function(k) { return k !== String(removedLevel); });
      var newCells = {};
      m.levels.forEach(function(lv, i) {
        newCells[String(lv.level)] = origKeys[i] !== undefined ? (oldCells[origKeys[i]] || '') : '';
      });
      m.cells[dim.id] = newCells;
    });
    m.updatedAt = _now();
    _save();
    renderModule();
  }

  function moveLevel(idx, dir) {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var m = findMatrix(_selectedId);
    if (!m) return;
    var targetIdx = dir === 'up' ? idx - 1 : idx + 1;
    if (targetIdx < 0 || targetIdx >= m.levels.length) return;
    // Collect cell values in current level order before swap
    var cellsByPos = {};
    m.dimensions.forEach(function(dim) {
      if (_IMX_UNSAFE_KEY.test(String(dim.id))) return; // défense en profondeur
      cellsByPos[dim.id] = m.levels.map(function(lv) {
        return (_imxOwn(m.cells, dim.id) && m.cells[dim.id][String(lv.level)]) || '';
      });
    });
    // Swap level objects in the array
    var tmp = m.levels[idx];
    m.levels[idx] = m.levels[targetIdx];
    m.levels[targetIdx] = tmp;
    // Swap corresponding cell values
    m.dimensions.forEach(function(dim) {
      var arr = cellsByPos[dim.id];
      var tmpVal = arr[idx];
      arr[idx] = arr[targetIdx];
      arr[targetIdx] = tmpVal;
    });
    // Renumber levels sequentially and rebuild cell keys
    m.levels.forEach(function(lv, i) { lv.level = i + 1; });
    m.dimensions.forEach(function(dim) {
      var newCells = {};
      m.levels.forEach(function(lv, i) {
        newCells[String(lv.level)] = cellsByPos[dim.id][i];
      });
      m.cells[dim.id] = newCells;
    });
    m.updatedAt = _now();
    _save();
    renderModule();
  }

  function addDimension() {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var m = findMatrix(_selectedId);
    if (!m) return;
    var n = m.dimensions.length + 1;
    var dimId = 'DIM_' + Date.now();
    m.dimensions.push({
      id: dimId,
      label: _t('impactMatrices.newDimensionLabel', 'Dimension') + ' ' + n,
      sourceLabel: '',
      order: n
    });
    m.cells[dimId] = {};
    m.levels.forEach(function(lv) {
      m.cells[dimId][String(lv.level)] = '';
    });
    m.updatedAt = _now();
    _save();
    renderModule();
  }

  function removeDimension(idx) {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode()) return;
    var m = findMatrix(_selectedId);
    if (!m || m.dimensions.length <= 1) return;
    var dimId = m.dimensions[idx].id;
    m.dimensions.splice(idx, 1);
    delete m.cells[dimId];
    // Reorder
    m.dimensions.forEach(function(d, i) { d.order = i + 1; });
    m.updatedAt = _now();
    _save();
    renderModule();
  }

  // ============================================================
  // PUBLIC API
  // ============================================================
  window.CSI_ImpactMatrices = {
    // === RA-UX (A2) === exposés pour le picker d'impact (risk-analysis) et les tests.
    tlpColorsFor: tlpColorsFor,
    levelColor: _levelColor,
    textColorFor: _textColorFor,
    getActiveMatrix: getActiveMatrix,
    getMatrixById: findMatrix,
    listMatrices: function() {
      // (audit du lot 2026-08-17) toujours un tableau — un magasin hérité malformé
      // (read-through d'un blob legacy) ne doit jamais faire déraper l'appelant.
      var store = getStore();
      return (store && Array.isArray(store.matrices)) ? store.matrices : [];
    },
    ensureInitialized: ensureInitialized,
    normalize: normalizeImpactMatrices,
    createNistMatrix: createNistMatrix,
    createEbiosMatrix: createEbiosMatrix
  };

})();
