/**
 * Cyber-Assistant - Module Analyse de Risques
 * Analyse pragmatique alignée ISO 27005
 * Wizard 8 étapes, scoring, recommandations, export PDF
 */
(function() {
  'use strict';

  // ============================================================
  // HELPERS
  // ============================================================
  function _t(key, fallback) {
    if (typeof window.t === 'function') {
      var val = window.t(key);
      return (val && val !== key) ? val : (fallback || key);
    }
    return fallback || key;
  }
  var _esc = window.escapeHtml;
  function _id() {
    return 'ra-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
  }
  function _now() { return new Date().toISOString(); }
  function _clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
  function _deepClone(o) { try { return JSON.parse(JSON.stringify(o)); } catch(e) { void 0; return o; } }
  function _hexToRgb(hex) { hex = hex.replace('#',''); return [parseInt(hex.substr(0,2),16), parseInt(hex.substr(2,2),16), parseInt(hex.substr(4,2),16)]; }
  function _generateReportNumber() {
    var d = new Date();
    var ymd = d.toISOString().slice(0, 10).replace(/-/g, '');
    var rand;
    try { rand = Array.from(crypto.getRandomValues(new Uint8Array(2))).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('').toUpperCase(); }
    catch(e) { rand = Math.random().toString(16).slice(2, 6).toUpperCase(); }
    return 'RA-' + ymd + '-' + rand;
  }
  /** Returns current UI language: 'en' or 'fr' (default 'fr') */
  function _lang() {
    return (window.CSI_i18n && typeof window.CSI_i18n.getLanguage === 'function') ? window.CSI_i18n.getLanguage() : 'fr';
  }
  /** Pick bilingual field: returns EN value when lang=en if available, otherwise FR value */
  function _bi(obj, frField, enField) {
    return (_lang() === 'en' && obj[enField]) ? obj[enField] : obj[frField];
  }

  // ============================================================
  // RISK THRESHOLDS — Data model + CRUD + scoring
  // ============================================================
  var DEFAULT_RISK_THRESHOLD = {
    id: 'rt-default-v1',
    name: 'Standard 5×5 Matrix',
    nameFr: 'Matrice standard 5×5',
    description: 'Default thresholds — Probability × Severity',
    descriptionFr: 'Seuils par défaut — Probabilité × Gravité',
    createdAt: '2026-02-09T00:00:00.000Z',
    updatedAt: '2026-02-09T00:00:00.000Z',
    probabilityLabels: ['Unknown','Very Low','Low','Medium','High'],
    probabilityLabelsFr: ['Inconnue','Très faible','Basse','Moyenne','Haute'],
    gravityLabels: ['Insignificant','Minor','Significant','Severe','Critical'],
    gravityLabelsFr: ['Insignifiant','Mineur','Significatif','Grave','Critique'],
    levels: [
      { id: 'minimum',    label: 'Minimum',     labelFr: 'Minimum',      color: '#22c55e' },
      { id: 'faible',     label: 'Low',          labelFr: 'Faible',       color: '#84cc16' },
      { id: 'modere',     label: 'Moderate',     labelFr: 'Modéré',       color: '#f97316' },
      { id: 'eleve',      label: 'High',         labelFr: 'Élevé',        color: '#ea580c' },
      { id: 'tres_eleve', label: 'Very High',    labelFr: 'Très élevé',   color: '#ef4444' }
    ],
    cells: {
      '1_1':'minimum','1_2':'minimum','1_3':'minimum','1_4':'faible','1_5':'modere',
      '2_1':'minimum','2_2':'faible','2_3':'faible','2_4':'modere','2_5':'eleve',
      '3_1':'minimum','3_2':'faible','3_3':'modere','3_4':'eleve','3_5':'tres_eleve',
      '4_1':'faible','4_2':'modere','4_3':'eleve','4_4':'eleve','4_5':'tres_eleve',
      '5_1':'modere','5_2':'eleve','5_3':'tres_eleve','5_4':'tres_eleve','5_5':'tres_eleve'
    }
  };

  var LEVEL_TO_TRAFFIC = { minimum: 'green', faible: 'green', modere: 'yellow', eleve: 'red', tres_eleve: 'red' };

  function ensureThresholdsInit() {
    if (!window.appData.riskThresholds || !window.appData.riskThresholds.items || !window.appData.riskThresholds.items.length) {
      window.appData.riskThresholds = { schemaVersion: 1, activeId: DEFAULT_RISK_THRESHOLD.id, items: [_deepClone(DEFAULT_RISK_THRESHOLD)] };
      _save();
    }
    return window.appData.riskThresholds;
  }

  function getActiveThreshold() {
    var store = ensureThresholdsInit();
    var t = store.items.find(function(i) { return i.id === store.activeId; });
    return t || store.items[0];
  }

  function getRiskLevelForCell(p, g, threshold) {
    var t = threshold || getActiveThreshold();
    // (audit 2026-07-03, BLOQUANT) borne réelle de la grille de cotation (standard 5x5 ;
    // respecte une grille personnalisée). Une valeur HORS grille — ex. un impact issu d'une
    // matrice d'impact à >5 niveaux (max des niveaux = 6, 7…) — produisait une clé absente
    // (« 5_7 ») → fallback 'minimum' = VERT : un risque catastrophique s'affichait vert
    // (faux-négatif). On clampe p et g à [1, axisMax] : une valeur extrême mappe sur la PIRE
    // cellule atteignable, jamais sur le vert.
    var axisMax = 5;
    for (var ck in t.cells) {
      if (t.cells.hasOwnProperty(ck)) {
        var cc = ck.split('_'); var a = +cc[0], b = +cc[1];
        if (a > axisMax) axisMax = a; if (b > axisMax) axisMax = b;
      }
    }
    var cp = Math.min(axisMax, Math.max(1, Math.round(Number(p) || 3)));
    var cg = Math.min(axisMax, Math.max(1, Math.round(Number(g) || 3)));
    var levelId = t.cells[cp + '_' + cg] || 'minimum';
    var lvl = t.levels.find(function(l) { return l.id === levelId; });
    return lvl || t.levels[0];
  }

  function riskLevelFromScore(likelihood, impact) {
    var lvl = getRiskLevelForCell(likelihood || 3, impact || 3);
    return { id: lvl.id, label: _bi(lvl, 'label', 'labelEn'), labelFr: lvl.labelFr || lvl.label, labelEn: lvl.labelEn || lvl.label, color: lvl.color, trafficLight: LEVEL_TO_TRAFFIC[lvl.id] || 'yellow' };
  }

  function createThreshold(name) {
    var t = _deepClone(DEFAULT_RISK_THRESHOLD);
    t.id = 'rt-' + Date.now() + '-' + Math.random().toString(36).substr(2, 4);
    t.name = name || _t('riskAnalysis.thresholds.newName', 'Nouveau seuil');
    t.createdAt = _now(); t.updatedAt = _now();
    ensureThresholdsInit().items.push(t);
    _save();
    return t;
  }

  function duplicateThreshold(id) {
    var store = ensureThresholdsInit();
    var src = store.items.find(function(i) { return i.id === id; });
    if (!src) return null;
    var t = _deepClone(src);
    t.id = 'rt-' + Date.now() + '-' + Math.random().toString(36).substr(2, 4);
    t.name = (t.name || '') + ' (copie)';
    t.createdAt = _now(); t.updatedAt = _now();
    store.items.push(t);
    _save();
    return t;
  }

  function deleteThreshold(id) {
    var store = ensureThresholdsInit();
    if (store.items.length <= 1) return false;
    store.items = store.items.filter(function(i) { return i.id !== id; });
    if (store.activeId === id) store.activeId = store.items[0].id;
    _save();
    return true;
  }

  function activateThreshold(id) {
    var store = ensureThresholdsInit();
    if (store.items.some(function(i) { return i.id === id; })) {
      store.activeId = id;
      _save();
    }
  }

  // Public API for other modules (dashboard)
  window.CSI_RiskThresholds = {
    getActiveThreshold: getActiveThreshold,
    getRiskLevelForCell: getRiskLevelForCell,
    ensureInit: ensureThresholdsInit
  };

  // Normalize imported risk analysis (ensure all fields exist)
  function normalizeRiskAnalysis(a) {
    if (!a) return a;
    if (!a.meta) a.meta = {};
    if (!a.spot) a.spot = {};
    if (a.spot.avisOverrideLight === undefined) a.spot.avisOverrideLight = null;
    if (a.spot.avisOverrideJustification === undefined) a.spot.avisOverrideJustification = '';
    if (!a.finalRecs) a.finalRecs = { items: [] };
    if (!a.context) a.context = {};
    if (!a.cia) a.cia = {};
    if (!a.continuity) a.continuity = {};
    if (!a.architecture) a.architecture = {};
    if (!a.architecture.iam) a.architecture.iam = {};
    if (!a.risks) a.risks = [];
    if (!a.controls) a.controls = {};
    return a;
  }

  window.CSI_RiskAnalysis = window.CSI_RiskAnalysis || {};
  window.CSI_RiskAnalysis.normalizeAnalysis = normalizeRiskAnalysis;

  // ============================================================
  // SEED DATA
  // ============================================================
  const RA_DOMAINS = [
    { id: 'IAM', label: 'Identité & Accès', labelEn: 'Identity & Access', icon: 'fas fa-user-shield' },
    { id: 'SDLC', label: 'Développement Sécurisé', labelEn: 'Secure Development', icon: 'fas fa-code' },
    { id: 'VULN', label: 'Vulnérabilités', labelEn: 'Vulnerability Management', icon: 'fas fa-bug' },
    { id: 'LOG', label: 'Logs & Détection', labelEn: 'Logging & Detection', icon: 'fas fa-clipboard-list' },
    { id: 'DATA', label: 'Protection des Données', labelEn: 'Data Protection', icon: 'fas fa-database' },
    { id: 'INFRA', label: 'Infrastructure', labelEn: 'Infrastructure', icon: 'fas fa-server' },
    { id: 'NETCRYPTO', label: 'Réseau & Crypto', labelEn: 'Network & Cryptography', icon: 'fas fa-network-wired' },
    { id: 'BCP', label: 'Continuité', labelEn: 'Business Continuity', icon: 'fas fa-life-ring' }
  ];

  const RA_CONTROLS_SEED = [
    { domainId: "IAM", id: "IAM_MFA", text: "MFA activée pour les comptes privilégiés et les accès distants", textEn: "MFA enabled for privileged accounts and remote access", hint: "Admin, VPN, console cloud, CI/CD, bastion", hintEn: "Admin, VPN, cloud console, CI/CD, bastion" },
    { domainId: "IAM", id: "IAM_RBAC", text: "RBAC/least privilege appliqué (droits minimaux + séparation des rôles)", textEn: "RBAC/least privilege applied (minimal rights + role separation)", hint: "", hintEn: "" },
    { domainId: "IAM", id: "IAM_REVIEW", text: "Revue périodique des droits et suppression des comptes orphelins", textEn: "Periodic access review and orphan account removal", hint: "", hintEn: "" },
    { domainId: "SDLC", id: "SDLC_CODE", text: "Code versionné + revues de code systématiques", textEn: "Version-controlled code + systematic code reviews", hint: "PR obligatoires, règles de merge", hintEn: "Mandatory PRs, merge rules" },
    { domainId: "SDLC", id: "SDLC_SEC_TEST", text: "Tests sécurité (SAST/DAST/pentest ou équivalent) planifiés", textEn: "Security testing (SAST/DAST/pentest or equivalent) scheduled", hint: "Au minimum à chaque release majeure", hintEn: "At minimum for each major release" },
    { domainId: "SDLC", id: "SDLC_SECRETS", text: "Gestion des secrets (pas de secrets en clair dans le code; rotation)", textEn: "Secrets management (no cleartext secrets in code; rotation)", hint: "Vault/KMS/Secret manager, rotation", hintEn: "Vault/KMS/Secret manager, rotation" },
    { domainId: "VULN", id: "VULN_INVENTORY", text: "Inventaire des composants/dépendances + suivi des versions", textEn: "Component/dependency inventory + version tracking", hint: "SBOM, dépendances applicatives et images", hintEn: "SBOM, application dependencies and images" },
    { domainId: "VULN", id: "VULN_SCANS", text: "Scans vulnérabilités réguliers + traitement priorisé", textEn: "Regular vulnerability scans + prioritized remediation", hint: "CI/CD + scans runtime si applicable", hintEn: "CI/CD + runtime scans if applicable" },
    { domainId: "VULN", id: "VULN_PATCH", text: "Process patching/updates avec SLA selon criticité", textEn: "Patching/update process with SLA based on criticality", hint: "SLA CVSS élevé, suivi des exceptions", hintEn: "High CVSS SLA, exception tracking" },
    { domainId: "LOG", id: "LOG_CENTRAL", text: "Logs centralisés (app, auth, admin, sécurité) avec horodatage fiable", textEn: "Centralized logging (app, auth, admin, security) with reliable timestamps", hint: "Centralisation + NTP", hintEn: "Centralization + NTP" },
    { domainId: "LOG", id: "LOG_ALERTS", text: "Alertes/détection sur événements critiques (auth, privilèges, erreurs, WAF)", textEn: "Alerting/detection on critical events (auth, privileges, errors, WAF)", hint: "Use-cases minimum", hintEn: "Minimum use-cases" },
    { domainId: "LOG", id: "LOG_RETENTION", text: "Politique de rétention et protection d'intégrité des logs", textEn: "Log retention policy and integrity protection", hint: "Rétention adaptée, accès restreint", hintEn: "Appropriate retention, restricted access" },
    { domainId: "DATA", id: "DATA_CLASSIF", text: "Classification des données + règles d'accès associées", textEn: "Data classification + associated access rules", hint: "Données sensibles/PII, niveaux", hintEn: "Sensitive data/PII, classification levels" },
    { domainId: "DATA", id: "DATA_ENCRYPT_REST", text: "Chiffrement au repos des données sensibles (ou équivalent compensatoire)", textEn: "Encryption at rest for sensitive data (or compensating control)", hint: "DB/volumes/objets + gestion clefs", hintEn: "DB/volumes/objects + key management" },
    { domainId: "DATA", id: "DATA_RETENTION", text: "Rétention/purge/effacement (privacy) définis et appliqués", textEn: "Retention/purge/erasure (privacy) defined and enforced", hint: "Durées, purge auto, preuve", hintEn: "Durations, automated purge, evidence" },
    { domainId: "INFRA", id: "INFRA_HARDEN", text: "Hardening et configuration sécurisée (baseline) appliqués", textEn: "Hardening and secure configuration (baseline) applied", hint: "CIS benchmarks, IaC", hintEn: "CIS benchmarks, IaC" },
    { domainId: "INFRA", id: "INFRA_SEGMENT", text: "Segmentation environnements (dev/préprod/prod) et restrictions réseau", textEn: "Environment segmentation (dev/staging/prod) and network restrictions", hint: "Séparation + ACL/SG", hintEn: "Separation + ACL/SG" },
    { domainId: "INFRA", id: "INFRA_BACKUP_PROTECT", text: "Sauvegardes protégées (immutables/offline) ou mesures anti-ransomware", textEn: "Protected backups (immutable/offline) or anti-ransomware measures", hint: "Immutabilité, compte séparé", hintEn: "Immutability, separate account" },
    { domainId: "NETCRYPTO", id: "NET_TLS", text: "TLS en transit (versions modernes) + configuration sécurisée", textEn: "TLS in transit (modern versions) + secure configuration", hint: "TLS 1.2+ / 1.3, ciphers", hintEn: "TLS 1.2+ / 1.3, ciphers" },
    { domainId: "NETCRYPTO", id: "NET_WAF", text: "Protection périmétrique (WAF/API Gateway/rate-limit) pour endpoints exposés", textEn: "Perimeter protection (WAF/API Gateway/rate-limit) for exposed endpoints", hint: "WAF, rate-limit, bot protection", hintEn: "WAF, rate-limit, bot protection" },
    { domainId: "NETCRYPTO", id: "NET_KEYS", text: "Gestion des clefs (KMS/HSM/rotation/accès contrôlé)", textEn: "Key management (KMS/HSM/rotation/controlled access)", hint: "Rotation, séparation des rôles", hintEn: "Rotation, role separation" },
    { domainId: "BCP", id: "BCP_BACKUP", text: "Sauvegardes régulières alignées sur RPO", textEn: "Regular backups aligned with RPO", hint: "Fréquence conforme RPO", hintEn: "RPO-compliant frequency" },
    { domainId: "BCP", id: "BCP_RESTORE_TEST", text: "Tests de restauration et preuves de réussite", textEn: "Restore tests and proof of success", hint: "Test au moins trimestriel", hintEn: "At least quarterly testing" },
    { domainId: "BCP", id: "BCP_DR", text: "Procédure PRA/PCA alignée sur RTO (plan + responsabilités)", textEn: "DRP/BCP procedure aligned with RTO (plan + responsibilities)", hint: "Runbook + exercices", hintEn: "Runbook + exercises" }
  ];

  const RA_RISK_CATALOG = [
    { id: "RISK_EXT_ATTACK", threat: "Attaque externe", threatEn: "External Attack", scenario: "Exploitation d'une vulnérabilité applicative sur un endpoint exposé", scenarioEn: "Exploitation of an application vulnerability on an exposed endpoint", domainHints: ["NETCRYPTO","SDLC","VULN","LOG"] },
    { id: "RISK_DATA_LEAK", threat: "Divulgation", threatEn: "Data Disclosure", scenario: "Accès non autorisé à des données sensibles via défaut IAM ou configuration", scenarioEn: "Unauthorized access to sensitive data via IAM or configuration flaw", domainHints: ["IAM","DATA","INFRA","LOG"] },
    { id: "RISK_PRIV_ESC", threat: "Élévation de privilèges", threatEn: "Privilege Escalation", scenario: "Abus de comptes privilégiés ou droits excessifs", scenarioEn: "Abuse of privileged accounts or excessive permissions", domainHints: ["IAM","LOG"] },
    { id: "RISK_RANSOM", threat: "Ransomware", threatEn: "Ransomware", scenario: "Chiffrement/destruction de données et indisponibilité prolongée", scenarioEn: "Data encryption/destruction and extended unavailability", domainHints: ["INFRA","BCP","LOG"] },
    { id: "RISK_SUPPLY", threat: "Supply chain", threatEn: "Supply Chain Attack", scenario: "Compromission d'une dépendance/CI-CD introduisant du code malveillant", scenarioEn: "Compromise of a dependency/CI-CD pipeline introducing malicious code", domainHints: ["SDLC","VULN","IAM","LOG"] },
    { id: "RISK_MISCONFIG", threat: "Mauvaise configuration", threatEn: "Misconfiguration", scenario: "Exposition involontaire (bucket public, règles réseau, ports)", scenarioEn: "Unintended exposure (public bucket, network rules, ports)", domainHints: ["INFRA","NETCRYPTO","LOG"] },
    { id: "RISK_DDOS", threat: "Déni de service", threatEn: "Denial of Service", scenario: "Saturation rendant le service indisponible au-delà du RTO", scenarioEn: "Saturation rendering the service unavailable beyond RTO", domainHints: ["NETCRYPTO","BCP"] },
    { id: "RISK_LOG_GAP", threat: "Détection insuffisante", threatEn: "Insufficient Detection", scenario: "Incident non détecté faute de logs/alerting", scenarioEn: "Incident undetected due to lack of logging/alerting", domainHints: ["LOG"] },
    { id: "RISK_PRIVACY", threat: "Non-conformité privacy", threatEn: "Privacy Non-Compliance", scenario: "Rétention excessive ou absence d'effacement conduisant à incident/sanction", scenarioEn: "Excessive retention or lack of erasure leading to incident/sanction", domainHints: ["DATA","IAM"] },
    { id: "RISK_INSIDER", threat: "Interne/erreur", threatEn: "Internal/Human Error", scenario: "Erreur de manipulation ou action malveillante interne", scenarioEn: "Operational error or malicious insider action", domainHints: ["IAM","LOG","DATA"] }
  ];

  const RA_THREATS_SEED = [
    { id: "THREAT_EXT_ATTACK", label: "Cyberattaque via périmètre externe (exploitation, injection, API abuse)", labelEn: "Cyberattack via external perimeter (exploitation, injection, API abuse)" },
    { id: "THREAT_MALWARE", label: "Malware ou ransomware affectant disponibilité et intégrité", labelEn: "Malware or ransomware affecting availability and integrity" },
    { id: "THREAT_DATA_LEAK", label: "Divulgation de données (accès non autorisé, exfiltration)", labelEn: "Data disclosure (unauthorized access, exfiltration)" },
    { id: "THREAT_PRIV_ESC", label: "Élévation de privilèges et abus d'accès", labelEn: "Privilege escalation and access abuse" },
    { id: "THREAT_DDOS", label: "Déni de service et indisponibilité prolongée", labelEn: "Denial of service and extended unavailability" },
    { id: "THREAT_SUPPLY_CHAIN", label: "Risque supply chain (dépendances, composants, CI/CD)", labelEn: "Supply chain risk (dependencies, components, CI/CD)" },
    { id: "THREAT_MISCONFIG", label: "Mauvaise configuration et exposition involontaire", labelEn: "Misconfiguration and unintended exposure" },
    { id: "THREAT_LOG_GAP", label: "Absence de traçabilité empêchant détection et réponse", labelEn: "Lack of traceability preventing detection and response" },
    { id: "THREAT_BACKUP_FAIL", label: "Sauvegardes insuffisantes ou non restaurables (RTO/RPO non tenus)", labelEn: "Insufficient or non-restorable backups (RTO/RPO not met)" },
    { id: "THREAT_COMPLIANCE", label: "Non-conformité (privacy, exigences internes) générant incident ou sanction", labelEn: "Non-compliance (privacy, internal requirements) leading to incident or sanction" }
  ];

  const RA_RECOMMENDATIONS_RULES = [{"id":"REC_IAM_MFA_001","semanticKey":"iam.mfa","when":{"any":[{"path":"architecture.iam.mfa","op":"eq","value":false},{"path":"controls.IAM.IAM_MFA","op":"in","value":["no","partial"]}]},"then":{"title":"Activer la MFA pour les comptes privilégiés et les accès sensibles","domainId":"IAM","priority":"P1","effort":"S","rationale":"L'absence de MFA augmente le risque de compromission de compte et d'abus de privilèges.","actions":["Activer MFA sur comptes admin et accès distants","Imposer MFA pour actions sensibles (changement droits, export données)","Définir un processus d'exception documenté et revu"],"doneWhen":"MFA activée pour 100% des comptes privilégiés; exceptions revues mensuellement.","titleEn":"Enable MFA for privileged accounts and sensitive access","rationaleEn":"Lack of MFA increases the risk of account compromise and privilege abuse.","actionsEn":["Enable MFA on admin accounts and remote access","Enforce MFA for sensitive actions (permission changes, data export)","Define a documented and reviewed exception process"],"doneWhenEn":"MFA enabled for 100% of privileged accounts; exceptions reviewed monthly."}},{"id":"REC_IAM_RBAC_001","semanticKey":"iam.rbac","when":{"any":[{"path":"controls.IAM.IAM_RBAC","op":"in","value":["no","partial"]}]},"then":{"title":"Appliquer le moindre privilège (RBAC) et séparer les rôles sensibles","domainId":"IAM","priority":"P1","effort":"M","rationale":"Des droits excessifs augmentent l'impact d'un compte compromis ou d'une erreur interne.","actions":["Définir des rôles standards (RBAC) et supprimer les droits individuels non justifiés","Séparer administration et usage courant; limiter les comptes 'break-glass'","Tracer et approuver les élévations temporaires"],"doneWhen":"Rôles RBAC en place; 90%+ des comptes rattachés à un rôle; élévations tracées.","titleEn":"Apply least privilege (RBAC) and separate sensitive roles","rationaleEn":"Excessive permissions increase the impact of a compromised account or internal error.","actionsEn":["Define standard roles (RBAC) and remove unjustified individual permissions","Separate administration and day-to-day usage; limit break-glass accounts","Trace and approve temporary privilege elevations"],"doneWhenEn":"RBAC roles in place; 90%+ of accounts assigned to a role; elevations traced."}},{"id":"REC_IAM_REVIEW_001","semanticKey":"iam.access_review","when":{"any":[{"path":"controls.IAM.IAM_REVIEW","op":"in","value":["no","partial"]}]},"then":{"title":"Mettre en place une revue périodique des accès et la gestion des comptes orphelins","domainId":"IAM","priority":"P2","effort":"S","rationale":"Sans revue, les droits s'accumulent et les comptes inactifs persistent, augmentant la surface d'attaque.","actions":["Programmer une revue trimestrielle des accès (owners métier/tech)","Définir un processus de suppression des comptes inactifs","Conserver une preuve de revue (audit)"],"doneWhen":"Revue trimestrielle effectuée; comptes inactifs supprimés sous 30 jours.","titleEn":"Implement periodic access reviews and orphan account management","rationaleEn":"Without reviews, permissions accumulate and inactive accounts persist, increasing the attack surface.","actionsEn":["Schedule quarterly access reviews (business/tech owners)","Define a process for removing inactive accounts","Retain proof of review (audit)"],"doneWhenEn":"Quarterly review completed; inactive accounts removed within 30 days."}},{"id":"REC_IAM_ADMINSEP_001","semanticKey":"iam.admin_separation","when":{"any":[{"path":"architecture.iam.adminSeparation","op":"eq","value":false}]},"then":{"title":"Séparer les comptes administrateurs des comptes nominaux","domainId":"IAM","priority":"P2","effort":"S","rationale":"La séparation réduit le risque d'abus de privilèges via compromission d'un compte quotidien.","actions":["Créer des comptes admin dédiés (nominatifs) pour actions privilégiées","Interdire la navigation/mail sur comptes admin","Journaliser toutes les actions admin et les revoir"],"doneWhen":"Comptes admin dédiés en place; usage contrôlé; logs admin consultables.","titleEn":"Separate administrator accounts from standard user accounts","rationaleEn":"Separation reduces the risk of privilege abuse through compromise of a daily-use account.","actionsEn":["Create dedicated admin accounts (named) for privileged actions","Prohibit browsing/email on admin accounts","Log all admin actions and review them"],"doneWhenEn":"Dedicated admin accounts in place; controlled usage; admin logs accessible."}},{"id":"REC_SDLC_REVIEW_001","semanticKey":"sdlc.code_review","when":{"any":[{"path":"controls.SDLC.SDLC_CODE","op":"in","value":["no","partial"]}]},"then":{"title":"Formaliser les revues de code et règles de merge","domainId":"SDLC","priority":"P2","effort":"S","rationale":"La revue réduit l'introduction de vulnérabilités et de changements non maîtrisés.","actions":["Imposer PR + approbation (2 eyes) sur branches protégées","Ajouter checklists sécurité (auth, input, logging, crypto)","Bloquer le merge si tests clés échouent"],"doneWhen":"Branches protégées; PR obligatoires; checklist sécurité utilisée.","titleEn":"Formalize code reviews and merge rules","rationaleEn":"Code reviews reduce the introduction of vulnerabilities and uncontrolled changes.","actionsEn":["Enforce PR + approval (2 eyes) on protected branches","Add security checklists (auth, input, logging, crypto)","Block merge if key tests fail"],"doneWhenEn":"Protected branches; mandatory PRs; security checklist in use."}},{"id":"REC_SDLC_SAST_001","semanticKey":"sdlc.security_testing","when":{"any":[{"path":"controls.SDLC.SDLC_SEC_TEST","op":"in","value":["no","partial"]}]},"then":{"title":"Mettre en place des tests sécurité (SAST/DAST/pentest) selon un calendrier","domainId":"SDLC","priority":"P1","effort":"M","rationale":"Sans tests, des vulnérabilités peuvent persister jusqu'à la production.","actions":["Intégrer SAST dans CI/CD (au minimum sur PR et releases)","Planifier DAST sur environnements préprod","Programmer un pentest périodique selon criticité"],"doneWhen":"SAST actif dans CI; DAST/pentest planifiés; findings suivis jusqu'à correction.","titleEn":"Implement security testing (SAST/DAST/pentest) on a schedule","rationaleEn":"Without testing, vulnerabilities may persist into production.","actionsEn":["Integrate SAST in CI/CD (at minimum on PRs and releases)","Schedule DAST on staging environments","Plan periodic pentests based on criticality"],"doneWhenEn":"SAST active in CI; DAST/pentest scheduled; findings tracked to remediation."}},{"id":"REC_SDLC_SECRETS_001","semanticKey":"sdlc.secrets","when":{"any":[{"path":"controls.SDLC.SDLC_SECRETS","op":"in","value":["no","partial"]}]},"then":{"title":"Mettre en place une gestion robuste des secrets (stockage, rotation, détection)","domainId":"SDLC","priority":"P1","effort":"M","rationale":"Les secrets exposés permettent compromission immédiate de systèmes et données.","actions":["Stocker secrets dans un gestionnaire (Vault/KMS/Secrets Manager)","Activer détection de secrets dans le code et l'historique","Définir une politique de rotation (et rotation après incident)"],"doneWhen":"Aucun secret en clair; détection active; rotation documentée et appliquée.","titleEn":"Implement robust secrets management (storage, rotation, detection)","rationaleEn":"Exposed secrets allow immediate compromise of systems and data.","actionsEn":["Store secrets in a manager (Vault/KMS/Secrets Manager)","Enable secret detection in code and history","Define a rotation policy (and rotate after incident)"],"doneWhenEn":"No cleartext secrets; active detection; documented and enforced rotation."}},{"id":"REC_VULN_SBOM_001","semanticKey":"vuln.inventory","when":{"any":[{"path":"controls.VULN.VULN_INVENTORY","op":"in","value":["no","partial"]}]},"then":{"title":"Maintenir un inventaire des composants (SBOM) et versions","domainId":"VULN","priority":"P2","effort":"M","rationale":"Sans inventaire, le traitement des vulnérabilités est incomplet et lent.","actions":["Générer un SBOM sur chaque build/release","Inventorier images, libs, runtime et dépendances transverses","Associer propriétaires et cycle de vie"],"doneWhen":"SBOM généré à chaque release; inventaire consultable; owners identifiés.","titleEn":"Maintain a component inventory (SBOM) and version tracking","rationaleEn":"Without an inventory, vulnerability management is incomplete and slow.","actionsEn":["Generate an SBOM on each build/release","Inventory images, libraries, runtimes and transverse dependencies","Assign owners and lifecycle"],"doneWhenEn":"SBOM generated per release; queryable inventory; owners identified."}},{"id":"REC_VULN_SCANS_001","semanticKey":"vuln.scanning","when":{"any":[{"path":"controls.VULN.VULN_SCANS","op":"in","value":["no","partial"]}]},"then":{"title":"Automatiser les scans de vulnérabilités et prioriser la remédiation","domainId":"VULN","priority":"P1","effort":"M","rationale":"Les vulnérabilités connues sont une cause fréquente d'intrusion.","actions":["Scanner dépendances et images (CI) + scanner infra si applicable","Définir un triage (CVSS, exploitabilité, exposition Internet)","Créer un backlog de remédiation avec SLA"],"doneWhen":"Scans réguliers; SLA défini; top vuln critiques corrigées dans les délais.","titleEn":"Automate vulnerability scans and prioritize remediation","rationaleEn":"Known vulnerabilities are a frequent cause of intrusion.","actionsEn":["Scan dependencies and images (CI) + infrastructure if applicable","Define triage (CVSS, exploitability, Internet exposure)","Create a remediation backlog with SLA"],"doneWhenEn":"Regular scans; SLA defined; top critical vulnerabilities remediated on time."}},{"id":"REC_VULN_PATCH_001","semanticKey":"vuln.patch_sla","when":{"any":[{"path":"controls.VULN.VULN_PATCH","op":"in","value":["no","partial"]}]},"then":{"title":"Mettre en place un processus de patching avec SLA selon criticité","domainId":"VULN","priority":"P1","effort":"M","rationale":"Sans SLA, les correctifs critiques sont retardés, augmentant la fenêtre d'exploitation.","actions":["Définir SLA (critique/haut/moyen) et un owner de remédiation","Mesurer le temps de correction et publier un tableau de bord","Gérer les exceptions (risque accepté) avec justification"],"doneWhen":"SLA documenté; suivi métriques; exceptions formalisées.","titleEn":"Implement a patching process with SLA based on criticality","rationaleEn":"Without SLA, critical patches are delayed, increasing the exploitation window.","actionsEn":["Define SLA (critical/high/medium) and a remediation owner","Measure correction time and publish a dashboard","Manage exceptions (accepted risk) with justification"],"doneWhenEn":"SLA documented; metrics tracked; exceptions formalized."}},{"id":"REC_LOG_CENTRAL_001","semanticKey":"log.centralization","when":{"any":[{"path":"controls.LOG.LOG_CENTRAL","op":"in","value":["no","partial"]}]},"then":{"title":"Centraliser les logs (app, auth, admin) et garantir l'horodatage","domainId":"LOG","priority":"P1","effort":"M","rationale":"Sans logs centralisés, la détection et l'investigation d'incidents sont limitées.","actions":["Centraliser logs applicatifs, authentification, actions admin, WAF/API GW","Synchroniser le temps (NTP) et normaliser formats","Restreindre l'accès et protéger l'intégrité"],"doneWhen":"Logs centralisés couvrant auth/admin; NTP en place; accès restreint.","titleEn":"Centralize logs (app, auth, admin) and ensure reliable timestamps","rationaleEn":"Without centralized logs, incident detection and investigation are limited.","actionsEn":["Centralize application, authentication, admin, and WAF/API GW logs","Synchronize time (NTP) and normalize formats","Restrict access and protect integrity"],"doneWhenEn":"Centralized logs covering auth/admin; NTP in place; access restricted."}},{"id":"REC_LOG_ALERTS_001","semanticKey":"log.alerting","when":{"any":[{"path":"controls.LOG.LOG_ALERTS","op":"in","value":["no","partial"]}]},"then":{"title":"Définir des use-cases de détection et des alertes sur événements critiques","domainId":"LOG","priority":"P1","effort":"M","rationale":"Sans alertes, les intrusions peuvent durer longtemps sans détection.","actions":["Créer alertes: échecs auth, MFA bypass, élévations privilèges, erreurs 5xx anormales","Configurer seuils et suppression de bruit (tuning)","Définir processus de réponse (on-call, escalade)"],"doneWhen":"Use-cases actifs; on-call défini; tests d'alertes réalisés.","titleEn":"Define detection use-cases and alerts on critical events","rationaleEn":"Without alerts, intrusions can persist undetected for extended periods.","actionsEn":["Create alerts: auth failures, MFA bypass, privilege elevations, abnormal 5xx errors","Configure thresholds and noise suppression (tuning)","Define response process (on-call, escalation)"],"doneWhenEn":"Use-cases active; on-call defined; alert tests performed."}},{"id":"REC_LOG_RETENTION_001","semanticKey":"log.retention_integrity","when":{"any":[{"path":"controls.LOG.LOG_RETENTION","op":"in","value":["no","partial"]}]},"then":{"title":"Définir rétention des logs et protection d'intégrité","domainId":"LOG","priority":"P2","effort":"S","rationale":"La rétention et l'intégrité des logs sont nécessaires pour audit et investigation.","actions":["Définir durée de rétention adaptée (conformité, besoin forensic)","Protéger contre suppression/modification non autorisée (WORM/immutabilité si possible)","Tracer les accès aux logs"],"doneWhen":"Rétention configurée; protections intégrité; accès aux logs audité.","titleEn":"Define log retention and integrity protection","rationaleEn":"Log retention and integrity are necessary for audit and investigation.","actionsEn":["Define appropriate retention period (compliance, forensic needs)","Protect against unauthorized deletion/modification (WORM/immutability if possible)","Trace access to logs"],"doneWhenEn":"Retention configured; integrity protections in place; log access audited."}},{"id":"REC_DATA_CLASSIF_001","semanticKey":"data.classification","when":{"any":[{"path":"controls.DATA.DATA_CLASSIF","op":"in","value":["no","partial"]}]},"then":{"title":"Mettre en place une classification des données et règles d'accès associées","domainId":"DATA","priority":"P2","effort":"S","rationale":"La classification permet d'adapter les protections aux données réellement sensibles.","actions":["Définir niveaux (public/interne/sensible) et exemples","Mapper niveaux aux exigences (chiffrement, logs, accès, rétention)","Appliquer sur les datasets clés"],"doneWhen":"Classification publiée; datasets clés classifiés; règles d'accès appliquées.","titleEn":"Implement data classification and associated access rules","rationaleEn":"Classification allows adapting protections to genuinely sensitive data.","actionsEn":["Define levels (public/internal/sensitive) and examples","Map levels to requirements (encryption, logs, access, retention)","Apply to key datasets"],"doneWhenEn":"Classification published; key datasets classified; access rules applied."}},{"id":"REC_DATA_ENCRYPT_001","semanticKey":"data.encryption_rest","when":{"any":[{"path":"controls.DATA.DATA_ENCRYPT_REST","op":"in","value":["no","partial"]}]},"then":{"title":"Assurer le chiffrement au repos des données sensibles","domainId":"DATA","priority":"P1","effort":"M","rationale":"Le chiffrement réduit l'impact d'une fuite de support ou d'un accès non autorisé à la couche stockage.","actions":["Activer chiffrement au repos (DB, volumes, objets)","Gérer les clefs via KMS et limiter les accès","Documenter exceptions/compensations si contraintes"],"doneWhen":"Chiffrement au repos actif; clefs gérées et rotées; exceptions documentées.","titleEn":"Ensure encryption at rest for sensitive data","rationaleEn":"Encryption reduces the impact of media leakage or unauthorized access to the storage layer.","actionsEn":["Enable encryption at rest (DB, volumes, objects)","Manage keys via KMS and restrict access","Document exceptions/compensations if constraints exist"],"doneWhenEn":"Encryption at rest active; keys managed and rotated; exceptions documented."}},{"id":"REC_DATA_RETENTION_001","semanticKey":"data.retention","when":{"any":[{"path":"controls.DATA.DATA_RETENTION","op":"in","value":["no","partial"]}]},"then":{"title":"Définir et appliquer une politique de rétention et d'effacement (privacy)","domainId":"DATA","priority":"P1","effort":"M","rationale":"Une rétention excessive augmente l'exposition et le risque de non-conformité.","actions":["Définir durées de conservation par type de données","Mettre en place purge/effacement automatisés et traçables","Tester et prouver l'effacement (échantillons)"],"doneWhen":"Durées validées; purge auto active; preuve d'effacement disponible.","titleEn":"Define and enforce a retention and erasure policy (privacy)","rationaleEn":"Excessive retention increases exposure and non-compliance risk.","actionsEn":["Define retention durations by data type","Implement automated and traceable purge/erasure","Test and prove erasure (samples)"],"doneWhenEn":"Durations validated; automated purge active; proof of erasure available."}},{"id":"REC_INFRA_HARDEN_001","semanticKey":"infra.hardening","when":{"any":[{"path":"controls.INFRA.INFRA_HARDEN","op":"in","value":["no","partial"]}]},"then":{"title":"Appliquer un durcissement (hardening) et des baselines de configuration","domainId":"INFRA","priority":"P2","effort":"M","rationale":"Des configurations faibles facilitent l'exploitation et les mouvements latéraux.","actions":["Définir baseline (CIS/équivalent) et la rendre vérifiable","Automatiser via IaC et contrôles de conformité","Traiter les écarts avec exceptions documentées"],"doneWhen":"Baseline définie; scans de conformité; taux d'écarts en baisse.","titleEn":"Apply hardening and configuration baselines","rationaleEn":"Weak configurations facilitate exploitation and lateral movement.","actionsEn":["Define baseline (CIS/equivalent) and make it verifiable","Automate via IaC and compliance controls","Address deviations with documented exceptions"],"doneWhenEn":"Baseline defined; compliance scans; deviation rate decreasing."}},{"id":"REC_INFRA_SEGMENT_001","semanticKey":"infra.segmentation","when":{"any":[{"path":"controls.INFRA.INFRA_SEGMENT","op":"in","value":["no","partial"]}]},"then":{"title":"Renforcer la segmentation réseau et la séparation des environnements","domainId":"INFRA","priority":"P1","effort":"M","rationale":"La segmentation limite les mouvements latéraux et l'impact d'une compromission.","actions":["Séparer dev/préprod/prod et limiter les flux inter-zones","Appliquer des règles 'deny by default' et inventorier flux nécessaires","Mettre en place revues périodiques des règles"],"doneWhen":"Environnements séparés; flux justifiés; revues régulières des règles.","titleEn":"Strengthen network segmentation and environment separation","rationaleEn":"Segmentation limits lateral movement and the impact of a compromise.","actionsEn":["Separate dev/staging/prod and limit inter-zone traffic","Apply deny-by-default rules and inventory required flows","Implement periodic rule reviews"],"doneWhenEn":"Environments separated; flows justified; regular rule reviews."}},{"id":"REC_INFRA_IMMUTABLE_BACKUP_001","semanticKey":"infra.backup_protection","when":{"any":[{"path":"controls.INFRA.INFRA_BACKUP_PROTECT","op":"in","value":["no","partial"]}]},"then":{"title":"Protéger les sauvegardes contre ransomware (immutabilité / isolation)","domainId":"INFRA","priority":"P1","effort":"M","rationale":"Des sauvegardes accessibles et modifiables peuvent être chiffrées par un attaquant.","actions":["Activer immutabilité / WORM / offline selon capacités","Séparer comptes/permissions de sauvegarde (least privilege)","Surveiller l'activité anormale sur stockage de sauvegarde"],"doneWhen":"Sauvegardes immuables ou isolées; comptes séparés; surveillance active.","titleEn":"Protect backups against ransomware (immutability / isolation)","rationaleEn":"Accessible and modifiable backups can be encrypted by an attacker.","actionsEn":["Enable immutability / WORM / offline as available","Separate backup accounts/permissions (least privilege)","Monitor abnormal activity on backup storage"],"doneWhenEn":"Backups immutable or isolated; accounts separated; active monitoring."}},{"id":"REC_NET_TLS_001","semanticKey":"net.tls","when":{"any":[{"path":"controls.NETCRYPTO.NET_TLS","op":"in","value":["no","partial"]}]},"then":{"title":"Renforcer TLS (versions modernes, configuration, HSTS si web)","domainId":"NETCRYPTO","priority":"P1","effort":"S","rationale":"TLS faible augmente le risque d'interception et d'attaque MITM.","actions":["Activer TLS 1.2+ (idéalement 1.3) et ciphers robustes","Automatiser le renouvellement certificats","Vérifier configuration via tests récurrents"],"doneWhen":"TLS moderne actif; renouvellement auto; tests TLS passés.","titleEn":"Strengthen TLS (modern versions, configuration, HSTS if web)","rationaleEn":"Weak TLS increases the risk of interception and MITM attacks.","actionsEn":["Enable TLS 1.2+ (ideally 1.3) and strong ciphers","Automate certificate renewal","Verify configuration via recurring tests"],"doneWhenEn":"Modern TLS active; auto-renewal; TLS tests passed."}},{"id":"REC_NET_WAF_001","semanticKey":"net.waf","when":{"all":[{"path":"architecture.internetExposed","op":"eq","value":true},{"path":"controls.NETCRYPTO.NET_WAF","op":"in","value":["no","partial"]}]},"then":{"title":"Mettre en place une protection périmétrique (WAF/API Gateway/rate-limit)","domainId":"NETCRYPTO","priority":"P1","effort":"M","rationale":"L'exposition Internet sans contrôle périmétrique augmente l'exploitation et l'abus d'API.","actions":["Déployer WAF ou API Gateway devant endpoints publics","Configurer rate limiting et protection contre bots","Journaliser et alerter sur anomalies (4xx/5xx, patterns OWASP)"],"doneWhen":"WAF/API GW actif; rate-limit configuré; alerting sur anomalies en place.","titleEn":"Implement perimeter protection (WAF/API Gateway/rate-limit)","rationaleEn":"Internet exposure without perimeter controls increases exploitation and API abuse.","actionsEn":["Deploy WAF or API Gateway in front of public endpoints","Configure rate limiting and bot protection","Log and alert on anomalies (4xx/5xx, OWASP patterns)"],"doneWhenEn":"WAF/API GW active; rate-limit configured; anomaly alerting in place."}},{"id":"REC_NET_KEYS_001","semanticKey":"net.key_management","when":{"any":[{"path":"controls.NETCRYPTO.NET_KEYS","op":"in","value":["no","partial"]}]},"then":{"title":"Structurer la gestion des clefs (KMS/HSM, rotation, accès contrôlé)","domainId":"NETCRYPTO","priority":"P2","effort":"M","rationale":"Une gestion faible des clefs compromet le chiffrement et la confidentialité.","actions":["Centraliser clefs dans KMS/HSM si possible","Définir rotation et séparation des rôles d'accès","Journaliser l'usage des clefs et alerter sur anomalies"],"doneWhen":"Clefs gérées via KMS/HSM; rotation active; logs d'usage consultables.","titleEn":"Structure key management (KMS/HSM, rotation, controlled access)","rationaleEn":"Weak key management compromises encryption and confidentiality.","actionsEn":["Centralize keys in KMS/HSM if possible","Define rotation and access role separation","Log key usage and alert on anomalies"],"doneWhenEn":"Keys managed via KMS/HSM; rotation active; usage logs accessible."}},{"id":"REC_NET_RATE_LIMIT_001","semanticKey":"net.rate_limit","when":{"all":[{"path":"architecture.internetExposed","op":"eq","value":true},{"path":"controls.NETCRYPTO.NET_WAF","op":"in","value":["no","partial"]}]},"then":{"title":"Mettre en place un rate limiting et une protection anti-abus d'API","domainId":"NETCRYPTO","priority":"P2","effort":"S","rationale":"La limitation réduit les attaques par brute force et abus de ressources.","actions":["Définir quotas et limites par client/clé API","Appliquer protection brute force sur auth","Tracer les dépassements et bloquer progressivement"],"doneWhen":"Quotas en place; brute force protégé; métriques de dépassement suivies.","titleEn":"Implement rate limiting and API abuse protection","rationaleEn":"Rate limiting reduces brute force attacks and resource abuse.","actionsEn":["Define quotas and limits per client/API key","Apply brute force protection on auth","Trace overages and progressively block"],"doneWhenEn":"Quotas in place; brute force protected; overage metrics tracked."}},{"id":"REC_BCP_BACKUP_001","semanticKey":"bcp.backups_rpo","when":{"any":[{"path":"controls.BCP.BCP_BACKUP","op":"in","value":["no","partial"]}]},"then":{"title":"Aligner la stratégie de sauvegardes sur le RPO","domainId":"BCP","priority":"P1","effort":"S","rationale":"Des sauvegardes trop espacées empêchent de tenir l'objectif de perte acceptable (RPO).","actions":["Définir fréquence de sauvegarde conforme RPO","Vérifier couverture (DB, fichiers, config, secrets si applicable)","Surveiller succès/échecs de jobs"],"doneWhen":"Fréquence conforme RPO; couverture validée; taux de succès > 99%.","titleEn":"Align backup strategy with RPO","rationaleEn":"Infrequent backups prevent meeting the acceptable data loss objective (RPO).","actionsEn":["Define backup frequency compliant with RPO","Verify coverage (DB, files, config, secrets if applicable)","Monitor job success/failure"],"doneWhenEn":"RPO-compliant frequency; validated coverage; success rate > 99%."}},{"id":"REC_BCP_RESTORE_TEST_001","semanticKey":"bcp.restore_tests","when":{"any":[{"path":"controls.BCP.BCP_RESTORE_TEST","op":"in","value":["no","partial"]}]},"then":{"title":"Tester régulièrement les restaurations et conserver des preuves","domainId":"BCP","priority":"P1","effort":"S","rationale":"Sans tests, une sauvegarde peut être inutilisable au moment critique.","actions":["Planifier tests de restauration (trimestriels minimum selon criticité)","Vérifier intégrité données et temps de restauration","Archiver les preuves (logs, comptes-rendus)"],"doneWhen":"Tests réalisés selon calendrier; temps mesuré; preuves archivées.","titleEn":"Regularly test restores and retain proof of success","rationaleEn":"Without tests, a backup may be unusable when critically needed.","actionsEn":["Schedule restore tests (quarterly minimum based on criticality)","Verify data integrity and restoration time","Archive evidence (logs, reports)"],"doneWhenEn":"Tests performed on schedule; time measured; evidence archived."}},{"id":"REC_BCP_DR_001","semanticKey":"bcp.dr_plan","when":{"any":[{"path":"controls.BCP.BCP_DR","op":"in","value":["no","partial"]}]},"then":{"title":"Formaliser un PRA/PCA et l'aligner sur le RTO","domainId":"BCP","priority":"P1","effort":"M","rationale":"Sans plan, le RTO est rarement tenable en situation de crise.","actions":["Rédiger runbook PRA (responsabilités, séquence, critères)","Exercer le plan (simulation) et améliorer","S'assurer de la disponibilité des accès/clefs/contacts en crise"],"doneWhen":"Runbook disponible; exercice réalisé; actions d'amélioration suivies.","titleEn":"Formalize a DRP/BCP and align it with RTO","rationaleEn":"Without a plan, RTO is rarely achievable in a crisis situation.","actionsEn":["Draft DRP runbook (responsibilities, sequence, criteria)","Exercise the plan (simulation) and improve","Ensure availability of access/keys/contacts during crisis"],"doneWhenEn":"Runbook available; exercise performed; improvement actions tracked."}},{"id":"REC_CTX_INTERNET_MINIMIZE_001","semanticKey":"ctx.minimize_exposure","when":{"all":[{"path":"architecture.internetExposed","op":"eq","value":true}]},"then":{"title":"Réduire l'exposition Internet au strict nécessaire","domainId":"NETCRYPTO","priority":"P2","effort":"S","rationale":"Moins d'exposition réduit mécaniquement la surface d'attaque.","actions":["Inventorier endpoints publics et supprimer ceux non nécessaires","Restreindre par IP/VPN/Zero Trust quand possible","Durcir auth et monitoring sur endpoints restants"],"doneWhen":"Inventaire à jour; endpoints inutiles supprimés; restrictions appliquées.","titleEn":"Reduce Internet exposure to the strict minimum","rationaleEn":"Less exposure mechanically reduces the attack surface.","actionsEn":["Inventory public endpoints and remove unnecessary ones","Restrict by IP/VPN/Zero Trust where possible","Harden auth and monitoring on remaining endpoints"],"doneWhenEn":"Inventory up to date; unnecessary endpoints removed; restrictions applied."}},{"id":"REC_CTX_CIA_HIGH_HARDEN_001","semanticKey":"ctx.cia_high_hardening","when":{"all":[{"path":"context.dataSensitivity","op":"eq","value":"high"}]},"then":{"title":"Appliquer un niveau renforcé de contrôles pour données sensibles","domainId":"DATA","priority":"P1","effort":"M","rationale":"Données sensibles exigent des protections renforcées.","actions":["Restreindre accès aux datasets sensibles (RBAC + justification)","Renforcer logs/alerting sur accès aux données","Revoir rétention et minimisation des données"],"doneWhen":"Accès restreints; logs/alertes sur accès; rétention revue.","titleEn":"Apply enhanced controls for sensitive data","rationaleEn":"Sensitive data requires reinforced protections.","actionsEn":["Restrict access to sensitive datasets (RBAC + justification)","Strengthen logging/alerting on data access","Review retention and data minimization"],"doneWhenEn":"Access restricted; logs/alerts on access; retention reviewed."}},{"id":"REC_CTX_RTO_STRICT_DRILL_001","semanticKey":"ctx.rto_drill","when":{"any":[{"path":"continuity.rtoHours","op":"lte","value":8},{"path":"continuity.rpoHours","op":"lte","value":6}]},"then":{"title":"Exercer régulièrement la reprise et mesurer les temps (RTO/RPO)","domainId":"BCP","priority":"P1","effort":"M","rationale":"Des objectifs serrés nécessitent des exercices et mesures pour être crédibles.","actions":["Organiser un exercice PRA/PCA (au moins annuel)","Mesurer temps de reprise et perte de données","Mettre à jour le plan et les dépendances"],"doneWhen":"Exercice réalisé; RTO/RPO mesurés; actions correctives suivies.","titleEn":"Regularly exercise recovery and measure times (RTO/RPO)","rationaleEn":"Tight objectives require exercises and measurements to be credible.","actionsEn":["Organize a DRP/BCP exercise (at least annually)","Measure recovery time and data loss","Update plan and dependencies"],"doneWhenEn":"Exercise performed; RTO/RPO measured; corrective actions tracked."}},{"id":"REC_LOG_ADMIN_AUDIT_001","semanticKey":"log.admin_audit","when":{"any":[{"path":"domainTrafficLight.LOG","op":"in","value":["yellow","red"]}]},"then":{"title":"Surveiller les actions administrateur et les changements sensibles","domainId":"LOG","priority":"P2","effort":"S","rationale":"Les actions admin sont souvent la source d'incidents ou d'abus.","actions":["Tracer actions admin (création droits, exports, changements config)","Alerter sur activités anormales (heures, volumes, pays)","Revoir périodiquement les changements"],"doneWhen":"Mesure mise en œuvre et vérifiée.","titleEn":"Monitor administrator actions and sensitive changes","rationaleEn":"Admin actions are often the source of incidents or abuse.","actionsEn":["Trace admin actions (permission creation, exports, config changes)","Alert on abnormal activities (hours, volumes, countries)","Periodically review changes"],"doneWhenEn":"Measure implemented and verified."}},{"id":"REC_VULN_EXPOSED_FASTPATCH_001","semanticKey":"vuln.exposed_fastpatch","when":{"any":[{"path":"domainTrafficLight.VULN","op":"in","value":["yellow","red"]}]},"then":{"title":"Accélérer la remédiation des vulnérabilités sur surfaces exposées","domainId":"VULN","priority":"P1","effort":"M","rationale":"Les vulnérabilités exposées Internet sont plus exploitables.","actions":["Tagger les composants exposés","Appliquer SLA plus court sur CVE critiques exposées","Mettre en place une fenêtre de déploiement rapide"],"doneWhen":"Mesure mise en œuvre et vérifiée.","titleEn":"Accelerate vulnerability remediation on exposed surfaces","rationaleEn":"Internet-exposed vulnerabilities are more exploitable.","actionsEn":["Tag exposed components","Apply shorter SLA for critical exposed CVEs","Implement a rapid deployment window"],"doneWhenEn":"Measure implemented and verified."}},{"id":"REC_SDLC_RELEASE_GATES_001","semanticKey":"sdlc.release_gates","when":{"any":[{"path":"domainTrafficLight.SDLC","op":"in","value":["yellow","red"]}]},"then":{"title":"Mettre en place des 'gates' de release (tests + vulnérabilités)","domainId":"SDLC","priority":"P2","effort":"S","rationale":"Amélioration priorisée au vu de la posture actuelle.","actions":["Bloquer release si vulnérabilités critiques non justifiées","Exiger tests sécurité minimum sur release","Tracer les dérogations"],"doneWhen":"Mesure mise en œuvre et vérifiée.","titleEn":"Implement release gates (tests + vulnerabilities)","rationaleEn":"Prioritized improvement based on current security posture.","actionsEn":["Block release if unjustified critical vulnerabilities exist","Require minimum security tests on release","Trace waivers"],"doneWhenEn":"Measure implemented and verified."}},{"id":"REC_DATA_BACKUP_SCOPE_001","semanticKey":"data.backup_scope","when":{"any":[{"path":"domainTrafficLight.BCP","op":"in","value":["yellow","red"]}]},"then":{"title":"Vérifier la couverture des sauvegardes (données + configuration + secrets)","domainId":"BCP","priority":"P2","effort":"S","rationale":"Amélioration priorisée au vu de la posture actuelle.","actions":["Lister les éléments à sauvegarder (DB, fichiers, config, IaC)","Vérifier restauration complète (pas seulement DB)","Documenter la procédure"],"doneWhen":"Mesure mise en œuvre et vérifiée.","titleEn":"Verify backup coverage (data + configuration + secrets)","rationaleEn":"Prioritized improvement based on current security posture.","actionsEn":["List elements to back up (DB, files, config, IaC)","Verify complete restoration (not just DB)","Document the procedure"],"doneWhenEn":"Measure implemented and verified."}},{"id":"REC_NET_ZERO_TRUST_001","semanticKey":"net.zero_trust","when":{"any":[{"path":"domainTrafficLight.NETCRYPTO","op":"in","value":["yellow","red"]}]},"then":{"title":"Restreindre l'administration via accès conditionnel / bastion","domainId":"NETCRYPTO","priority":"P2","effort":"M","rationale":"Amélioration priorisée au vu de la posture actuelle.","actions":["Imposer un bastion ou accès conditionnel","Limiter l'accès admin par réseau et identité","Tracer toutes les sessions admin"],"doneWhen":"Mesure mise en œuvre et vérifiée.","titleEn":"Restrict administration via conditional access / bastion","rationaleEn":"Prioritized improvement based on current security posture.","actionsEn":["Enforce a bastion or conditional access","Limit admin access by network and identity","Trace all admin sessions"],"doneWhenEn":"Measure implemented and verified."}},{"id":"REC_INFRA_IAC_POLICY_001","semanticKey":"infra.iac_policy","when":{"any":[{"path":"domainTrafficLight.INFRA","op":"in","value":["yellow","red"]}]},"then":{"title":"Contrôler l'infrastructure as code (policies, revues, drift)","domainId":"INFRA","priority":"P2","effort":"M","rationale":"Amélioration priorisée au vu de la posture actuelle.","actions":["Imposer revue IaC","Appliquer policy-as-code (deny misconfig)","Détecter et corriger le drift"],"doneWhen":"Mesure mise en œuvre et vérifiée.","titleEn":"Control infrastructure as code (policies, reviews, drift)","rationaleEn":"Prioritized improvement based on current security posture.","actionsEn":["Enforce IaC review","Apply policy-as-code (deny misconfig)","Detect and fix drift"],"doneWhenEn":"Measure implemented and verified."}}];

  // ============================================================
  // DATA MODEL
  // ============================================================
  function createBlankAnalysis() {
    return {
      id: _id(),
      meta: { title: '', status: 'draft', createdAt: _now(), updatedAt: _now(), schemaVersion: 1 },
      impactMatrixId: null,
      identity: { encoder: '', validator: '', controlBody: '', beneficiary: '', projectManager: '' },
      context: { perimeter: '', description: '', stakeholders: '', personalData: false, dataSensitivity: 'medium', sourceFramework: '' },
      cia: { confidentiality: 2, integrity: 2, availability: 2, justification: '' },
      gdpr: { personalData: false, sensitiveData: false, processingRegistry: 'no' },
      continuity: { rtoHours: 24, rpoHours: 24, comment: '' },
      architecture: { hosting: 'cloud', internetExposed: false, iam: { sso: false, mfa: false, adminSeparation: false }, architectureDoc: false },
      controls: {},
      risks: [],
      results: null,
      finalRecs: { items: [] },
      spot: {
        avisComment: '',
        residualCorrectedComment: '',
        openRiskAdjustment: 0,
        residualOverrideScore: null,
        residualOverrideJustification: '',
        avisOverrideLight: null,
        avisOverrideJustification: ''
      }
    };
  }

  function getAnalyses() { return (window.appData && window.appData.riskAnalyses) || []; }
  function ensureAnalysisSchema(a) {
    if (!a) return a;
    if (!a.identity) a.identity = { encoder: '', validator: '', controlBody: '', beneficiary: '', projectManager: '' };
    if (a.architecture && a.architecture.architectureDoc === undefined) a.architecture.architectureDoc = false;
    if (!a.gdpr) a.gdpr = { personalData: !!(a.context && a.context.personalData), sensitiveData: false, processingRegistry: 'no' };
    if (a.risks) a.risks.forEach(function(r) { if (r.owner === undefined) r.owner = ''; });
    return a;
  }
  function findAnalysis(id) { var a = getAnalyses().find(function(x) { return x.id === id; }); return ensureAnalysisSchema(a); }
  function _save() { if (typeof window.saveData === 'function') window.saveData(); }

  // ============================================================
  // SCORING ENGINE
  // ============================================================
  function computeDomainMaturity(controls, domainId) {
    var questions = RA_CONTROLS_SEED.filter(q => q.domainId === domainId);
    var total = 0, max = 0, plannedCount = 0;
    questions.forEach(function(q) {
      var c = controls[q.id];
      var status = c ? (c.status || 'no') : 'no';
      if (status === 'na') return;
      max += 3;
      if (status === 'implemented') total += 3;
      else if (status === 'partial') total += 2;
      else if (status === 'planned') { total += 2; plannedCount++; }
      else if (status === 'toVerify') total += 1;
    });
    if (max === 0) return { maturity: 3, percent: 100, light: 'green', plannedCount: 0 };
    var maturity = Math.round((total / max) * 3);
    var percent = Math.round((total / max) * 100);
    var light = (maturity >= 2 && percent >= 70) ? 'green' : (maturity < 1 || percent < 40) ? 'red' : 'yellow';
    return { maturity: maturity, percent: percent, light: light, plannedCount: plannedCount };
  }

  function computeAllDomainScores(analysis) {
    var scores = {};
    RA_DOMAINS.forEach(function(d) {
      scores[d.id] = computeDomainMaturity(analysis.controls || {}, d.id);
    });
    return scores;
  }

  function computeInherentScore(likelihood, impact) { return likelihood * impact; }

  function computeResidualScore(inherent, controlsMaturity, analysis) {
    var reduction = 1 - (controlsMaturity / 3) * 0.40;
    var base = Math.round(inherent * reduction);
    if (analysis.architecture && analysis.architecture.internetExposed) base += 1;
    var cia = analysis.cia || {};
    if (cia.confidentiality === 3 || cia.integrity === 3 || cia.availability === 3) base += 1;
    var cont = analysis.continuity || {};
    if (cont.rtoHours <= 8 || cont.rpoHours <= 6) base += 1;
    return _clamp(base, 1, 25);
  }

  function riskTrafficLight(score) {
    return score <= 8 ? 'green' : score <= 15 ? 'yellow' : 'red';
  }

  /** Resolve the 5-level risk level for a given likelihood + impact pair */
  function resolveRiskLevel(likelihood, impact) {
    var lvl = getRiskLevelForCell(likelihood || 3, impact || 3);
    var tl = LEVEL_TO_TRAFFIC[lvl.id] || 'yellow';
    return { id: lvl.id, label: _bi(lvl, 'label', 'labelEn'), labelFr: lvl.labelFr || lvl.label, labelEn: lvl.labelEn || lvl.label, color: lvl.color, trafficLight: tl, light: tl };
  }

  function computeGlobalTrafficLight(riskScores) {
    if (riskScores.some(function(r) { return r.light === 'red'; })) return 'red';
    if (riskScores.some(function(r) { return r.light === 'yellow'; })) return 'yellow';
    return 'green';
  }

  function computeGlobalScore(globalLight, analysis, domainScores) {
    var base = globalLight === 'green' ? 2 : globalLight === 'yellow' ? 3 : 4;
    var cia = analysis.cia || {};
    if ((cia.confidentiality === 3 || cia.integrity === 3 || cia.availability === 3) && analysis.architecture && analysis.architecture.internetExposed) base += 1;
    var cont = analysis.continuity || {};
    if ((cont.rtoHours <= 8 || cont.rpoHours <= 6) && domainScores.BCP && domainScores.BCP.light === 'red') base += 1;
    return _clamp(base, 1, 5);
  }

  function computeAllResults(analysis) {
    var domainScores = computeAllDomainScores(analysis);
    // Aggregate planned controls info
    var totalPlannedControls = 0;
    var plannedControlsList = [];
    RA_DOMAINS.forEach(function(d) {
      var ds = domainScores[d.id];
      if (ds && ds.plannedCount > 0) {
        totalPlannedControls += ds.plannedCount;
        RA_CONTROLS_SEED.filter(function(q) { return q.domainId === d.id; }).forEach(function(q) {
          var c = (analysis.controls || {})[q.id];
          if (c && c.status === 'planned') {
            plannedControlsList.push({ questionId: q.id, domainId: d.id, text: _bi(q, 'text', 'textEn'), textFr: q.text, textEn: q.textEn || q.text, comment: c.comment || '' });
          }
        });
      }
    });
    // Build comprehensive controls list (all controls by domain) for preview/PDF
    var allControlsList = [];
    RA_DOMAINS.forEach(function(d) {
      var domainCtrls = [];
      RA_CONTROLS_SEED.filter(function(q) { return q.domainId === d.id; }).forEach(function(q) {
        var c = (analysis.controls || {})[q.id];
        domainCtrls.push({
          questionId: q.id,
          domainId: d.id,
          text: _bi(q, 'text', 'textEn'),
          textFr: q.text, textEn: q.textEn || q.text,
          hint: _bi(q, 'hint', 'hintEn'),
          hintFr: q.hint || '', hintEn: q.hintEn || q.hint || '',
          status: c ? (c.status || 'no') : 'no',
          comment: c ? (c.comment || '') : ''
        });
      });
      allControlsList.push({ domainId: d.id, label: _bi(d, 'label', 'labelEn'), labelFr: d.label, labelEn: d.labelEn || d.label, icon: d.icon, controls: domainCtrls });
    });
    var riskScores = [];
    (analysis.risks || []).forEach(function(r) {
      var inh = computeInherentScore(r.likelihood || 3, r.impact || 3);
      var avgMat = 0, cnt = 0;
      var cat = RA_RISK_CATALOG.find(c => c.id === r.catalogId);
      if (cat && cat.domainHints) {
        cat.domainHints.forEach(function(d) { if (domainScores[d]) { avgMat += domainScores[d].maturity; cnt++; } });
      }
      if (cnt > 0) avgMat = avgMat / cnt;
      var res = computeResidualScore(inh, avgMat, analysis);
      var lvl = resolveRiskLevel(r.likelihood || 3, r.impact || 3);
      riskScores.push({ id: r.id, inherent: inh, residual: res, light: lvl.light, levelId: lvl.id, levelLabel: lvl.label, levelLabelFr: lvl.labelFr, levelLabelEn: lvl.labelEn, levelColor: lvl.color, trafficLight: lvl.trafficLight });
    });
    var globalLight = riskScores.length > 0 ? computeGlobalTrafficLight(riskScores) : 'green';
    var globalScore = computeGlobalScore(globalLight, analysis, domainScores);
    var recommendations = generateRecommendations(analysis, domainScores);
    var spotSummary = computeSpotSummary(analysis, { domainScores: domainScores, riskScores: riskScores });
    // Preserve auto values, then apply manual override if present
    var autoGlobalLight = globalLight;
    var autoGlobalScore = globalScore;
    if (spotSummary.avisOverridden) {
      globalLight = spotSummary.light;
      globalScore = computeGlobalScore(spotSummary.light, analysis, domainScores);
    }
    return {
      domainScores: domainScores,
      riskScores: riskScores,
      globalLight: globalLight,
      globalScore: globalScore,
      autoGlobalLight: autoGlobalLight,
      autoGlobalScore: autoGlobalScore,
      recommendations: recommendations,
      spotSummary: spotSummary,
      totalPlannedControls: totalPlannedControls,
      plannedControlsList: plannedControlsList,
      allControlsList: allControlsList,
      computedAt: _now()
    };
  }

  function computeSpotSummary(analysis, results) {
    var cia = analysis.cia || {};
    var ctx = analysis.context || {};
    var arch = analysis.architecture || {};
    var spot = analysis.spot || {};
    var finalRecs = analysis.finalRecs || { items: [] };

    // 1. Inherent criticality (1..5)
    var maxCIA = Math.max(cia.confidentiality || 1, cia.integrity || 1, cia.availability || 1);
    var base = maxCIA === 1 ? 2 : maxCIA === 2 ? 3 : 4;
    if (arch.internetExposed) base += 1;
    if (ctx.dataSensitivity === 'high' || ctx.personalData === true) base += 1;
    var inherentCriticalityScore = _clamp(base, 1, 5);

    // 2. Measures implementation percent
    var totalP = 0, domC = 0;
    RA_DOMAINS.forEach(function(d) {
      if (results.domainScores && results.domainScores[d.id]) { totalP += results.domainScores[d.id].percent; domC++; }
    });
    var measuresImplementationPercent = domC > 0 ? Math.round(totalP / domC) : 0;

    // 3. Residual score (1..5) from worst risk score (1..25)
    var worstResidual = 1;
    if (results.riskScores && results.riskScores.length > 0) {
      results.riskScores.forEach(function(rs) { if (rs.residual > worstResidual) worstResidual = rs.residual; });
    }
    var residualScore;
    if (worstResidual <= 5) residualScore = 1;
    else if (worstResidual <= 8) residualScore = 2;
    else if (worstResidual <= 12) residualScore = 3;
    else if (worstResidual <= 15) residualScore = 4;
    else residualScore = 5;
    // Override
    if (spot.residualOverrideScore != null && spot.residualOverrideScore >= 1 && spot.residualOverrideScore <= 5) {
      residualScore = spot.residualOverrideScore;
    }

    // 4. Residual + open risk
    var openAdj = _clamp(spot.openRiskAdjustment || 0, 0, 2);
    var residualPlusOpenScore = _clamp(residualScore + openAdj, 1, 5);

    // 5. Traffic light + avis (automatic)
    var validItems = finalRecs.items.filter(function(it) { return it && (it.title || it.details); });
    var anyTodo = validItems.some(function(it) { return it.status !== 'done'; });
    var autoLight, autoAvisLabel;
    if (residualPlusOpenScore >= 5) {
      autoLight = 'red'; autoAvisLabel = _t('riskAnalysis.spot.avisLabels.red', 'Avis défavorable');
    } else if (validItems.length > 0 && anyTodo) {
      autoLight = 'yellow'; autoAvisLabel = _t('riskAnalysis.spot.avisLabels.yellow', 'Avis favorable moyennant la mise en œuvre des recommandations clefs');
    } else {
      autoLight = 'green'; autoAvisLabel = _t('riskAnalysis.spot.avisLabels.green', 'Avis favorable');
    }

    // 6. Manual override of avis
    var light = autoLight, avisLabel = autoAvisLabel;
    var avisOverride = spot.avisOverrideLight;
    if (avisOverride === 'green' || avisOverride === 'yellow' || avisOverride === 'red') {
      light = avisOverride;
      avisLabel = _t('riskAnalysis.spot.avisLabels.' + avisOverride, autoAvisLabel);
    }

    return {
      inherentCriticalityScore: inherentCriticalityScore,
      measuresImplementationPercent: measuresImplementationPercent,
      residualScore: residualScore,
      residualPlusOpenScore: residualPlusOpenScore,
      autoLight: autoLight,
      autoAvisLabel: autoAvisLabel,
      light: light,
      avisLabel: avisLabel,
      avisOverridden: light !== autoLight
    };
  }

  // ============================================================
  // RECOMMENDATION ENGINE
  // ============================================================
  function resolvePath(analysis, path, indexes) {
    if (path.startsWith('controls.')) {
      var parts = path.split('.');
      var c = analysis.controls || {};
      var ctrl = c[parts[2]];
      return ctrl ? (ctrl.status || 'no') : 'no';
    }
    if (path.startsWith('domainTrafficLight.')) {
      var dom = path.split('.')[1];
      return indexes.domainLights[dom] || 'grey';
    }
    var segs = path.split('.'), val = analysis;
    for (var i = 0; i < segs.length; i++) {
      if (val == null) return undefined;
      val = val[segs[i]];
    }
    return val;
  }

  function evaluateCondition(analysis, cond, indexes) {
    var actual = resolvePath(analysis, cond.path, indexes);
    switch (cond.op) {
      case 'eq': return actual === cond.value;
      case 'in':
        if (Array.isArray(cond.value) && cond.value.indexOf(actual) >= 0) return true;
        // 'planned' controls match rules that check for 'partial' (attenuated recommendations)
        if (actual === 'planned' && Array.isArray(cond.value) && cond.value.indexOf('partial') >= 0) return true;
        if (actual === 'toVerify' && Array.isArray(cond.value) && cond.value.indexOf('no') >= 0) return true;
        return false;
      case 'lte': return typeof actual === 'number' && actual <= cond.value;
      case 'gte': return typeof actual === 'number' && actual >= cond.value;
      default: return false;
    }
  }

  function evaluateRule(analysis, rule, indexes) {
    var when = rule.when;
    if (when.all) return when.all.every(function(c) { return evaluateCondition(analysis, c, indexes); });
    if (when.any) return when.any.some(function(c) { return evaluateCondition(analysis, c, indexes); });
    return false;
  }

  function generateRecommendations(analysis, domainScores) {
    var indexes = { domainLights: {} };
    RA_DOMAINS.forEach(function(d) { indexes.domainLights[d.id] = domainScores[d.id] ? domainScores[d.id].light : 'grey'; });
    var candidates = [];
    RA_RECOMMENDATIONS_RULES.forEach(function(rule) {
      if (evaluateRule(analysis, rule, indexes)) {
        var t = rule.then;
        var score = t.priority === 'P1' ? 300 : t.priority === 'P2' ? 200 : 100;
        // Bumps
        var linkedRisks = (analysis.risks || []).filter(function(r) {
          var cat = RA_RISK_CATALOG.find(c => c.id === r.catalogId);
          return cat && cat.domainHints && cat.domainHints.indexOf(t.domainId) >= 0;
        });
        linkedRisks.forEach(function() { score += 25; });
        if (analysis.architecture && analysis.architecture.internetExposed && ['NETCRYPTO','VULN','LOG'].indexOf(t.domainId) >= 0) score += 20;
        var cont = analysis.continuity || {};
        if ((cont.rtoHours <= 8 || cont.rpoHours <= 6) && t.domainId === 'BCP') score += 20;
        var cia = analysis.cia || {};
        if ((cia.confidentiality === 3 || cia.integrity === 3 || cia.availability === 3) && ['IAM','DATA','NETCRYPTO','LOG'].indexOf(t.domainId) >= 0) score += 10;
        // Check if recommendation triggered by a 'planned' control — attenuate score
        var isPlanned = false;
        var conds = rule.when.all || rule.when.any || [];
        conds.forEach(function(cond) {
          if (cond.path && cond.path.startsWith('controls.')) {
            var actual = resolvePath(analysis, cond.path, indexes);
            if (actual === 'planned') isPlanned = true;
          }
        });
        if (isPlanned) score -= 75;
        candidates.push({
          id: rule.id, semanticKey: rule.semanticKey,
          title: _bi(t, 'title', 'titleEn'), titleFr: t.title, titleEn: t.titleEn || t.title,
          domainId: t.domainId, priority: t.priority, effort: t.effort,
          rationale: _bi(t, 'rationale', 'rationaleEn'), rationaleFr: t.rationale, rationaleEn: t.rationaleEn || t.rationale,
          actions: (_lang() === 'en' && t.actionsEn) ? t.actionsEn : t.actions, actionsFr: t.actions, actionsEn: t.actionsEn || t.actions,
          doneWhen: _bi(t, 'doneWhen', 'doneWhenEn'), doneWhenFr: t.doneWhen, doneWhenEn: t.doneWhenEn || t.doneWhen,
          score: score, isPlanned: isPlanned
        });
      }
    });
    // Dedup by semanticKey
    var seen = {};
    var deduped = [];
    candidates.sort(function(a, b) { return SortUtils.numeric(b.score, a.score); });
    candidates.forEach(function(c) {
      if (!seen[c.semanticKey]) { seen[c.semanticKey] = true; deduped.push(c); }
    });
    return deduped.slice(0, 12);
  }

  // ============================================================
  // WIZARD STATE
  // ============================================================
  var wizardState = { analysisId: null, step: 0, resultTab: 'synthese' };
  var STEP_KEYS = ['context','cia','continuity','architecture','controls','risks','mesures','results','preview','exportPdf'];

  // ============================================================
  // ENTRY POINT
  // ============================================================
  window.renderRiskAnalysisModule = function() {
    var container = document.getElementById('riskAnalysisContent');
    if (!container) return;
    var cons = window.CSI_EntityConsolidation;
    if (cons && cons.getSectionMode && cons.getSectionMode('risk-analysis') === 'consolidated') {
      wizardState.analysisId = null;
      wizardState.step = 0;
    }
    if (window._csiRenderConsolidatedSectionIfNeeded &&
        window._csiRenderConsolidatedSectionIfNeeded('risk-analysis', { contentId: 'riskAnalysisContent' })) {
      SectionToolbar.destroy('risk-analysis');
      var btnConsolidated = document.getElementById('addRiskAnalysisBtn');
      if (btnConsolidated) btnConsolidated.onclick = null;
      return;
    }
    if (wizardState.analysisId) {
      SectionToolbar.destroy('risk-analysis');
      renderWizard(container);
      return;
    }
    renderList(container);
    // Initialise SectionToolbar only if it does not already exist in the DOM
    if (!document.getElementById('toolbar-risk-analysis')) {
      SectionToolbar.init('risk-analysis', {
        search: true,
        filters: [
          {
            key: 'status',
            label: _t('riskAnalysis.filters.status', 'Statut'),
            defaultValue: raListFilters.status !== 'all' ? raListFilters.status : '',
            options: [
              { value: 'draft', label: _t('riskAnalysis.filters.statusDraft', 'Brouillon') },
              { value: 'complete', label: _t('riskAnalysis.filters.statusComplete', 'Terminée') }
            ]
          }
        ],
        sort: {
          options: [
            { key: 'date', label: _t('riskAnalysis.filters.sortDateDesc', 'Date'), default: true, dir: 'desc' },
            { key: 'title', label: _t('riskAnalysis.filters.sortTitle', 'Titre') },
            { key: 'score', label: _t('riskAnalysis.filters.sortScore', 'Score'), dir: 'desc' }
          ]
        },
        onUpdate: window.raFilterList
      });
    }
    var btn = document.getElementById('addRiskAnalysisBtn');
    if (btn) btn.onclick = function() {
      if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode(_t('editMode.createAnalysis', 'create an analysis'))) return;
      createNewAnalysis();
    };
  };

  window.cleanupRiskAnalysisModule = function() {
    SectionToolbar.destroy('risk-analysis');
    Object.keys(_raChartInstances).forEach(function(key) {
      if (_raChartInstances[key]) {
        try { _raChartInstances[key].destroy(); } catch(e) {}
        _raChartInstances[key] = null;
      }
    });
    if (_raObserver) { _raObserver.disconnect(); _raObserver = null; }
  };

  // ============================================================
  // LIST VIEW
  // ============================================================
  var thresholdPanelOpen = false;
  var thresholdEditId = null;

  var raListFilters = { status: 'all' };

  // Lot 1b : petit menu « ⋯ » par ligne (dupliquer / PDF / export / sync).
  // Fermeture au clic extérieur et à Escape ; un seul menu ouvert à la fois.
  window.raCloseMoreMenus = function() {
    var menus = document.querySelectorAll('.ra-more-menu');
    for (var i = 0; i < menus.length; i++) {
      menus[i].hidden = true;
      var btn = menus[i].parentNode ? menus[i].parentNode.querySelector('[aria-haspopup]') : null;
      if (btn) btn.setAttribute('aria-expanded', 'false');
    }
  };
  window.raToggleMoreMenu = function(btn) {
    var menu = btn.parentNode ? btn.parentNode.querySelector('.ra-more-menu') : null;
    if (!menu) return;
    var willOpen = menu.hidden;
    window.raCloseMoreMenus();
    menu.hidden = !willOpen;
    btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
    if (typeof window.refreshReadOnlyLocks === 'function') window.refreshReadOnlyLocks();
  };
  document.addEventListener('click', function(e) {
    if (!e.target || !e.target.closest || !e.target.closest('.ra-more-wrap')) window.raCloseMoreMenus();
  });
  document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape' || e.key === 'Esc') window.raCloseMoreMenus();
  });

  window.raFilterList = function() {
    var filters = SectionToolbar.getFilters('risk-analysis');
    if (filters && filters.status !== undefined) raListFilters.status = filters.status || 'all';
    window.renderRiskAnalysisModule();
  };

  function renderList(container) {
    var analyses = getAnalyses();
    var html = '';

    // Threshold toggle button
    html += '<div class="ra-list-toolbar">';
    html += '<button class="btn btn--sm' + (thresholdPanelOpen ? ' btn--active' : '') + '" onclick="raToggleThresholdPanel()"><i class="fas fa-table-cells"></i> ' + _esc(_t('riskAnalysis.thresholds.title','Seuils de risque')) + '</button>';
    html += '</div>';

    // Threshold management panel (collapsible)
    if (thresholdPanelOpen) {
      html += renderThresholdPanel();
    }

    // SectionToolbar placeholder — actual toolbar is inserted by SectionToolbar.init
    // into the #risk-analysis module div (between .module__header and content)

    if (!analyses.length) {
      html += '<div class="ra-empty-state"><i class="fas fa-magnifying-glass-chart"></i><p>' + _esc(_t('riskAnalysis.noAnalyses', 'Aucune analyse de risques.')) + '</p><p>' + _esc(_t('riskAnalysis.noAnalysesHint', 'Créez votre première analyse.')) + '</p></div>';
      container.innerHTML = html;
      if (typeof window.refreshReadOnlyLocks === 'function') window.refreshReadOnlyLocks();
      return;
    }

    // Apply filters
    var searchQuery = SectionToolbar.getSearch('risk-analysis').toLowerCase();
    var filtered = analyses.filter(function(a) {
      if (searchQuery) {
        var hay = [(a.meta && a.meta.title) || '', (a.identity && a.identity.beneficiary) || '', (a.meta && a.meta.reportNumber) || ''].join(' ').toLowerCase();
        if (hay.indexOf(searchQuery) < 0) return false;
      }
      if (raListFilters.status !== 'all') {
        var st = (a.meta && a.meta.status === 'complete') ? 'complete' : 'draft';
        if (st !== raListFilters.status) return false;
      }
      return true;
    });

    // Apply sort
    var sortState = SectionToolbar.getSort('risk-analysis');
    var sortKey = sortState ? sortState.key : 'date';
    var sortDir = sortState ? sortState.dir : 'desc';
    filtered.sort(function(a, b) {
      switch (sortKey) {
        case 'date':
          return sortDir === 'asc'
            ? SortUtils.date((a.meta && a.meta.createdAt) || '', (b.meta && b.meta.createdAt) || '')
            : SortUtils.date((b.meta && b.meta.createdAt) || '', (a.meta && a.meta.createdAt) || '');
        case 'title':
          return sortDir === 'asc'
            ? SortUtils.text((a.meta && a.meta.title) || '', (b.meta && b.meta.title) || '')
            : SortUtils.text((b.meta && b.meta.title) || '', (a.meta && a.meta.title) || '');
        case 'score':
          return sortDir === 'desc'
            ? SortUtils.numeric((b.results && b.results.globalScore) || 0, (a.results && a.results.globalScore) || 0)
            : SortUtils.numeric((a.results && a.results.globalScore) || 0, (b.results && b.results.globalScore) || 0);
        default:
          return SortUtils.date((b.meta && b.meta.createdAt) || '', (a.meta && a.meta.createdAt) || '');
      }
    });

    if (!filtered.length) {
      html += '<div class="ra-empty-state"><i class="fas fa-filter"></i><p>' + _esc(_t('riskAnalysis.noResults','Aucun résultat.')) + '</p></div>';
      container.innerHTML = html;
      if (typeof window.refreshReadOnlyLocks === 'function') window.refreshReadOnlyLocks();
      return;
    }

    html += '<table class="ra-list-table"><thead><tr>';
    html += '<th>' + _esc(_t('riskAnalysis.listColumns.title','Titre')) + '</th>';
    html += '<th>' + _esc(_t('riskAnalysis.listColumns.created','Créée le')) + '</th>';
    html += '<th>' + _esc(_t('riskAnalysis.listColumns.status','Statut')) + '</th>';
    html += '<th>' + _esc(_t('riskAnalysis.listColumns.score','Score')) + '</th>';
    html += '<th>' + _esc(_t('riskAnalysis.reviewDate.column','Revue')) + '</th>';
    html += '<th>' + _esc(_t('riskAnalysis.listColumns.actions','Actions')) + '</th>';
    html += '</tr></thead><tbody>';
    filtered.forEach(function(a) {
      var dateStr = a.meta && a.meta.createdAt ? new Date(a.meta.createdAt).toLocaleDateString() : '—';
      var status = a.meta && a.meta.status === 'complete' ? 'complete' : 'draft';
      var statusLabel = status === 'complete' ? _t('riskAnalysis.statusComplete','Terminée') : _t('riskAnalysis.statusDraft','Brouillon');
      var r = a.results;
      var lightClass = r ? 'ra-light--' + r.globalLight : 'ra-light--grey';
      html += '<tr class="ra-list-row" onclick="raOpenWizard(\'' + a.id + '\')">';
      html += '<td><strong>' + _esc(a.meta ? a.meta.title || 'Sans titre' : 'Sans titre') + '</strong></td>';
      html += '<td>' + _esc(dateStr) + '</td>';
      html += '<td><span class="ra-status-badge ra-status-badge--' + status + '">' + _esc(statusLabel) + '</span></td>';
      // Lot 1b : colonne « Feu » fusionnée dans « Score » (pastille + valeur)
      html += '<td class="ra-score-cell"><span class="ra-light ' + lightClass + '"></span> ' + (r ? r.globalScore + '/5' : '—') + '</td>';
      // Review date column
      var reviewDate = a.meta && a.meta.nextReviewDate;
      var reviewHtml = '—';
      if (reviewDate) {
        try {
          var rd = new Date(reviewDate);
          var now = new Date();
          var diffDays = Math.round((rd - now) / (1000 * 60 * 60 * 24));
          reviewHtml = rd.toLocaleDateString();
          if (diffDays < 0) reviewHtml = '<span class="ra-review-overdue">' + reviewHtml + '</span>';
          else if (diffDays <= 30) reviewHtml = '<span class="ra-review-upcoming">' + reviewHtml + '</span>';
        } catch(e) { reviewHtml = _esc(reviewDate); }
      }
      html += '<td>' + reviewHtml + '</td>';
      html += '<td class="ra-list-actions">';
      // Lot 1b : bouton « éditer » supprimé (redondant avec le clic-ligne) ; dupliquer/PDF/
      // export/sync regroupés derrière un menu « ⋯ » ; « supprimer » reste visible.
      html += '<div class="ra-more-wrap">';
      html += '<button class="btn btn--sm" onclick="event.stopPropagation();raToggleMoreMenu(this)" aria-haspopup="true" aria-expanded="false" title="' + _esc(_t('riskAnalysis.moreActions', 'Plus d’actions')) + '"><i class="fas fa-ellipsis"></i></button>';
      html += '<div class="ra-more-menu" hidden>';
      // FIX-RA-I18N (audit 2026-06-10) : 'common.duplicate' n'existe dans aucune locale →
      // réutilise la clé existante 'riskAnalysis.thresholds.duplicate'.
      html += '<button class="btn btn--sm" onclick="event.stopPropagation();raCloseMoreMenus();raDuplicate(\'' + a.id + '\')"><i class="fas fa-copy"></i> ' + _esc(_t('riskAnalysis.thresholds.duplicate', 'Dupliquer')) + '</button>';
      html += '<button class="btn btn--sm" onclick="event.stopPropagation();raCloseMoreMenus();raExportPdf(\'' + a.id + '\')"><i class="fas fa-file-pdf"></i> PDF</button>';
      html += '<button class="btn btn--sm" onclick="event.stopPropagation();raCloseMoreMenus();raExportAnalyseFile(\'' + a.id + '\')"><i class="fas fa-file-export"></i> ' + _esc(_t('riskAnalysis.exportAnalyseFile','Export .analyse')) + '</button>';
      if (status === 'complete') {
        html += '<button class="btn btn--sm ra-sync-btn" onclick="event.stopPropagation();raCloseMoreMenus();raSyncToProject(\'' + a.id + '\')" data-readonly-lock="true"><i class="fas fa-arrow-right-to-bracket"></i> ' + _esc(_t('riskAnalysis.syncToProject','Sync vers Risques projets')) + '</button>';
      }
      html += '</div></div>';
      html += '<button class="btn btn--danger btn--sm" onclick="event.stopPropagation();raDelete(\'' + a.id + '\')" data-readonly-lock="true" title="' + _esc(_t('common.delete', 'Delete')) + '"><i class="fas fa-trash"></i></button>';
      html += '</td></tr>';
    });
    html += '</tbody></table>';
    container.innerHTML = html;
    if (typeof window.refreshReadOnlyLocks === 'function') window.refreshReadOnlyLocks();
  }

  // ============================================================
  // THRESHOLD MANAGEMENT PANEL
  // ============================================================
  function renderThresholdPanel() {
    var store = ensureThresholdsInit();
    var activeT = getActiveThreshold();
    var editT = thresholdEditId ? store.items.find(function(i) { return i.id === thresholdEditId; }) : activeT;
    if (!editT) editT = activeT;
    thresholdEditId = editT.id;

    var html = '<div class="ra-threshold-panel">';
    html += '<div class="ra-threshold-panel__header">';
    html += '<h4 class="ra-section-title"><i class="fas fa-table-cells"></i> ' + _esc(_t('riskAnalysis.thresholds.title','Seuils de risque')) + '</h4>';
    html += '<div class="ra-threshold-panel__actions">';
    html += '<button class="btn btn--sm" onclick="raCreateThreshold()" data-readonly-lock="true"><i class="fas fa-plus"></i> ' + _esc(_t('riskAnalysis.thresholds.new','Nouveau')) + '</button>';
    html += '</div></div>';

    // List of threshold configs
    html += '<div class="ra-threshold-list">';
    store.items.forEach(function(t) {
      var isActive = t.id === store.activeId;
      var isEditing = t.id === thresholdEditId;
      html += '<div class="ra-threshold-card' + (isEditing ? ' ra-threshold-card--selected' : '') + '">';
      html += '<div class="ra-threshold-card__info" onclick="raSelectThresholdEdit(\'' + t.id + '\')">';
      html += '<strong>' + _esc(t.name) + '</strong>';
      if (isActive) html += ' <span class="ra-threshold-badge-active"><i class="fas fa-check-circle"></i> ' + _esc(_t('riskAnalysis.thresholds.active','Actif')) + '</span>';
      html += '</div>';
      html += '<div class="ra-threshold-card__actions">';
      if (!isActive) html += '<button class="btn btn--sm" onclick="raActivateThreshold(\'' + t.id + '\')" data-readonly-lock="true" title="' + _esc(_t('riskAnalysis.thresholds.activate','Activer')) + '"><i class="fas fa-check"></i></button>';
      html += '<button class="btn btn--sm" onclick="raDuplicateThreshold(\'' + t.id + '\')" data-readonly-lock="true" title="' + _esc(_t('riskAnalysis.thresholds.duplicate','Dupliquer')) + '"><i class="fas fa-copy"></i></button>';
      if (store.items.length > 1) html += '<button class="btn btn--danger btn--sm" onclick="raDeleteThreshold(\'' + t.id + '\')" data-readonly-lock="true" title="' + _esc(_t('riskAnalysis.thresholds.delete','Supprimer')) + '"><i class="fas fa-trash"></i></button>';
      html += '</div></div>';
    });
    html += '</div>';

    // Editor for selected threshold
    html += '<div class="ra-threshold-editor">';
    html += '<div class="ra-form-group"><label class="ra-form-label">' + _esc(_t('riskAnalysis.thresholds.editName','Nom')) + '</label>';
    html += '<input type="text" id="raThreshName" value="' + _esc(editT.name) + '" onchange="raUpdateThreshField(\'name\',this.value)" data-readonly-lock="true"></div>';
    html += '<div class="ra-form-group"><label class="ra-form-label">' + _esc(_t('riskAnalysis.thresholds.editDescription','Description')) + '</label>';
    html += '<input type="text" id="raThreshDesc" value="' + _esc(editT.description || '') + '" onchange="raUpdateThreshField(\'description\',this.value)" data-readonly-lock="true"></div>';

    // Levels editor
    html += '<div class="ra-threshold-levels">';
    html += '<label class="ra-form-label">' + _esc(_t('riskAnalysis.thresholds.riskLevels','Niveaux de risque')) + '</label>';
    html += '<div class="ra-threshold-levels-list">';
    editT.levels.forEach(function(lv, idx) {
      html += '<div class="ra-threshold-level-item">';
      html += '<input type="color" value="' + _esc(lv.color) + '" onchange="raUpdateThreshLevel(' + idx + ',\'color\',this.value)" data-readonly-lock="true" class="ra-color-input">';
      html += '<input type="text" value="' + _esc(lv.label) + '" onchange="raUpdateThreshLevel(' + idx + ',\'label\',this.value)" data-readonly-lock="true" class="ra-level-input">';
      html += '</div>';
    });
    html += '</div></div>';

    // 5×5 matrix grid
    html += '<div class="ra-threshold-matrix-section">';
    html += '<label class="ra-form-label">' + _esc(_t('riskAnalysis.thresholds.matrix','Matrice Probabilité × Gravité')) + '</label>';
    html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.thresholds.clickToChange','Cliquez sur une cellule pour changer son niveau')) + '</div>';
    html += renderThresholdMatrix(editT);
    html += '</div>';

    // Legend
    html += '<div class="ra-threshold-legend">';
    editT.levels.forEach(function(lv) {
      html += '<span class="ra-threshold-legend-item"><span class="ra-threshold-legend-dot" style="background:' + lv.color + '"></span>' + _esc(_bi(lv, 'label', 'labelEn')) + '</span>';
    });
    html += '</div>';

    html += '</div></div>';
    return html;
  }

  function renderThresholdMatrix(t) {
    var lang = _lang();
    var gLabels = (lang === 'en' && t.gravityLabels) ? t.gravityLabels : (t.gravityLabelsFr || t.gravityLabels);
    var pLabels = (lang === 'en' && t.probabilityLabels) ? t.probabilityLabels : (t.probabilityLabelsFr || t.probabilityLabels);
    var html = '<div class="ra-threshold-grid">';
    // Header row: empty corner + gravity labels
    html += '<div class="ra-tg-corner"><span class="ra-tg-axis-label ra-tg-axis-g">' + _esc(_t('riskAnalysis.thresholds.gravity','Gravité')) + ' →</span><span class="ra-tg-axis-label ra-tg-axis-p">↑ ' + _esc(_t('riskAnalysis.thresholds.probability','Probabilité')) + '</span></div>';
    for (var g = 1; g <= 5; g++) {
      html += '<div class="ra-tg-header">' + g + '<br><small>' + _esc(gLabels[g-1] || '') + '</small></div>';
    }
    // Rows: probability 5→1 (top to bottom)
    for (var p = 5; p >= 1; p--) {
      html += '<div class="ra-tg-rowlabel">' + p + '<br><small>' + _esc(pLabels[p-1] || '') + '</small></div>';
      for (var g2 = 1; g2 <= 5; g2++) {
        var key = p + '_' + g2;
        var levelId = t.cells[key] || 'minimum';
        var lvl = t.levels.find(function(l) { return l.id === levelId; });
        var color = lvl ? lvl.color : '#ccc';
        var score = p * g2;
        html += '<div class="ra-tg-cell" style="background:' + color + '" onclick="raThreshCellCycle(' + p + ',' + g2 + ')" title="P' + p + ' × G' + g2 + ' = ' + score + ' → ' + _esc(lvl ? _bi(lvl, 'label', 'labelEn') : '') + '">';
        html += '<span class="ra-tg-score">' + score + '</span>';
        html += '</div>';
      }
    }
    html += '</div>';
    return html;
  }

  // Global functions for threshold management
  window.raToggleThresholdPanel = function() {
    thresholdPanelOpen = !thresholdPanelOpen;
    window.renderRiskAnalysisModule();
  };
  window.raSelectThresholdEdit = function(id) {
    thresholdEditId = id;
    window.renderRiskAnalysisModule();
  };
  window.raCreateThreshold = function() {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode(_t('editMode.createThreshold', 'create a threshold'))) return;
    var t = createThreshold();
    thresholdEditId = t.id;
    window.renderRiskAnalysisModule();
  };
  window.raDuplicateThreshold = function(id) {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode('duplicate a threshold')) return;
    var t = duplicateThreshold(id);
    if (t) thresholdEditId = t.id;
    window.renderRiskAnalysisModule();
  };
  window.raDeleteThreshold = function(id) {
    showConfirmModal(
      _t('riskAnalysis.thresholds.confirmDelete','Supprimer ce seuil ?'),
      _t('riskAnalysis.thresholds.confirmDeleteMsg','Cette action est irréversible.'),
      function() {
        deleteThreshold(id);
        thresholdEditId = null;
        window.renderRiskAnalysisModule();
      },
      { type: 'danger' }
    );
  };
  window.raActivateThreshold = function(id) {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode('activate a threshold')) return;
    activateThreshold(id);
    window.renderRiskAnalysisModule();
  };
  window.raUpdateThreshField = function(field, value) {
    if (!thresholdEditId) return;
    var store = ensureThresholdsInit();
    var t = store.items.find(function(i) { return i.id === thresholdEditId; });
    if (!t) return;
    t[field] = value;
    t.updatedAt = _now();
    _save();
  };
  window.raUpdateThreshLevel = function(idx, field, value) {
    if (!thresholdEditId) return;
    var store = ensureThresholdsInit();
    var t = store.items.find(function(i) { return i.id === thresholdEditId; });
    if (!t || !t.levels[idx]) return;
    t.levels[idx][field] = value;
    t.updatedAt = _now();
    _save();
    window.renderRiskAnalysisModule();
  };
  window.raThreshCellCycle = function(p, g) {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode(_t('editMode.editThreshold', 'edit a threshold'))) return;
    if (!thresholdEditId) return;
    var store = ensureThresholdsInit();
    var t = store.items.find(function(i) { return i.id === thresholdEditId; });
    if (!t) return;
    var key = p + '_' + g;
    var currentId = t.cells[key] || 'minimum';
    var ids = t.levels.map(function(l) { return l.id; });
    var curIdx = ids.indexOf(currentId);
    var nextIdx = (curIdx + 1) % ids.length;
    t.cells[key] = ids[nextIdx];
    t.updatedAt = _now();
    _save();
    window.renderRiskAnalysisModule();
  };

  function createNewAnalysis() {
    var a = createBlankAnalysis();
    // FIX-RA-I18N (audit 2026-06-10) : la clé 'risk.newAnalysis' n'existe dans aucune locale
    // → toute nouvelle analyse créée en UI française recevait le titre anglais « New analysis N »
    // (persisté dans les données). La clé correcte est 'riskAnalysis.newAnalysis' (fr+en).
    a.meta.title = _t('riskAnalysis.newAnalysis', 'Nouvelle analyse') + ' ' + (getAnalyses().length + 1);
    if (!window.appData.riskAnalyses) window.appData.riskAnalyses = [];
    window.appData.riskAnalyses.push(a);
    _save();
    if (typeof window.updateDashboard === 'function') window.updateDashboard();
    wizardState.analysisId = a.id;
    wizardState.step = 0;
    window.renderRiskAnalysisModule();
  }

  window.raOpenWizard = function(id) { wizardState.analysisId = id; wizardState.step = 0; window.renderRiskAnalysisModule(); };
  window.raDuplicate = function(id) {
    var a = findAnalysis(id); if (!a) return;
    var clone = _deepClone(a);
    clone.id = _id();
    clone.meta.title = (clone.meta.title || '') + ' (copie)';
    clone.meta.status = 'draft';
    clone.meta.createdAt = _now();
    clone.meta.updatedAt = _now();
    clone.results = null;
    if (clone.meta) { delete clone.meta.reportNumber; delete clone.meta.reportDate; }
    window.appData.riskAnalyses.push(clone);
    _save();
    if (typeof window.updateDashboard === 'function') window.updateDashboard();
    if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.duplicated','Analyse dupliquée.'), 'success');
    window.renderRiskAnalysisModule();
  };
  window.raDelete = function(id) {
    showConfirmModal(
      _t('riskAnalysis.confirmDelete','Supprimer cette analyse de risques ?'),
      _t('riskAnalysis.confirmDeleteMsg','L\'analyse sera définitivement supprimée ainsi que les risques projets associés.'),
      function() {
        window.appData.riskAnalyses = getAnalyses().filter(a => a.id !== id);
        removeProjectRiskForAnalysis(id);
        _save();
        if (typeof window.updateDashboard === 'function') window.updateDashboard();
        if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.deleted','Analyse supprimée.'), 'info');
        window.renderRiskAnalysisModule();
      },
      { type: 'danger' }
    );
  };
  window.raExportPdf = function(id) { var a = findAnalysis(id); if (a) generateRiskAnalysisPDF(a); };

  // ============================================================
  // MANUAL SYNC TO PROJECT RISKS
  // ============================================================
  window.raSyncToProject = function(analysisId) {
    var a = findAnalysis(analysisId);
    if (!a) return;
    if (!window.appData.projectRisks) window.appData.projectRisks = [];
    var pr = window.appData.projectRisks;

    // Check if already synced by sourceAnalysisId
    var existingBySource = pr.find(function(r) { return r.sourceAnalysisId === analysisId; });
    if (existingBySource) {
      syncAnalysisToProjectRisks(a);
      _save();
      if (typeof window.updateDashboard === 'function') window.updateDashboard();
      if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.syncAlreadySynced', 'Risque projet mis à jour.'), 'success');
      return;
    }

    // Check for name duplicate (case-insensitive)
    var analysisTitle = (a.meta && a.meta.title) || '';
    var existingByName = pr.find(function(r) {
      return (r.projectName || '').toLowerCase() === analysisTitle.toLowerCase() && analysisTitle !== '';
    });

    if (existingByName) {
      // Close any existing modal before showing sync dialog
      if (typeof window.closeModal === 'function') window.closeModal();
      // Show 3-option dialog via custom modal body
      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) return;

      modalTitle.textContent = _t('riskAnalysis.syncDuplicateTitle', 'Doublon détecté');
      modalBody.innerHTML = '<div class="confirm-modal__icon confirm-modal__icon--warning"><i class="fas fa-exclamation-circle"></i></div>' +
        '<div class="confirm-modal__message">' + _esc(_t('riskAnalysis.syncDuplicateMsg', 'Un risque projet avec le même nom existe déjà. Que souhaitez-vous faire ?')) + '</div>' +
        '<div style="display:flex;gap:8px;justify-content:center;margin-top:16px;flex-wrap:wrap;">' +
        '<button class="btn" id="raSyncCancel">' + _esc(_t('riskAnalysis.syncOptionCancel', 'Annuler')) + '</button>' +
        '<button class="btn btn--primary" id="raSyncNew">' + _esc(_t('riskAnalysis.syncOptionNew', 'Créer nouveau')) + '</button>' +
        '<button class="btn btn--warning" id="raSyncOverwrite">' + _esc(_t('riskAnalysis.syncOptionOverwrite', 'Écraser existant')) + '</button>' +
        '</div>';
      if (saveBtn) saveBtn.style.display = 'none';
      if (cancelBtn) cancelBtn.style.display = 'none';
      if (closeBtn) { closeBtn.style.display = ''; closeBtn.disabled = false; closeBtn.onclick = function() { window.closeModal(); }; }
      if (deleteBtn) deleteBtn.style.display = 'none';
      overlay.classList.add('modal-overlay--visible');

      document.getElementById('raSyncCancel').onclick = function() { window.closeModal(); };
      document.getElementById('raSyncNew').onclick = function() {
        window.closeModal();
        syncAnalysisToProjectRisks(a);
        _save();
        if (typeof window.updateDashboard === 'function') window.updateDashboard();
        if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.syncSuccess', 'Risque projet créé avec succès.'), 'success');
      };
      document.getElementById('raSyncOverwrite').onclick = function() {
        window.closeModal();
        // Remove existing by name, then sync (creates new with sourceAnalysisId)
        window.appData.projectRisks = window.appData.projectRisks.filter(function(r) { return r.id !== existingByName.id; });
        syncAnalysisToProjectRisks(a);
        _save();
        if (typeof window.updateDashboard === 'function') window.updateDashboard();
        if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.syncSuccess', 'Risque projet créé avec succès.'), 'success');
      };
      return;
    }

    // No duplicate — sync directly
    syncAnalysisToProjectRisks(a);
    _save();
    if (typeof window.updateDashboard === 'function') window.updateDashboard();
    if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.syncSuccess', 'Risque projet créé avec succès.'), 'success');
  };

  // ============================================================
  // EXPORT / IMPORT .analyse FILE
  // ============================================================
  window.raExportAnalyseFile = function(id) {
    var a = findAnalysis(id);
    if (!a) return;
    var envelope = {
      type: 'cyber-assistant-risk-analysis',
      version: '1.0',
      exportDate: new Date().toISOString(),
      analysis: _deepClone(a)
    };
    // Embed linked impact matrix if present
    if (a.impactMatrixId && window.CSI_ImpactMatrices) {
      var m = window.CSI_ImpactMatrices.getMatrixById(a.impactMatrixId);
      if (m) envelope.impactMatrix = _deepClone(m);
    }
    // Embed active risk threshold
    var t = getActiveThreshold();
    if (t) envelope.riskThreshold = _deepClone(t);
    var json = JSON.stringify(envelope, null, 2);
    var blob = new Blob([json], { type: 'application/json' });
    var url = URL.createObjectURL(blob);
    var el = document.createElement('a');
    el.href = url;
    var slug = (a.meta.title || 'analyse').replace(/[^a-zA-Z0-9àâäéèêëïîôùûüçÀÂÄÉÈÊËÏÎÔÙÛÜÇ]/g, '_').substring(0, 40);
    el.download = 'RA_' + slug + '_' + new Date().toISOString().slice(0, 10) + '.analyse';
    document.body.appendChild(el);
    el.click();
    document.body.removeChild(el);
    URL.revokeObjectURL(url);
    if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.analyseExported', 'Fichier .analyse exporté.'), 'success');
  };

  // Import from file picker
  window.raImportAnalyseFile = function() {
    if (typeof window.ensureEditMode === 'function' && !window.ensureEditMode(_t('editMode.importAnalysis', 'import an analysis'))) return;
    var input = document.getElementById('analyseFileInput');
    if (!input) return;
    input.value = '';
    input.click();
    input.onchange = function(e) {
      var file = e.target.files[0];
      if (!file) return;
      var reader = new FileReader();
      reader.onload = function(ev) {
        try {
          var data = JSON.parse(ev.target.result);
          window.raImportAnalyseFromData(data, file.name);
        } catch (err) {
          if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.importAnalyseError', 'Fichier .analyse invalide.'), 'error');
        }
      };
      reader.readAsText(file);
    };
  };

  // Import from parsed data (used by both file picker and global import routing)
  window.raImportAnalyseFromData = function(data, fileName) {
    if (!data || data.type !== 'cyber-assistant-risk-analysis' || !data.analysis) {
      if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.importAnalyseError', 'Fichier .analyse invalide.'), 'error');
      return;
    }
    var a = _deepClone(data.analysis);
    // Build summary for confirmation
    var title = (a.meta && a.meta.title) || 'Sans titre';
    var status = (a.meta && a.meta.status) || 'draft';
    var risksCount = Array.isArray(a.risks) ? a.risks.length : 0;
    var controlsCount = a.controls ? Object.keys(a.controls).length : 0;
    var summary = _t('riskAnalysis.importAnalyseSummary', 'Résumé de l\'analyse à importer :') + '\n\n';
    summary += '• ' + _t('riskAnalysis.listColumns.title', 'Titre') + ': ' + title + '\n';
    summary += '• ' + _t('riskAnalysis.listColumns.status', 'Statut') + ': ' + status + '\n';
    summary += '• ' + _t('riskAnalysis.risks.title', 'Risques') + ': ' + risksCount + '\n';
    summary += '• ' + _t('riskAnalysis.controls.title', 'Contrôles') + ': ' + controlsCount + '\n';
    if (data.impactMatrix) summary += '• ' + _t('riskAnalysis.pdfImpactMatrix', 'Matrice d\'impact') + ': ' + (data.impactMatrix.name || 'Oui') + '\n';
    if (fileName) summary += '\n' + _t('riskAnalysis.importSource', 'Source') + ': ' + fileName;
    showConfirmModal(
      _t('riskAnalysis.importConfirm', 'Importer cette analyse ?'),
      summary,
      function() {
        // Assign new ID (always create a copy)
        a.id = _id();
        if (!a.meta) a.meta = {};
        // FIX-RA-I18N (audit 2026-06-10) : 'riskAnalysis.imported' n'existe dans aucune locale
        // (le fallback français « importé » s'affichait en UI anglaise). La clé existante est
        // 'riskAnalysis.controls.imported' (« importé » fr / « imported » en).
        a.meta.title = (a.meta.title || '') + ' (' + _t('riskAnalysis.controls.imported', 'importé') + ')';
        a.meta.createdAt = _now();
        a.meta.updatedAt = _now();
        delete a.meta.reportNumber;
        delete a.meta.reportDate;
        // Normalize
        a = normalizeRiskAnalysis(a);
        // AUDIT-M1: Import embedded impact matrix — avec détection de doublons améliorée.
        // Avant: le check utilisait l'ID original du fichier, mais le clone avait un nouvel ID.
        // Importer le même .analyse 2x créait N matrices identiques orphelines.
        // Fix: chercher aussi par _importedFromId pour détecter les clones existants.
        if (data.impactMatrix && window.CSI_ImpactMatrices) {
          var matrices = (window.appData.impactMatrices && window.appData.impactMatrices.matrices) || [];
          var existing = window.CSI_ImpactMatrices.getMatrixById(data.impactMatrix.id);
          // Chercher aussi un clone déjà importé depuis le même original
          if (!existing) {
            existing = matrices.find(function(m) { return m._importedFromId === data.impactMatrix.id; });
          }
          if (existing) {
            // Réutiliser la matrice existante — mettre à jour le lien
            a.impactMatrixId = existing.id;
          } else {
            var cloneM = _deepClone(data.impactMatrix);
            cloneM._importedFromId = data.impactMatrix.id;
            cloneM.id = _id();
            matrices.push(cloneM);
            if (!window.appData.impactMatrices) window.appData.impactMatrices = { matrices: [] };
            window.appData.impactMatrices.matrices = matrices;
            a.impactMatrixId = cloneM.id;
          }
        }
        // AUDIT-M2: Import embedded risk threshold — même fix anti-duplication + linking.
        // Avant: le seuil était importé dans le store mais JAMAIS lié à l'analyse.
        if (data.riskThreshold) {
          var store = ensureThresholdsInit();
          var existingT = store.items.find(function(i) { return i.id === data.riskThreshold.id; });
          // Chercher aussi un clone déjà importé depuis le même original
          if (!existingT) {
            existingT = store.items.find(function(i) { return i._importedFromId === data.riskThreshold.id; });
          }
          if (existingT) {
            a.riskThresholdId = existingT.id;
          } else {
            var cloneT = _deepClone(data.riskThreshold);
            cloneT._importedFromId = data.riskThreshold.id;
            cloneT.id = _id();
            store.items.push(cloneT);
            a.riskThresholdId = cloneT.id;
          }
        }
        // Add to analyses
        if (!window.appData.riskAnalyses) window.appData.riskAnalyses = [];
        window.appData.riskAnalyses.push(a);
        _save();
        // Sync to project risks
        syncAnalysisToProjectRisks(a);
        _save();
        // M1 : l'import a pu muter impactMatrices/riskThresholds (clones embarqués) —
        // les pousser explicitement en mode serveur (syncAnalysisToProjectRisks ne gère
        // que projectRisks). No-op hors mode serveur.
        _persistAffected(['impactMatrices', 'riskThresholds']);
        if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.importAnalyseSuccess', 'Analyse importée avec succès.'), 'success');
        window.renderRiskAnalysisModule();
      },
      { type: 'info' }
    );
  };

  // ============================================================
  // AUTO-SYNC TO PROJECT RISKS
  // ============================================================
  // M2 (audit 2026-07-10) : fusionne les recommandations recalculées (top-3) avec celles
  // déjà présentes au lieu de les ÉCRASER. Préserve l'état 'implemented', le texte édité par
  // l'utilisateur, les id stables et les ajouts manuels. Rapprochement par titre normalisé.
  function mergeProjectRecommendations(oldRecs, newRecs) {
    var out = Array.isArray(oldRecs) ? oldRecs.slice() : [];
    var norm = function(s) { return String(s || '').trim().toLowerCase(); };
    var byTitle = {};
    out.forEach(function(r) { if (r && r.title) byTitle[norm(r.title)] = r; });
    (Array.isArray(newRecs) ? newRecs : []).forEach(function(nr) {
      var key = norm(nr && nr.title);
      var match = key && byTitle[key];
      if (match) {
        // Rafraîchir le texte auto seulement s'il était vide (ne pas clobber l'édition) ;
        // conserver id + implemented existants.
        if (nr.text && !match.text) match.text = nr.text;
      } else {
        out.push(nr);
        if (key) byTitle[key] = nr;
      }
    });
    return out;
  }

  // M1 (audit 2026-07-10) : en mode serveur, saveData() ne pousse que l'onglet courant
  // (risk-analysis) + global-meta. Les mutations d'autres onglets (projectRisks,
  // impactMatrices, riskThresholds) doivent être poussées explicitement, sinon elles ne sont
  // jamais persistées et sont écrasées au prochain pull (même famille que FIX-CASCADE-PERSIST).
  // No-op hors mode serveur (_csiPersistAffectedTabs est gardé par isServerModeUnlocked).
  function _persistAffected(keys) {
    try {
      if (typeof window._csiPersistAffectedTabs === 'function') window._csiPersistAffectedTabs(keys);
    } catch (e) { /* best-effort */ }
  }

  function syncAnalysisToProjectRisks(analysis) {
    if (!analysis || !analysis.id) return;
    if (!window.appData.projectRisks) window.appData.projectRisks = [];
    var pr = window.appData.projectRisks;
    var existing = pr.find(function(r) { return r.sourceAnalysisId === analysis.id; });
    // Determine status from global light
    var prStatus = 'todo';
    if (analysis.meta && analysis.meta.status === 'complete' && analysis.results) {
      var gl = analysis.results.globalLight;
      if (gl === 'green') prStatus = 'green';
      else if (gl === 'yellow') prStatus = 'yellow';
      else if (gl === 'red') prStatus = 'red';
    }
    // Build recommendations from top 3
    var recs = [];
    if (analysis.results && analysis.results.recommendations) {
      analysis.results.recommendations.slice(0, 3).forEach(function(rec) {
        recs.push({
          id: 'proj-reco-' + Math.random().toString(36).slice(2, 10),
          title: rec.title || '',
          text: rec.rationale || '',
          implemented: false
        });
      });
    }
    // Format date
    var updatedAt = (analysis.meta && analysis.meta.updatedAt) || _now();
    var dateStr = '';
    try { dateStr = new Date(updatedAt).toISOString().slice(0, 10); } catch(e) { dateStr = ''; }
    if (existing) {
      // Update existing entry (preserve user-edited fields: beneficiary, tags additions)
      existing.projectName = (analysis.meta && analysis.meta.title) || 'Sans titre';
      existing.reportNumber = (analysis.meta && analysis.meta.reportNumber) || '';
      existing.description = (analysis.context && (analysis.context.perimeter || analysis.context.description)) || '';
      existing.completionDate = dateStr;
      existing.status = prStatus;
      existing.recommendations = mergeProjectRecommendations(existing.recommendations, recs);
      existing.nextReviewDate = (analysis.meta && analysis.meta.nextReviewDate) || null;
      // Keep beneficiary and user-edited priority
    } else {
      // Create new entry
      pr.push({
        id: 'proj-risk-' + Math.random().toString(36).slice(2, 10),
        projectName: (analysis.meta && analysis.meta.title) || 'Sans titre',
        beneficiary: '',
        reportNumber: (analysis.meta && analysis.meta.reportNumber) || '',
        revisionNumber: 0,
        description: (analysis.context && (analysis.context.perimeter || analysis.context.description)) || '',
        analysisLink: '#risk-analysis:' + analysis.id,
        completionDate: dateStr,
        status: prStatus,
        priority: '',
        recommendations: recs,
        sourceAnalysisId: analysis.id,
        nextReviewDate: (analysis.meta && analysis.meta.nextReviewDate) || null,
        note: ''
      });
    }
    // M1 : persister l'onglet project-risks muté (mode serveur).
    _persistAffected(['projectRisks']);
  }

  function removeProjectRiskForAnalysis(analysisId) {
    if (!window.appData.projectRisks) return;
    window.appData.projectRisks = window.appData.projectRisks.filter(function(r) {
      return r.sourceAnalysisId !== analysisId;
    });
    // M1 : persister la suppression du risque projet miroir (mode serveur).
    _persistAffected(['projectRisks']);
  }

  // ============================================================
  // WIZARD RENDER
  // ============================================================
  function renderWizard(container) {
    var a = findAnalysis(wizardState.analysisId);
    if (!a) { wizardState.analysisId = null; renderList(container); return; }
    var html = '<div class="ra-wizard">';
    // Sticky top wrapper
    html += '<div class="ra-wizard__sticky-top">';
    // Header
    html += '<div class="ra-wizard__header"><h3>' + _esc(a.meta.title || 'Analyse') + '</h3>';
    html += '<button class="btn btn--primary btn--sm ra-back-btn" onclick="raCloseWizard()"><i class="fas fa-arrow-left"></i> ' + _esc(_t('riskAnalysis.nav.backToList','Retour à la liste')) + '</button></div>';
    // Progress
    html += '<div class="ra-wizard__progress">';
    var stepLabels = [_t('riskAnalysis.steps.context','Contexte'),_t('riskAnalysis.steps.cia','CIA'),_t('riskAnalysis.steps.continuity','Continuité'),_t('riskAnalysis.steps.architecture','Architecture'),_t('riskAnalysis.steps.controls','Contrôles'),_t('riskAnalysis.steps.risks','Risques'),_t('riskAnalysis.steps.mesures','Mesures & Revue'),_t('riskAnalysis.steps.results','Résultats'),_t('riskAnalysis.steps.preview','Prévisualisation'),_t('riskAnalysis.steps.exportPdf','Export')];
    for (var i = 0; i < 10; i++) {
      if (i > 0) html += '<div class="ra-step-connector' + (i <= wizardState.step ? ' ra-step-connector--completed' : '') + '"></div>';
      var dotClass = i === wizardState.step ? 'ra-step-dot--active' : i < wizardState.step ? 'ra-step-dot--completed' : '';
      var labelClass = i === wizardState.step ? ' ra-step-label--active' : '';
      html += '<div class="ra-step-item" onclick="raGoStep(' + i + ')"><div class="ra-step-dot ' + dotClass + '">' + (i+1) + '</div><span class="ra-step-label' + labelClass + '">' + _esc(stepLabels[i]) + '</span></div>';
    }
    html += '</div>';
    html += '</div>'; // close sticky-top
    // Body
    html += '<div class="ra-wizard__body" id="raWizardBody"></div>';
    // Footer
    html += '<div class="ra-wizard__footer">';
    html += wizardState.step > 0 ? '<button class="btn btn--sm" onclick="raGoStep(' + (wizardState.step-1) + ')"><i class="fas fa-chevron-left"></i> ' + _esc(_t('riskAnalysis.nav.previous','Précédent')) + '</button>' : '<span></span>';
    html += wizardState.step < 9 ? '<button class="btn btn--primary btn--sm" onclick="raGoStep(' + (wizardState.step+1) + ')">' + _esc(_t('riskAnalysis.nav.next','Suivant')) + ' <i class="fas fa-chevron-right"></i></button>' : '<button class="btn btn--primary btn--sm" onclick="raFinish()">' + _esc(_t('riskAnalysis.nav.finish','Terminer')) + '</button>';
    html += '</div></div>';
    container.innerHTML = html;
    renderStepContent(a);
  }

  window.raCloseWizard = function() {
    saveCurrentStep();
    wizardState.analysisId = null;
    wizardState.step = 0;
    window.renderRiskAnalysisModule();
  };
  window.raGoStep = function(s) {
    saveCurrentStep();
    var a = findAnalysis(wizardState.analysisId);
    // Validate step 0: perimeter is required before advancing
    if (wizardState.step === 0 && s > 0 && a && !a.context.perimeter) {
      if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.perimeterRequired','Le périmètre est obligatoire.'), 'warning');
      return;
    }
    wizardState.step = _clamp(s, 0, 9);
    if (a && (wizardState.step === 7 || wizardState.step === 8)) {
      a.results = computeAllResults(a);
      syncAnalysisToProjectRisks(a);
      _save();
    }
    window.renderRiskAnalysisModule();
  };
  window.raFinish = function() {
    saveCurrentStep();
    var a = findAnalysis(wizardState.analysisId);
    if (a) { a.meta.status = 'complete'; a.meta.updatedAt = _now(); a.results = computeAllResults(a); syncAnalysisToProjectRisks(a); _save(); if (typeof window.updateDashboard === 'function') window.updateDashboard(); }
    wizardState.analysisId = null; wizardState.step = 0;
    if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.saved','Analyse sauvegardée.'), 'success');
    window.renderRiskAnalysisModule();
  };

  // ============================================================
  // STEP RENDERING
  // ============================================================
  function renderStepContent(a) {
    var body = document.getElementById('raWizardBody');
    if (!body) return;
    body.classList.remove('ra-step-slide-in');
    void body.offsetWidth;
    body.classList.add('ra-step-slide-in');
    var readOnly = typeof window.isReadOnlyMode === 'function' && window.isReadOnlyMode();
    var dis = readOnly ? ' disabled' : '';
    switch (wizardState.step) {
      case 0: renderStepContext(body, a, dis); break;
      case 1: renderStepCIA(body, a, dis); break;
      case 2: renderStepContinuity(body, a, dis); break;
      case 3: renderStepArchitecture(body, a, dis); break;
      case 4: renderStepControls(body, a, dis); break;
      case 5: renderStepRisks(body, a, dis); break;
      case 6: renderStepMesures(body, a); break;
      case 7: renderStepResults(body, a); break;
      case 8: renderStepPreview(body, a); break;
      case 9: renderStepExport(body, a); break;
    }
  }

  // Step 0: Context
  function renderStepContext(body, a, dis) {
    var c = a.context || {};
    var ident = a.identity || {};
    // Référentiel source du catalogue de risques/scénarios (demande Grandmaison D3d)
    var _raFrameworks = ['OWASP Top 10', 'OWASP Mobile Top 10', 'STRIDE', 'EBIOS RM', 'ISO/IEC 27005', 'NIST SP 800-30', 'MITRE ATT&CK'];
    var _raFwMatched = false;
    var _raFwOptions = '<option value="">' + _esc(_t('riskAnalysis.context.sourceFrameworkNone', '— Non précisé —')) + '</option>';
    for (var _raI = 0; _raI < _raFrameworks.length; _raI++) {
      var _raFw = _raFrameworks[_raI];
      var _raSel = (c.sourceFramework === _raFw);
      if (_raSel) _raFwMatched = true;
      _raFwOptions += '<option value="' + _esc(_raFw) + '"' + (_raSel ? ' selected' : '') + '>' + _esc(_raFw) + '</option>';
    }
    if (c.sourceFramework && !_raFwMatched) {
      _raFwOptions += '<option value="' + _esc(c.sourceFramework) + '" selected>' + _esc(c.sourceFramework) + '</option>';
    }
    body.innerHTML =
      '<h3>' + _esc(_t('riskAnalysis.identity.sectionTitle','Identité du projet')) + '</h3>' +
      fg(_t('riskAnalysis.identity.beneficiary','Nom du bénéficiaire/client'), '<input type="text" id="raBeneficiary" value="' + _esc(ident.beneficiary) + '"' + dis + '>') +
      fg(_t('riskAnalysis.identity.controlBody','Nom de l\'organisme de contrôle'), '<input type="text" id="raControlBody" value="' + _esc(ident.controlBody) + '"' + dis + '>') +
      fg(_t('riskAnalysis.identity.projectManager','Nom du chef de projet'), '<input type="text" id="raProjectManager" value="' + _esc(ident.projectManager) + '"' + dis + '>') +
      fg(_t('riskAnalysis.identity.encoder','Encodage de l\'analyse (Nom - Prénom)'), '<input type="text" id="raEncoder" value="' + _esc(ident.encoder) + '"' + dis + '>') +
      fg(_t('riskAnalysis.identity.validator','Validation de l\'analyse (Nom - Prénom)'), '<input type="text" id="raValidator" value="' + _esc(ident.validator) + '"' + dis + '>') +
      '<hr class="ra-section-divider" style="margin:var(--spacing-lg) 0;border:none;border-top:1px solid var(--color-border,#e9ecef)">' +
      '<h3>' + _esc(_t('riskAnalysis.steps.context','Contexte')) + '</h3>' +
      '<p class="ra-form-hint">' + _esc(_t('riskAnalysis.context.subtitle','Décrivez le contexte et le périmètre de l\'analyse.')) + '</p>' +
      fg(_t('riskAnalysis.context.titleField','Titre'), '<input type="text" id="raTitle" value="' + _esc(a.meta.title) + '"' + dis + '>', true) +
      fg(_t('riskAnalysis.context.perimeter','Périmètre'), '<textarea id="raPerimeter" placeholder="' + _esc(_t('riskAnalysis.context.perimeterPlaceholder','Ex: Application web de gestion des stocks, serveurs et bases de données.')) + '"' + dis + '>' + _esc(c.perimeter) + '</textarea>', true, _t('riskAnalysis.context.perimeterHint','')) +
      fg(_t('riskAnalysis.context.description','Description'), '<textarea id="raDesc" placeholder="' + _esc(_t('riskAnalysis.context.descriptionPlaceholder','Ex: Analyse réalisée dans le cadre de la mise en conformité ISO 27001.')) + '"' + dis + '>' + _esc(c.description) + '</textarea>') +
      fg(_t('riskAnalysis.context.stakeholders','Parties prenantes'), '<input type="text" id="raStake" value="' + _esc(c.stakeholders) + '"' + dis + '>', false, _t('riskAnalysis.context.stakeholdersHint','')) +
      fg(_t('riskAnalysis.context.sourceFramework','Référentiel source'), '<select id="raSourceFramework"' + dis + '>' + _raFwOptions + '</select>', false, _t('riskAnalysis.context.sourceFrameworkHint','Base du catalogue de risques/scénarios (OWASP, STRIDE, EBIOS…).')) +
      fg(_t('riskAnalysis.context.dataSensitivity','Sensibilité'), '<select id="raSensitivity"' + dis + '><option value="low"' + (c.dataSensitivity==='low'?' selected':'') + '>' + _esc(_t('riskAnalysis.context.sensitivityLow','Faible')) + '</option><option value="medium"' + (c.dataSensitivity==='medium'?' selected':'') + '>' + _esc(_t('riskAnalysis.context.sensitivityMedium','Moyenne')) + '</option><option value="high"' + (c.dataSensitivity==='high'?' selected':'') + '>' + _esc(_t('riskAnalysis.context.sensitivityHigh','Élevée')) + '</option></select>', true);
    // Impact matrix selector (only if module available)
    if (window.CSI_ImpactMatrices) {
      var matrices = CSI_ImpactMatrices.listMatrices();
      if (matrices.length > 0) {
        var matOpts = '<option value="">' + _esc(_t('riskAnalysis.impactMatrix.noMatrix','Aucune (évaluation manuelle 1-5)')) + '</option>';
        var activeM = CSI_ImpactMatrices.getActiveMatrix();
        var selectedMatId = a.impactMatrixId || (activeM ? activeM.id : '');
        matrices.forEach(function(m) {
          matOpts += '<option value="' + _esc(m.id) + '"' + (m.id === selectedMatId ? ' selected' : '') + '>' + _esc(m.name) + '</option>';
        });
        body.innerHTML += fg(
          _t('riskAnalysis.impactMatrix.selectMatrix','Matrice d\'impact'),
          '<select id="raMatrixSelect"' + dis + '>' + matOpts + '</select>',
          false,
          _t('riskAnalysis.impactMatrix.selectHint','Choisissez une matrice pour évaluer l\'impact de chaque risque selon plusieurs dimensions (ex: financier, réputation, juridique…). Si aucune n\'est sélectionnée, l\'impact sera évalué sur une simple échelle de 1 à 5.')
        );
      } else {
        body.innerHTML += '<div class="ra-info-hint" style="margin-top:var(--spacing-md);padding:var(--spacing-sm) var(--spacing-md);background:var(--color-surface-secondary);border-radius:var(--border-radius-md,8px);font-size:0.85rem;color:var(--color-text-light)"><i class="fas fa-info-circle" style="margin-right:6px;color:var(--color-primary)"></i>' + _esc(_t('riskAnalysis.impactMatrix.configureHint','Vous pouvez configurer des matrices d\'impacts dans la section dédiée pour évaluer les risques selon plusieurs dimensions.')) + '</div>';
      }
    }
  }

  function fg(label, input, required, hint) {
    return '<div class="ra-form-group"><label class="ra-form-label">' + _esc(label) + (required ? ' <span class="ra-required">*</span>' : '') + '</label>' + (hint ? '<div class="ra-form-hint">' + _esc(hint) + '</div>' : '') + input + '</div>';
  }

  // Step 1: CIA
  function renderStepCIA(body, a, dis) {
    var cia = a.cia || {};
    var dims = [
      { key: 'confidentiality', label: _t('riskAnalysis.cia.confidentiality','Confidentialité'), icon: 'fas fa-lock' },
      { key: 'integrity', label: _t('riskAnalysis.cia.integrity','Intégrité'), icon: 'fas fa-shield-halved' },
      { key: 'availability', label: _t('riskAnalysis.cia.availability','Disponibilité'), icon: 'fas fa-clock' }
    ];
    var levels = [
      { v: 1, label: _t('riskAnalysis.cia.level1','Faible'), desc: _t('riskAnalysis.cia.level1Desc','Impact limité.') },
      { v: 2, label: _t('riskAnalysis.cia.level2','Moyen'), desc: _t('riskAnalysis.cia.level2Desc','Impact significatif.') },
      { v: 3, label: _t('riskAnalysis.cia.level3','Élevé'), desc: _t('riskAnalysis.cia.level3Desc','Impact critique.') }
    ];
    var html = '<h3>' + _esc(_t('riskAnalysis.cia.title','Classification CIA')) + '</h3><p class="ra-form-hint">' + _esc(_t('riskAnalysis.cia.subtitle','')) + '</p><div class="ra-cia-grid">';
    dims.forEach(function(d) {
      html += '<div class="ra-cia-card"><i class="' + d.icon + '"></i><h4>' + _esc(d.label) + '</h4><div class="ra-cia-options">';
      levels.forEach(function(l) {
        var sel = (cia[d.key] === l.v) ? ' ra-cia-option--selected' : '';
        html += '<div class="ra-cia-option' + sel + '" data-cia="' + d.key + '" data-val="' + l.v + '" onclick="raCiaSelect(this)"' + (dis ? ' style="pointer-events:none;opacity:0.6"' : '') + '><strong>' + _esc(l.label) + '</strong> — ' + _esc(l.desc) + '</div>';
      });
      html += '</div></div>';
    });
    html += '</div>' + fg(_t('riskAnalysis.cia.justification','Justification'), '<textarea id="raCiaJustif"' + dis + '>' + _esc(cia.justification || '') + '</textarea>');
    // RGPD block
    var gdpr = a.gdpr || {};
    html += '<hr class="ra-section-divider">';
    html += '<h3><i class="fas fa-user-shield"></i> ' + _esc(_t('riskAnalysis.gdpr.title','RGPD — Protection des données')) + '</h3>';
    html += '<p class="ra-form-hint">' + _esc(_t('riskAnalysis.gdpr.subtitle','Évaluez les implications en matière de protection des données personnelles.')) + '</p>';
    html += '<div class="ra-gdpr-block">';
    html += '<div class="ra-gdpr-item"><label><input type="checkbox" id="raGdprPersonal"' + (gdpr.personalData ? ' checked' : '') + dis + '> ' + _esc(_t('riskAnalysis.gdpr.personalData','Présence de données à caractère personnel')) + '</label>';
    html += '<p class="ra-gdpr-explain">' + _esc(_t('riskAnalysis.gdpr.personalDataExplain','Toute information se rapportant à une personne physique identifiée ou identifiable (article 4 RGPD).')) + '</p></div>';
    html += '<div class="ra-gdpr-item"><label><input type="checkbox" id="raGdprSensitive"' + (gdpr.sensitiveData ? ' checked' : '') + dis + '> ' + _esc(_t('riskAnalysis.gdpr.sensitiveData','Présence de données à caractère sensible')) + '</label>';
    html += '<p class="ra-gdpr-explain">' + _esc(_t('riskAnalysis.gdpr.sensitiveDataExplain','Données révélant l\'origine raciale/ethnique, opinions politiques, données de santé, etc. (article 9 RGPD).')) + '</p></div>';
    html += '<div class="ra-gdpr-item"><label>' + _esc(_t('riskAnalysis.gdpr.processingRegistry','Registre des traitements réalisé pour ce projet ?')) + '</label>';
    html += '<select id="raGdprRegistry"' + dis + '>';
    html += '<option value="no"' + (gdpr.processingRegistry === 'no' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.gdpr.registryNo','Non')) + '</option>';
    html += '<option value="yes"' + (gdpr.processingRegistry === 'yes' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.gdpr.registryYes','Oui')) + '</option>';
    html += '<option value="planned"' + (gdpr.processingRegistry === 'planned' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.gdpr.registryPlanned','Planifié')) + '</option>';
    html += '</select>';
    html += '<p class="ra-gdpr-explain">' + _esc(_t('riskAnalysis.gdpr.processingRegistryExplain','Le registre des activités de traitement (article 30 RGPD) est obligatoire pour les organismes de plus de 250 salariés.')) + '</p></div>';
    html += '</div>';
    body.innerHTML = html;
  }
  window.raCiaSelect = function(el) {
    var key = el.getAttribute('data-cia'), val = parseInt(el.getAttribute('data-val'));
    el.parentNode.querySelectorAll('.ra-cia-option').forEach(function(o) { o.classList.remove('ra-cia-option--selected'); });
    el.classList.add('ra-cia-option--selected');
    var a = findAnalysis(wizardState.analysisId);
    if (a) { if (!a.cia) a.cia = {}; a.cia[key] = val; _save(); }
  };

  // Step 2: Continuity
  function renderStepContinuity(body, a, dis) {
    var c = a.continuity || {};
    function rtoLabel(h) { return h <= 4 ? _t('riskAnalysis.continuity.strict','Très strict') : h <= 8 ? _t('riskAnalysis.continuity.moderate','Strict') : h <= 24 ? _t('riskAnalysis.continuity.standard','Standard') : _t('riskAnalysis.continuity.relaxed','Souple'); }
    function rtoClass(h) { return h <= 8 ? 'strict' : h <= 24 ? 'moderate' : 'relaxed'; }
    body.innerHTML = '<h3>' + _esc(_t('riskAnalysis.continuity.title','Continuité')) + '</h3><p class="ra-form-hint">' + _esc(_t('riskAnalysis.continuity.subtitle','')) + '</p><div class="ra-rto-grid">' +
      '<div class="ra-rto-card"><h4>RTO</h4>' + fg(_t('riskAnalysis.continuity.rtoLabel','RTO (heures)'), '<input type="number" id="raRto" min="0" value="' + (c.rtoHours||24) + '" oninput="raUpdateRtoIndicator()"' + dis + '>', false, _t('riskAnalysis.continuity.rtoHint','')) + '<div id="raRtoInd" class="ra-rto-indicator ra-rto-indicator--' + rtoClass(c.rtoHours||24) + '">' + _esc(rtoLabel(c.rtoHours||24)) + '</div></div>' +
      '<div class="ra-rto-card"><h4>RPO</h4>' + fg(_t('riskAnalysis.continuity.rpoLabel','RPO (heures)'), '<input type="number" id="raRpo" min="0" value="' + (c.rpoHours||24) + '" oninput="raUpdateRtoIndicator()"' + dis + '>', false, _t('riskAnalysis.continuity.rpoHint','')) + '<div id="raRpoInd" class="ra-rto-indicator ra-rto-indicator--' + rtoClass(c.rpoHours||24) + '">' + _esc(rtoLabel(c.rpoHours||24)) + '</div></div>' +
      '</div>' +
      '<div class="ra-form-group" style="margin-top:var(--spacing-md)"><label class="ra-form-label">' + _esc(_t('riskAnalysis.continuity.commentLabel','Commentaire continuité')) + '</label>' +
      '<textarea id="raContinuityComment" rows="3" class="ra-form-textarea" placeholder="' + _esc(_t('riskAnalysis.continuity.commentPlaceholder','Justification des objectifs RTO/RPO, contraintes métier...')) + '"' + dis + '>' + _esc(c.comment || '') + '</textarea></div>';
  }
  window.raUpdateRtoIndicator = function() {
    function upd(inputId, indId) {
      var el = document.getElementById(inputId);
      var ind = document.getElementById(indId);
      if (!el || !ind) return;
      var v = parseInt(el.value) || 24;
      var cls = v <= 8 ? 'strict' : v <= 24 ? 'moderate' : 'relaxed';
      var lbl = v <= 4 ? _t('riskAnalysis.continuity.strict','Très strict') : v <= 8 ? _t('riskAnalysis.continuity.moderate','Strict') : v <= 24 ? _t('riskAnalysis.continuity.standard','Standard') : _t('riskAnalysis.continuity.relaxed','Souple');
      ind.className = 'ra-rto-indicator ra-rto-indicator--' + cls;
      ind.textContent = lbl;
    }
    upd('raRto','raRtoInd'); upd('raRpo','raRpoInd');
  };

  // Step 3: Documentation (was Architecture)
  function renderStepArchitecture(body, a, dis) {
    var arch = a.architecture || {};
    var iam = arch.iam || {};
    body.innerHTML = '<h3>' + _esc(_t('riskAnalysis.architecture.title','Documentation')) + '</h3>' +
      '<p class="ra-form-hint">' + _esc(_t('riskAnalysis.architecture.subtitle','Documentation technique et gestion des identités.')) + '</p>' +
      '<h4 class="ra-subsection-title">' + _esc(_t('riskAnalysis.architecture.techArchTitle','Architecture technique')) + '</h4>' +
      fg(_t('riskAnalysis.architecture.hosting','Hébergement'), '<select id="raHosting"' + dis + '><option value="onprem"' + (arch.hosting==='onprem'?' selected':'') + '>' + _esc(_t('riskAnalysis.architecture.hostingOnprem','On-premise')) + '</option><option value="cloud"' + (arch.hosting==='cloud'?' selected':'') + '>' + _esc(_t('riskAnalysis.architecture.hostingCloud','Cloud')) + '</option><option value="hybrid"' + (arch.hosting==='hybrid'?' selected':'') + '>' + _esc(_t('riskAnalysis.architecture.hostingHybrid','Hybride')) + '</option></select>') +
      '<div class="ra-checkbox-group"><input type="checkbox" id="raInternet"' + (arch.internetExposed?' checked':'') + dis + '><label for="raInternet">' + _esc(_t('riskAnalysis.architecture.internetExposed','Exposé Internet')) + '</label></div>' +
      '<h4 class="ra-subsection-title">' + _esc(_t('riskAnalysis.architecture.iamTitle','Gestion des identités (IAM)')) + '</h4>' +
      '<div class="ra-checkbox-group"><input type="checkbox" id="raSso"' + (iam.sso?' checked':'') + dis + '><label for="raSso">' + _esc(_t('riskAnalysis.architecture.sso','SSO')) + '</label></div>' +
      '<div class="ra-checkbox-group"><input type="checkbox" id="raMfa"' + (iam.mfa?' checked':'') + dis + '><label for="raMfa">' + _esc(_t('riskAnalysis.architecture.mfa','MFA')) + '</label></div>' +
      '<div class="ra-checkbox-group"><input type="checkbox" id="raAdminSep"' + (iam.adminSeparation?' checked':'') + dis + '><label for="raAdminSep">' + _esc(_t('riskAnalysis.architecture.adminSeparation','Séparation admin')) + '</label></div>' +
      '<h4 class="ra-subsection-title">' + _esc(_t('riskAnalysis.architecture.architectureDocTitle','Architecture')) + '</h4>' +
      '<div class="ra-checkbox-group"><input type="checkbox" id="raArchDoc"' + (arch.architectureDoc?' checked':'') + dis + '><label for="raArchDoc">' + _esc(_t('riskAnalysis.architecture.architectureDoc','Dossier d\'architecture complété')) + '</label></div>';
  }

  // Step 4: Controls
  function renderStepControls(body, a, dis) {
    if (!a.controls || Object.keys(a.controls).length === 0) {
      a.controls = {};
      RA_CONTROLS_SEED.forEach(function(q) { a.controls[q.id] = { status: 'no', comment: '' }; });
    }
    var html = '<h3>' + _esc(_t('riskAnalysis.controls.title','Contrôles')) + '</h3><p class="ra-form-hint">' + _esc(_t('riskAnalysis.controls.subtitle','')) + '</p>';
    RA_DOMAINS.forEach(function(d) {
      var qs = RA_CONTROLS_SEED.filter(q => q.domainId === d.id);
      var answered = qs.filter(q => a.controls[q.id] && a.controls[q.id].status !== 'no').length;
      var progressClass = answered === qs.length ? ' ra-domain-progress--complete' : '';
      html += '<div class="ra-domain-section"><div class="ra-domain-header" onclick="raToggleDomain(this, event)">';
      html += '<div class="ra-domain-header__left"><i class="' + d.icon + '"></i> ' + _esc(_t('riskAnalysis.domains.' + d.id, _bi(d, 'label', 'labelEn'))) + '</div>';
      html += '<div><span class="ra-domain-progress' + progressClass + '">' + answered + '/' + qs.length + '</span> <i class="fas fa-chevron-down ra-domain-chevron"></i></div>';
      html += '</div><div class="ra-domain-body">';
      qs.forEach(function(q) {
        var ctrl = a.controls[q.id] || { status: 'no', comment: '' };
        var statuses = [
          { key: 'implemented', label: _t('riskAnalysis.controls.implemented','Implémenté'), cls: 'ra-answer-btn--implemented' },
          { key: 'partial', label: _t('riskAnalysis.controls.partial','Partiel'), cls: 'ra-answer-btn--partial' },
          { key: 'planned', label: _t('riskAnalysis.controls.planned','Planifié'), cls: 'ra-answer-btn--planned' },
          { key: 'toVerify', label: _t('riskAnalysis.controls.toVerify','À vérifier'), cls: 'ra-answer-btn--toVerify' },
          { key: 'no', label: _t('riskAnalysis.controls.no','Non'), cls: 'ra-answer-btn--no' },
          { key: 'na', label: _t('riskAnalysis.controls.na','N/A'), cls: 'ra-answer-btn--na' }
        ];
        html += '<div class="ra-question"><div class="ra-question__text">' + _esc(_bi(q, 'text', 'textEn')) + '</div>';
        if (q.hint || q.hintEn) html += '<div class="ra-question__hint">' + _esc(_bi(q, 'hint', 'hintEn')) + '</div>';
        html += '<div class="ra-answer-group">';
        statuses.forEach(function(s) {
          var sel = ctrl.status === s.key ? ' ra-answer-btn--selected' : '';
          html += '<button class="ra-answer-btn ' + s.cls + sel + '" onclick="raSetControl(\'' + q.id + '\',\'' + s.key + '\',this)"' + dis + '>' + _esc(s.label) + '</button>';
        });
        html += '</div>';
        html += '<div class="ra-question__comment"><textarea placeholder="' + _esc(_t('riskAnalysis.controls.comment','Commentaire')) + '" data-ctrl="' + q.id + '" onchange="raSetComment(\'' + q.id + '\',this.value)"' + dis + '>' + _esc(ctrl.comment || '') + '</textarea></div>';
        html += '</div>';
      });
      html += '</div></div>';
    });
    body.innerHTML = html;
  }
  window.raToggleDomain = function(el, e) {
    if (e && e.target && e.target.closest && e.target.closest('.ra-answer-btn, button, textarea, input')) return;
    el.classList.toggle('ra-domain-header--expanded');
    var b = el.nextElementSibling;
    b.classList.toggle('ra-domain-body--visible');
  };
  window.raSetControl = function(qId, status, btn) {
    var a = findAnalysis(wizardState.analysisId); if (!a) return;
    if (!btn) return;
    if (!a.controls[qId]) a.controls[qId] = { status: 'no', comment: '' };
    a.controls[qId].status = status;
    // Auto-comment for planned status
    if (status === 'planned' && !a.controls[qId].comment) {
      var autoComment = _t('riskAnalysis.controls.plannedAutoComment', '[Planifié] Implémentation prévue — justification : ');
      a.controls[qId].comment = autoComment;
      var ta = btn.closest('.ra-question').querySelector('textarea[data-ctrl="' + qId + '"]');
      if (ta) ta.value = autoComment;
    }
    _save();
    btn.parentNode.querySelectorAll('.ra-answer-btn').forEach(function(b) { b.classList.remove('ra-answer-btn--selected'); });
    btn.classList.add('ra-answer-btn--selected');
    // Update progress
    var domId = RA_CONTROLS_SEED.find(q => q.id === qId).domainId;
    var section = btn.closest('.ra-domain-section');
    if (section) {
      var qs = RA_CONTROLS_SEED.filter(q => q.domainId === domId);
      var answered = qs.filter(q => a.controls[q.id] && a.controls[q.id].status !== 'no').length;
      var badge = section.querySelector('.ra-domain-progress');
      if (badge) { badge.textContent = answered + '/' + qs.length; badge.className = 'ra-domain-progress' + (answered === qs.length ? ' ra-domain-progress--complete' : ''); }
    }
  };
  window.raSetComment = function(qId, val) {
    var a = findAnalysis(wizardState.analysisId); if (!a) return;
    if (!a.controls[qId]) a.controls[qId] = { status: 'no', comment: '' };
    a.controls[qId].comment = val;
    _save();
  };

  // Step 5: Risks
  function renderStepRisks(body, a, dis) {
    if (!a.risks) a.risks = [];
    var selected = {};
    a.risks.forEach(function(r) { if (r.catalogId) selected[r.catalogId] = true; });
    // Auto-preselect if empty
    if (a.risks.length === 0) {
      var preselect = ['RISK_RANSOM','RISK_LOG_GAP'];
      if (a.architecture && a.architecture.internetExposed) preselect.push('RISK_EXT_ATTACK','RISK_MISCONFIG','RISK_DDOS');
      if (a.context && (a.context.dataSensitivity === 'high' || a.context.personalData)) preselect.push('RISK_DATA_LEAK','RISK_PRIVACY');
      preselect.forEach(function(cid) {
        if (!selected[cid]) {
          a.risks.push({ id: _id(), catalogId: cid, likelihood: 3, impact: 3, impactBreakdown: null, treatmentDecision: 'reduce', owner: '', notes: '' });
          selected[cid] = true;
        }
      });
    }
    // Owner datalist from identity fields + stakeholders (FIX-STAKEHOLDER-CASCADE, audit 2026-07-10)
    var ident = a.identity || {};
    var ownerNames = [ident.encoder, ident.validator, ident.projectManager, ident.beneficiary].filter(Boolean);
    ((window.appData && window.appData.stakeholders) || []).forEach(function(st) {
      if (st && st.name) ownerNames.push(st.name);
    });
    var uniqueOwners = ownerNames.filter(function(v, i, arr) { return arr.indexOf(v) === i; });
    var html = '<datalist id="raOwnerSuggestions">';
    uniqueOwners.forEach(function(n) { html += '<option value="' + _esc(n).replace(/"/g,'&quot;') + '">'; });
    html += '</datalist>';
    html += '<h3>' + _esc(_t('riskAnalysis.risks.title','Risques')) + '</h3><p class="ra-form-hint">' + _esc(_t('riskAnalysis.risks.subtitle','')) + '</p>';
    html += '<h4>' + _esc(_t('riskAnalysis.risks.selectFromCatalog','Catalogue')) + '</h4><div class="ra-risk-catalog">';
    RA_RISK_CATALOG.forEach(function(cat) {
      var sel = selected[cat.id] ? ' ra-risk-card--selected' : '';
      html += '<div class="ra-risk-card' + sel + '" onclick="raToggleRisk(\'' + cat.id + '\')"' + (dis ? ' style="pointer-events:none;opacity:0.6"' : '') + '>';
      html += '<div class="ra-risk-card__title">' + _esc(_bi(cat, 'threat', 'threatEn')) + '</div>';
      html += '<div class="ra-risk-card__scenario">' + _esc(_bi(cat, 'scenario', 'scenarioEn')) + '</div></div>';
    });
    html += '</div>';
    // Tuners for selected risks
    a.risks.forEach(function(r, idx) {
      var cat = RA_RISK_CATALOG.find(c => c.id === r.catalogId);
      var title = cat ? _bi(cat, 'threat', 'threatEn') : (r.customTitle || _t('riskAnalysis.risks.customTitle','Risque personnalisé'));
      var rl = resolveRiskLevel(r.likelihood || 3, r.impact || 3);
      html += '<div class="ra-risk-tuner"><div class="ra-risk-tuner__header">';
      if (!cat) {
        html += '<input type="text" class="ra-custom-risk-title" value="' + _esc(title).replace(/"/g,'&quot;') + '"'
             + ' placeholder="' + _esc(_t('riskAnalysis.risks.customTitlePlaceholder','Nom du risque')) + '"'
             + ' onchange="raRenameCustomRisk(' + idx + ',this.value)"'
             + ' onblur="raRenameCustomRisk(' + idx + ',this.value)"' + dis + '>';
        html += '<button type="button" class="ra-custom-risk-delete" onclick="raDeleteCustomRisk(' + idx + ')"'
             + ' title="' + _esc(_t('riskAnalysis.risks.deleteCustom','Supprimer ce risque')) + '"' + dis + '>'
             + '<i class="fas fa-trash"></i></button>';
      } else {
        html += '<h4>' + _esc(title) + '</h4>';
      }
      html += '<div class="ra-inline-score"><span class="ra-light" style="background:' + rl.color + '"></span> ' + _esc(rl.label) + '</div></div>';
      html += '<div class="ra-risk-tuner__controls">';
      html += '<div>' + fg(_t('riskAnalysis.risks.likelihood','Vraisemblance'), riskSelect('raL_' + idx, r.likelihood || 3, 'likelihood', dis)) + '</div>';
      html += '<div>' + fg(_t('riskAnalysis.risks.impact','Impact'), riskSelect('raI_' + idx, r.impact || 3, 'impact', dis)) + '</div>';
      html += '</div>';
      // Evaluate via matrix button (conditional)
      if (a.impactMatrixId && window.CSI_ImpactMatrices) {
        var bdCount = r.impactBreakdown ? Object.keys(r.impactBreakdown).length : 0;
        html += '<div class="ra-risk-tuner__row" style="margin-top:4px">';
        html += '<button class="btn btn--sm" onclick="raOpenImpactPicker(' + idx + ')"' + dis + ' title="' + _esc(_t('riskAnalysis.impactMatrix.evaluateHint','Utilisez la matrice d\'impact pour évaluer ce risque selon plusieurs dimensions')) + '"><i class="fas fa-table-cells"></i> ' + _esc(_t('riskAnalysis.impactMatrix.evaluateVia','Évaluer via matrice')) + '</button>';
        if (bdCount > 0) {
          html += '<span class="ra-matrix-badge"><i class="fas fa-check-circle"></i> ' + bdCount + ' ' + _esc(_t('riskAnalysis.impactMatrix.evaluated','dim. évaluée(s)')) + '</span>';
        }
        html += '</div>';
      }
      html += '<div class="ra-risk-tuner__row">' + fg(_t('riskAnalysis.risks.treatment','Traitement'), '<select id="raT_' + idx + '" onchange="raUpdateRiskField(' + idx + ',\'treatmentDecision\',this.value)"' + dis + '><option value="reduce"' + (r.treatmentDecision==='reduce'?' selected':'') + '>' + _esc(_t('riskAnalysis.risks.treatReduce','Réduire')) + '</option><option value="accept"' + (r.treatmentDecision==='accept'?' selected':'') + '>' + _esc(_t('riskAnalysis.risks.treatAccept','Accepter')) + '</option><option value="transfer"' + (r.treatmentDecision==='transfer'?' selected':'') + '>' + _esc(_t('riskAnalysis.risks.treatTransfer','Transférer')) + '</option><option value="avoid"' + (r.treatmentDecision==='avoid'?' selected':'') + '>' + _esc(_t('riskAnalysis.risks.treatAvoid','Éviter')) + '</option></select>') + '</div>';
      html += '<div class="ra-risk-tuner__row">' + fg(_t('riskAnalysis.risks.owner','Propriétaire du risque'), '<input type="text" id="raOwner_' + idx + '" list="raOwnerSuggestions" value="' + _esc(r.owner || '').replace(/"/g,'&quot;') + '" onchange="raUpdateRiskField(' + idx + ',\'owner\',this.value)" placeholder="' + _esc(_t('riskAnalysis.risks.ownerPlaceholder','Nom du responsable')) + '"' + dis + '>') + '</div>';
      html += '</div>';
    });
    html += '<button class="btn btn--sm" onclick="raAddCustomRisk()"' + dis + '><i class="fas fa-plus"></i> ' + _esc(_t('riskAnalysis.risks.addCustom','Ajouter un risque personnalisé')) + '</button>';
    body.innerHTML = html;
  }

  function riskSelect(id, val, type, dis) {
    var opts = '';
    for (var i = 1; i <= 5; i++) {
      var lbl = _t('riskAnalysis.risks.' + type + 'Levels.' + i, i.toString());
      opts += '<option value="' + i + '"' + (val === i ? ' selected' : '') + '>' + i + ' - ' + _esc(lbl) + '</option>';
    }
    var field = type === 'likelihood' ? 'likelihood' : 'impact';
    var idx = id.split('_')[1];
    return '<select id="' + id + '" onchange="raUpdateRiskField(' + idx + ',\'' + field + '\',parseInt(this.value))"' + dis + '>' + opts + '</select>';
  }

  window.raToggleRisk = function(catalogId) {
    var a = findAnalysis(wizardState.analysisId); if (!a || !a.risks) return;
    var idx = a.risks.findIndex(r => r.catalogId === catalogId);
    if (idx >= 0) { a.risks.splice(idx, 1); } else { a.risks.push({ id: _id(), catalogId: catalogId, likelihood: 3, impact: 3, impactBreakdown: null, treatmentDecision: 'reduce', owner: '', notes: '' }); }
    _save();
    renderStepRisks(document.getElementById('raWizardBody'), a, '');
  };
  window.raUpdateRiskField = function(idx, field, val) {
    var a = findAnalysis(wizardState.analysisId); if (!a || !a.risks[idx]) return;
    a.risks[idx][field] = val;
    _save();
    if (field === 'likelihood' || field === 'impact') {
      renderStepRisks(document.getElementById('raWizardBody'), a, '');
    }
  };
  window.raAddCustomRisk = function() {
    var a = findAnalysis(wizardState.analysisId); if (!a) return;
    a.risks.push({ id: _id(), catalogId: null, customTitle: _t('riskAnalysis.risks.customTitle','Risque personnalisé'), likelihood: 3, impact: 3, impactBreakdown: null, treatmentDecision: 'reduce', owner: '', notes: '' });
    _save();
    renderStepRisks(document.getElementById('raWizardBody'), a, '');
  };
  window.raRenameCustomRisk = function(idx, value) {
    var a = findAnalysis(wizardState.analysisId);
    if (!a || !a.risks[idx]) return;
    var trimmed = (value || '').trim();
    if (!trimmed) trimmed = _t('riskAnalysis.risks.customTitle', 'Risque personnalisé');
    a.risks[idx].customTitle = trimmed;
    _save();
  };
  window.raDeleteCustomRisk = function(idx) {
    var a = findAnalysis(wizardState.analysisId);
    if (!a || !a.risks[idx]) return;
    var riskId = a.risks[idx].id;
    showConfirmModal(
      _t('riskAnalysis.risks.confirmDeleteCustom', 'Supprimer ce risque personnalisé ?'),
      _t('riskAnalysis.risks.confirmDeleteCustomMsg', 'Ce risque sera retiré de l\'analyse.'),
      function() {
        a.risks = a.risks.filter(function(r) { return r.id !== riskId; });
        _save();
        renderStepRisks(document.getElementById('raWizardBody'), a, '');
      },
      { type: 'danger' }
    );
  };

  // Step 6: Mesures & Revue
  function renderStepMesures(body, a) {
    var html = '<h3>' + _esc(_t('riskAnalysis.steps.mesures','Mesures & Revue')) + '</h3>' +
      '<p class="ra-form-hint">' + _esc(_t('riskAnalysis.mesures.subtitle','Définissez vos recommandations clefs et planifiez la prochaine revue.')) + '</p>';
    // Key Recommendations (finalRecs) — editable section
    var frItems = (a.finalRecs && a.finalRecs.items) || [];
    html += '<div class="ra-finalrecs-section">';
    html += '<h4 class="ra-section-title"><i class="fas fa-star"></i> ' + _esc(_t('riskAnalysis.finalRecs.title','Recommandations clefs')) + ' <span class="ra-finalrecs-counter">(' + frItems.length + '/10)</span></h4>';
    frItems.forEach(function(item, idx) {
      html += '<div class="ra-finalrec-card" data-idx="' + idx + '">';
      html += '<div class="ra-finalrec-card__header"><strong>#' + (idx + 1) + '</strong>';
      html += '<div class="ra-finalrec-card__actions">';
      html += '<select id="raFrStatus_' + idx + '" onchange="raUpdateFinalRecField(' + idx + ',\'status\',this.value)">';
      html += '<option value="todo"' + (item.status !== 'done' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.finalRecs.statusTodo','À faire')) + '</option>';
      html += '<option value="done"' + (item.status === 'done' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.finalRecs.statusDone','Fait')) + '</option></select>';
      html += '<button class="btn btn--danger btn--sm" onclick="raRemoveFinalRec(' + idx + ')" title="' + _esc(_t('common.delete', 'Delete')) + '"><i class="fas fa-trash"></i></button>';
      html += '</div></div>';
      html += '<input type="text" id="raFrTitle_' + idx + '" value="' + _esc(item.title || '') + '" placeholder="' + _esc(_t('riskAnalysis.finalRecs.titlePlaceholder','Titre de la recommandation')) + '" onchange="raUpdateFinalRecField(' + idx + ',\'title\',this.value)">';
      html += '<textarea id="raFrDetails_' + idx + '" placeholder="' + _esc(_t('riskAnalysis.finalRecs.detailsPlaceholder','Détails, actions, contexte...')) + '" onchange="raUpdateFinalRecField(' + idx + ',\'details\',this.value)">' + _esc(item.details || '') + '</textarea>';
      html += '</div>';
    });
    if (frItems.length < 10) {
      html += '<button class="btn btn--sm" onclick="raAddFinalRec()"><i class="fas fa-plus"></i> ' + _esc(_t('riskAnalysis.finalRecs.add','Ajouter une recommandation clef')) + '</button>';
    }
    html += '</div>';
    // Review date section
    var reviewEnabled = !!(a.meta && a.meta.nextReviewDate);
    html += '<div class="ra-review-date-section">';
    html += '<h4><i class="fas fa-calendar-check"></i> ' + _esc(_t('riskAnalysis.reviewDate.title','Prochaine revue')) + '</h4>';
    html += '<label class="settings-checkbox"><input type="checkbox" id="raReviewEnabled"' + (reviewEnabled ? ' checked' : '') + ' onchange="raToggleReviewDate(this.checked)"> ' + _esc(_t('riskAnalysis.reviewDate.enable','Activer la date de prochaine revue')) + '</label>';
    html += '<div id="raReviewDateWrap" style="display:' + (reviewEnabled ? 'block' : 'none') + ';margin-top:8px">';
    html += '<input type="date" id="raNextReview" value="' + _esc((a.meta && a.meta.nextReviewDate) || '') + '">';
    html += '</div></div>';
    body.innerHTML = html;
  }

  // Step 7: Results
  function renderStepResults(body, a) {
    var r = a.results;
    if (!r) { body.innerHTML = '<p>' + _esc(_t('riskAnalysis.results.computing','Calcul...')) + '</p>'; return; }
    var html = '<h3>' + _esc(_t('riskAnalysis.results.title','Résultats')) + '</h3>' +
      '<p class="ra-form-hint">' + _esc(_t('riskAnalysis.results.subtitle','Synthèse automatique de votre analyse de risques.')) + '</p>';
    // Tab bar
    var tabDefs = [
      { id: 'synthese', label: _t('riskAnalysis.results.tabSynthese', 'Synthèse') },
      { id: 'risques', label: _t('riskAnalysis.results.tabRisques', 'Risques') },
      { id: 'recommandations', label: _t('riskAnalysis.results.tabRecommandations', 'Recommandations') }
    ];
    var activeTab = wizardState.resultTab || 'synthese';
    if (activeTab === 'mesures') activeTab = 'synthese';
    html += '<div class="ra-results-tabs">';
    tabDefs.forEach(function(tb) {
      html += '<button class="ra-results-tab' + (tb.id === activeTab ? ' ra-results-tab--active' : '') + '" data-tab="' + tb.id + '" onclick="raSetResultTab(\'' + tb.id + '\')">' + _esc(tb.label) + '</button>';
    });
    html += '</div>';
    // === Tab: Synthèse ===
    html += '<div class="ra-results-pane" data-tab="synthese" style="display:' + (activeTab === 'synthese' ? 'block' : 'none') + '">';
    // Summary
    var scoreLabel = _t('riskAnalysis.results.scoreLabels.' + r.globalScore, r.globalScore + '/5');
    var avgPercent = 0, domCount = 0;
    RA_DOMAINS.forEach(function(d) { if (r.domainScores[d.id]) { avgPercent += r.domainScores[d.id].percent; domCount++; } });
    avgPercent = domCount > 0 ? Math.round(avgPercent / domCount) : 0;
    html += '<div class="ra-results__summary"><div class="ra-global-light ra-global-light--' + r.globalLight + '">' + r.globalScore + '</div>';
    html += '<div class="ra-summary-details"><h3>' + _esc(scoreLabel) + '</h3>';
    html += '<p>' + _esc(_t('riskAnalysis.results.posture','Posture')) + ': ' + avgPercent + '%</p>';
    html += '</div></div>';
    // Planned controls warning banner
    if (r.totalPlannedControls > 0) {
      html += '<div class="ra-planned-banner"><i class="fas fa-exclamation-triangle"></i> ';
      html += _esc(_t('riskAnalysis.controls.improvedWarning', 'Le résultat d\'analyse des risques repose sur le fait que les mesures et contrôles planifiés seront correctement implémentés — il sera indispensable de réaliser une analyse de risque préalablement au lancement en production du projet.'));
      html += '</div>';
    }
    // Analyse de sécurité — Summary Panel
    if (r.spotSummary) {
      var sp = r.spotSummary;
      var spotData = a.spot || {};
      html += '<div class="ra-spot-panel">';
      html += '<h4 class="ra-section-title"><i class="fas fa-traffic-light"></i> ' + _esc(_t('riskAnalysis.spot.title','Résultats de l\'évaluation (Sécurité)')) + '</h4>';
      html += '<div class="ra-spot-header">';
      html += '<div class="ra-traffic-light-display">';
      html += '<div class="ra-tl-circle ra-tl-circle--red' + (sp.light === 'red' ? ' ra-tl-circle--active' : '') + '"></div>';
      html += '<div class="ra-tl-circle ra-tl-circle--yellow' + (sp.light === 'yellow' ? ' ra-tl-circle--active' : '') + '"></div>';
      html += '<div class="ra-tl-circle ra-tl-circle--green' + (sp.light === 'green' ? ' ra-tl-circle--active' : '') + '"></div>';
      html += '</div>';
      html += '<div class="ra-spot-avis ra-spot-avis--' + sp.light + '">' + _esc(sp.avisLabel) + '</div>';
      html += '</div>';
      html += '<table class="ra-spot-table"><tbody>';
      html += '<tr><td>' + _esc(_t('riskAnalysis.spot.inherentCriticality','Criticité de l\'actif (risque inhérent)')) + '</td><td class="ra-spot-value ra-spot-score--' + (sp.inherentCriticalityScore >= 4 ? 'red' : sp.inherentCriticalityScore === 3 ? 'yellow' : 'green') + '">' + sp.inherentCriticalityScore + '/5</td></tr>';
      html += '<tr><td>' + _esc(_t('riskAnalysis.spot.measuresPercent','Niveau d\'implémentation des mesures')) + '</td><td class="ra-spot-value ra-spot-score--' + (sp.measuresImplementationPercent >= 70 ? 'green' : sp.measuresImplementationPercent >= 40 ? 'yellow' : 'red') + '">' + sp.measuresImplementationPercent + '%</td></tr>';
      html += '<tr><td>' + _esc(_t('riskAnalysis.spot.residualScore','Niveau de risque résiduel')) + '</td><td class="ra-spot-value ra-spot-score--' + (sp.residualScore <= 2 ? 'green' : sp.residualScore === 3 ? 'yellow' : 'red') + '">' + sp.residualScore + '/5</td></tr>';
      html += '<tr><td>' + _esc(_t('riskAnalysis.spot.residualPlusOpen','Risque résiduel + risque ouvert')) + '</td><td class="ra-spot-value ra-spot-score--' + (sp.residualPlusOpenScore <= 2 ? 'green' : sp.residualPlusOpenScore === 3 ? 'yellow' : 'red') + '">' + sp.residualPlusOpenScore + '/5</td></tr>';
      html += '</tbody></table>';
      html += '<div class="ra-spot-fields">';
      html += '<div><label>' + _esc(_t('riskAnalysis.spot.openRiskAdj','Ajustement risque ouvert')) + '</label>';
      html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.spot.openRiskHint','Ajoutez un point si des risques connus ne sont pas encore traités (ex : vulnérabilités identifiées, actions en attente). Cela augmente le score résiduel final.')) + '</div>';
      html += '<select id="raSpotOpenAdj"><option value="0"' + (!spotData.openRiskAdjustment ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.spot.openRiskNone','0 - Aucun risque ouvert')) + '</option>';
      html += '<option value="1"' + (spotData.openRiskAdjustment === 1 ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.spot.openRiskModerate','1 - Quelques risques ouverts')) + '</option>';
      html += '<option value="2"' + (spotData.openRiskAdjustment === 2 ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.spot.openRiskSignificant','2 - Risques ouverts importants')) + '</option></select></div>';
      html += '<div><label>' + _esc(_t('riskAnalysis.spot.avisCommentLabel','Commentaire de l\'évaluateur')) + '</label>';
      html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.spot.avisCommentHint','Ajoutez ici vos observations ou nuances sur le résultat de l\'évaluation (ex : contexte particulier, points d\'attention).')) + '</div>';
      html += '<textarea id="raSpotAvisComment" rows="2">' + _esc(spotData.avisComment || '') + '</textarea></div>';
      html += '<div><label>' + _esc(_t('riskAnalysis.spot.avisOverrideLabel','Correction manuelle de l\'avis')) + '</label>';
      html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.spot.avisOverrideHint','Si l\'avis calculé automatiquement ne reflète pas la réalité (ex : facteurs externes à l\'analyse), vous pouvez le forcer manuellement. Laissez « — » pour conserver le calcul automatique.')) + '</div>';
      html += '<select id="raSpotAvisOverride">';
      html += '<option value=""' + (!spotData.avisOverrideLight ? ' selected' : '') + '>— (' + _esc(_t('riskAnalysis.spot.residualOverrideAuto','calcul automatique')) + ')</option>';
      html += '<option value="green"' + (spotData.avisOverrideLight === 'green' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.spot.avisLabels.green','Avis favorable')) + '</option>';
      html += '<option value="yellow"' + (spotData.avisOverrideLight === 'yellow' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.spot.avisLabels.yellow','Avis favorable moyennant la mise en œuvre des recommandations clefs')) + '</option>';
      html += '<option value="red"' + (spotData.avisOverrideLight === 'red' ? ' selected' : '') + '>' + _esc(_t('riskAnalysis.spot.avisLabels.red','Avis défavorable')) + '</option>';
      html += '</select>';
      if (sp.avisOverridden) { html += '<div class="ra-form-hint" style="color:var(--color-warning,#b37a30);margin-top:4px"><i class="fas fa-info-circle"></i> ' + _esc(_t('riskAnalysis.spot.avisOverrideActive','L\'avis automatique était : ')) + '<strong>' + _esc(sp.autoAvisLabel) + '</strong></div>'; }
      html += '</div>';
      html += '<div><label>' + _esc(_t('riskAnalysis.spot.avisOverrideJustifLabel','Justification de la correction de l\'avis')) + '</label>';
      html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.spot.avisOverrideJustifHint','Expliquez pourquoi vous corrigez l\'avis calculé (obligatoire si une correction est appliquée).')) + '</div>';
      html += '<textarea id="raSpotAvisOverrideJustif" rows="2">' + _esc(spotData.avisOverrideJustification || '') + '</textarea></div>';
      html += '<div><label>' + _esc(_t('riskAnalysis.spot.residualOverrideLabel','Correction manuelle du score résiduel')) + '</label>';
      html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.spot.residualOverrideHint','Si le score résiduel calculé automatiquement ne reflète pas la réalité, vous pouvez le remplacer manuellement. Laissez « — » pour conserver le calcul automatique.')) + '</div>';
      html += '<select id="raSpotResOverride"><option value=""' + (spotData.residualOverrideScore == null ? ' selected' : '') + '>— (' + _esc(_t('riskAnalysis.spot.residualOverrideAuto','calcul automatique')) + ')</option>';
      for (var ov = 1; ov <= 5; ov++) { html += '<option value="' + ov + '"' + (spotData.residualOverrideScore === ov ? ' selected' : '') + '>' + ov + '/5</option>'; }
      html += '</select></div>';
      html += '<div><label>' + _esc(_t('riskAnalysis.spot.residualOverrideJustifLabel','Justification de la correction')) + '</label>';
      html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.spot.residualOverrideJustifHint','Expliquez pourquoi vous corrigez le score calculé (obligatoire si une correction est appliquée).')) + '</div>';
      html += '<textarea id="raSpotResJustif" rows="2">' + _esc(spotData.residualOverrideJustification || '') + '</textarea></div>';
      html += '<div><label>' + _esc(_t('riskAnalysis.spot.residualCorrectedLabel','Commentaire sur le risque résiduel')) + '</label>';
      html += '<div class="ra-form-hint">' + _esc(_t('riskAnalysis.spot.residualCorrectedHint','Commentaire libre sur le niveau de risque résiduel après prise en compte des mesures de sécurité.')) + '</div>';
      html += '<textarea id="raSpotResCorrected" rows="2">' + _esc(spotData.residualCorrectedComment || '') + '</textarea></div>';
      html += '</div></div>';
    }
    // Domain table
    html += '<h4 class="ra-section-title"><i class="fas fa-chart-bar"></i> ' + _esc(_t('riskAnalysis.results.domainSynthesis','Synthèse')) + '</h4>';
    html += '<table class="ra-domain-table"><thead><tr><th>' + _esc(_t('riskAnalysis.domains.IAM','Domaine')) + '</th><th>' + _esc(_t('riskAnalysis.results.maturity','Maturité')) + '</th><th>%</th><th>' + _esc(_t('riskAnalysis.listColumns.light','Feu')) + '</th></tr></thead><tbody>';
    RA_DOMAINS.forEach(function(d) {
      var ds = r.domainScores[d.id] || { maturity: 0, percent: 0, light: 'grey' };
      html += '<tr><td><i class="' + d.icon + '"></i> ' + _esc(_t('riskAnalysis.domains.' + d.id, _bi(d, 'label', 'labelEn'))) + '</td>';
      html += '<td>' + ds.maturity + '/3</td>';
      html += '<td><div class="ra-maturity-bar"><div class="ra-maturity-bar__fill ra-maturity-bar__fill--' + ds.light + '" style="width:' + ds.percent + '%"></div></div> ' + ds.percent + '%';
      if (ds.plannedCount > 0) html += ' <span class="ra-planned-badge" title="' + _esc(_t('riskAnalysis.controls.plannedBadge', 'dont planifié(s)')) + '"><i class="fas fa-clock"></i> ' + ds.plannedCount + '</span>';
      html += '</td>';
      html += '<td class="ra-traffic-cell"><span class="ra-light ra-light--' + ds.light + '"></span></td></tr>';
    });
    html += '</tbody></table>';
    // Radar chart
    html += '<div class="ra-radar-wrapper"><canvas id="raRadarChart" width="400" height="400"></canvas></div>';
    html += '</div>'; // close synthese pane
    // === Tab: Risques ===
    html += '<div class="ra-results-pane" data-tab="risques" style="display:' + (activeTab === 'risques' ? 'block' : 'none') + '">';
    // Risk register
    if (a.risks && a.risks.length > 0) {
      html += '<h4 class="ra-section-title"><i class="fas fa-triangle-exclamation"></i> ' + _esc(_t('riskAnalysis.results.riskRegister','Registre')) + '</h4>';
      html += '<table class="ra-risk-register"><thead><tr><th>Risque</th><th>L</th><th>I</th><th>Inh.</th><th>Rés.</th><th>Feu</th><th>Traitement</th><th>' + _esc(_t('riskAnalysis.risks.owner','Propriétaire')) + '</th></tr></thead><tbody>';
      a.risks.forEach(function(rk) {
        var cat = RA_RISK_CATALOG.find(c => c.id === rk.catalogId);
        var title = cat ? _bi(cat, 'threat', 'threatEn') : (rk.customTitle || '?');
        var rs = r.riskScores.find(s => s.id === rk.id) || {};
        var impactCell = _esc('' + (rk.impact||3));
        if (rk.impactBreakdown && Object.keys(rk.impactBreakdown).length > 0) {
          impactCell += ' <span class="ra-matrix-badge" title="' + _esc(_t('riskAnalysis.impactMatrix.evaluated','dim. évaluée(s)')) + '"><i class="fas fa-table-cells"></i></span>';
        }
        var levelColor = (rs.levelColor || '').replace(/[^a-zA-Z0-9#(),.\s%]/g, '');
        var levelLabel = rs.levelLabel || '';
        html += '<tr><td>' + _esc(title) + '</td><td>' + _esc('' + (rk.likelihood||3)) + '</td><td>' + impactCell + '</td><td>' + _esc('' + (rs.inherent||'-')) + '</td><td>' + _esc('' + (rs.residual||'-')) + '</td>';
        html += '<td class="ra-traffic-cell">' + (levelColor ? '<span class="ra-level-dot" style="background:' + levelColor + '" title="' + _esc(levelLabel) + '"></span>' : '<span class="ra-light ra-light--' + _esc(rs.light||'grey') + '"></span>') + '</td>';
        html += '<td>' + _esc(rk.treatmentDecision||'-') + '</td>';
        html += '<td>' + _esc(rk.owner || '') + '</td></tr>';
      });
      html += '</tbody></table>';
    }
    // 5×5 Risk Matrix visualization
    if (a.risks && a.risks.length > 0) {
      html += renderResultsMatrix(a, r);
    }
    html += '</div>'; // close risques pane
    // === Tab: Recommandations ===
    html += '<div class="ra-results-pane" data-tab="recommandations" style="display:' + (activeTab === 'recommandations' ? 'block' : 'none') + '">';
    // Recommendations
    if (r.recommendations && r.recommendations.length > 0) {
      html += '<h4 class="ra-section-title"><i class="fas fa-lightbulb"></i> ' + _esc(_t('riskAnalysis.results.recommendations','Recommandations')) + '</h4>';
      html += '<table class="ra-rec-table"><thead><tr><th>#</th><th>Priorité</th><th>Recommandation</th><th>Domaine</th><th>Effort</th></tr></thead><tbody>';
      r.recommendations.forEach(function(rec, idx) {
        html += '<tr class="ra-rec-row" onclick="raToggleRec(' + idx + ')"><td><i class="fas fa-chevron-right ra-rec-chevron"></i>' + (idx+1) + '</td>';
        html += '<td><span class="ra-priority-badge ra-priority-badge--' + rec.priority + '">' + _esc(rec.priority) + '</span></td>';
        html += '<td>' + _esc(rec.title) + (rec.isPlanned ? ' <span class="ra-planned-badge"><i class="fas fa-clock"></i> ' + _esc(_t('riskAnalysis.controls.planned','Planifié')) + '</span>' : '') + '</td>';
        html += '<td>' + _esc(_t('riskAnalysis.domains.' + rec.domainId, rec.domainId)) + '</td>';
        html += '<td>' + _esc(rec.effort) + '</td></tr>';
        html += '<tr class="ra-rec-detail" id="raRecDetail_' + idx + '"><td colspan="5"><div class="ra-rec-detail__inner"><p><strong>' + _esc(_t('riskAnalysis.results.rationale','Rationale')) + ':</strong> ' + _esc(rec.rationale) + '</p>';
        html += '<p><strong>' + _esc(_t('riskAnalysis.results.actions','Actions')) + ':</strong></p><ul>';
        (rec.actions || []).forEach(function(act) { html += '<li>' + _esc(act) + '</li>'; });
        html += '</ul><p><strong>' + _esc(_t('riskAnalysis.results.doneWhen','Critère')) + ':</strong> ' + _esc(rec.doneWhen || '') + '</p></div></td></tr>';
      });
      html += '</tbody></table>';
    }
    html += '</div>'; // close recommandations pane
    body.innerHTML = html;
    // Draw radar
    setTimeout(function() { drawRadar(r.domainScores); }, 100);
  }
  window.raToggleReviewDate = function(checked) {
    var wrap = document.getElementById('raReviewDateWrap');
    if (wrap) wrap.style.display = checked ? 'block' : 'none';
  };
  window.raSetResultTab = function(tabId) {
    wizardState.resultTab = tabId;
    document.querySelectorAll('.ra-results-pane').forEach(function(p) {
      p.style.display = p.getAttribute('data-tab') === tabId ? 'block' : 'none';
    });
    document.querySelectorAll('.ra-results-tab').forEach(function(btn) {
      btn.classList.toggle('ra-results-tab--active', btn.getAttribute('data-tab') === tabId);
    });
    if (tabId === 'synthese') {
      var a = findAnalysis(wizardState.analysisId);
      if (a && a.results) setTimeout(function() { drawRadar(a.results.domainScores); }, 100);
    }
  };
  window.raToggleRec = function(idx) {
    var el = document.getElementById('raRecDetail_' + idx);
    if (el) {
      el.classList.toggle('ra-rec-detail--visible');
      var row = el.previousElementSibling;
      if (row) row.classList.toggle('ra-rec-row--expanded');
    }
  };
  window.raAddFinalRec = function() {
    var a = findAnalysis(wizardState.analysisId); if (!a) return;
    if (!a.finalRecs) a.finalRecs = { items: [] };
    if (a.finalRecs.items.length >= 10) return;
    a.finalRecs.items.push({ id: _id(), title: '', details: '', status: 'todo' });
    a.results = computeAllResults(a); _save();
    renderStepMesures(document.getElementById('raWizardBody'), a);
  };
  window.raRemoveFinalRec = function(idx) {
    var a = findAnalysis(wizardState.analysisId); if (!a || !a.finalRecs) return;
    var itemId = a.finalRecs.items[idx] ? a.finalRecs.items[idx].id : null;
    if (!itemId) return;
    a.finalRecs.items = a.finalRecs.items.filter(function(r) { return r.id !== itemId; });
    a.results = computeAllResults(a); _save();
    renderStepMesures(document.getElementById('raWizardBody'), a);
  };
  window.raUpdateFinalRecField = function(idx, field, val) {
    var a = findAnalysis(wizardState.analysisId); if (!a || !a.finalRecs || !a.finalRecs.items[idx]) return;
    a.finalRecs.items[idx][field] = val;
    _save();
    if (field === 'status') { a.results = computeAllResults(a); renderStepMesures(document.getElementById('raWizardBody'), a); }
  };

  function renderResultsMatrix(a, r) {
    var t = getActiveThreshold();
    var html = '<h4 class="ra-section-title"><i class="fas fa-table-cells"></i> ' + _esc(_t('riskAnalysis.thresholds.matrixTitle','Matrice de risque')) + '</h4>';
    // Build a map: "P_G" → count of risks at that position
    var riskMap = {};
    (a.risks || []).forEach(function(rk) {
      var pk = (rk.likelihood || 3) + '_' + (rk.impact || 3);
      riskMap[pk] = (riskMap[pk] || 0) + 1;
    });
    html += '<div class="ra-results-matrix">';
    html += '<div class="ra-threshold-grid ra-threshold-grid--results">';
    var _rLang = _lang();
    var _rGLabels = (_rLang === 'en' && t.gravityLabels) ? t.gravityLabels : (t.gravityLabelsFr || t.gravityLabels);
    var _rPLabels = (_rLang === 'en' && t.probabilityLabels) ? t.probabilityLabels : (t.probabilityLabelsFr || t.probabilityLabels);
    // Header
    html += '<div class="ra-tg-corner"><span class="ra-tg-axis-label ra-tg-axis-g">' + _esc(_t('riskAnalysis.thresholds.gravity','Gravité')) + ' →</span><span class="ra-tg-axis-label ra-tg-axis-p">↑ ' + _esc(_t('riskAnalysis.thresholds.probability','Probabilité')) + '</span></div>';
    for (var g = 1; g <= 5; g++) {
      html += '<div class="ra-tg-header">' + g + '<br><small>' + _esc(_rGLabels[g-1] || '') + '</small></div>';
    }
    for (var p = 5; p >= 1; p--) {
      html += '<div class="ra-tg-rowlabel">' + p + '<br><small>' + _esc(_rPLabels[p-1] || '') + '</small></div>';
      for (var g2 = 1; g2 <= 5; g2++) {
        var key = p + '_' + g2;
        var levelId = t.cells[key] || 'minimum';
        var lvl = t.levels.find(function(l) { return l.id === levelId; });
        var color = lvl ? lvl.color : '#ccc';
        var count = riskMap[key] || 0;
        html += '<div class="ra-tg-cell' + (count > 0 ? ' ra-tg-cell--has-risk' : '') + '" style="background:' + color + '" title="' + _esc(lvl ? _bi(lvl, 'label', 'labelEn') : '') + '">';
        if (count > 0) {
          html += '<span class="ra-tg-risk-count">' + count + '</span>';
        }
        html += '</div>';
      }
    }
    html += '</div>';
    // Legend
    html += '<div class="ra-threshold-legend ra-threshold-legend--results">';
    t.levels.forEach(function(lv) {
      html += '<span class="ra-threshold-legend-item"><span class="ra-threshold-legend-dot" style="background:' + lv.color + '"></span>' + _esc(_bi(lv, 'label', 'labelEn')) + '</span>';
    });
    html += '</div></div>';
    return html;
  }

  var _raChartInstances = {};
  var _raObserver = null;

  function drawRadar(domainScores) {
    var canvas = document.getElementById('raRadarChart');
    if (!canvas || typeof Chart === 'undefined') return;
    if (_raChartInstances.radar) { try { _raChartInstances.radar.destroy(); } catch(e){} _raChartInstances.radar = null; }
    var labels = [], data = [];
    RA_DOMAINS.forEach(function(d) {
      labels.push(_t('riskAnalysis.domains.' + d.id, _bi(d, 'label', 'labelEn')));
      data.push(domainScores[d.id] ? domainScores[d.id].maturity : 0);
    });
    _raChartInstances.radar = new Chart(canvas, {
      type: 'radar',
      data: {
        labels: labels,
        datasets: [{ label: _t('riskAnalysis.results.maturity','Maturité'), data: data, backgroundColor: 'rgba(31,184,205,0.2)', borderColor: 'rgba(31,184,205,1)', pointBackgroundColor: 'rgba(31,184,205,1)', borderWidth: 2 }]
      },
      options: { scales: { r: { min: 0, max: 3, ticks: { stepSize: 1 } } }, plugins: { legend: { display: false } }, responsive: true, maintainAspectRatio: true }
    });
  }

  // Step 7: Preview
  function renderStepPreview(body, a) {
    var r = a.results;
    if (!r) { body.innerHTML = '<p>' + _esc(_t('riskAnalysis.results.computing','Calcul...')) + '</p>'; return; }
    var html = '<div class="ra-preview">';
    // Info box
    html += '<div class="ra-preview-info"><i class="fas fa-eye"></i> ' + _esc(_t('riskAnalysis.preview.infoText','Prévisualisation du rapport. Vérifiez les informations avant l\'export PDF.')) + '</div>';

    // ==========================================================
    // METHODOLOGY SECTION (first, before cover)
    // ==========================================================
    html += '<div class="ra-preview-section ra-preview-methodology">';
    html += '<h3><i class="fas fa-book-open"></i> ' + _esc(_t('riskAnalysis.methodology.title','Cadre méthodologique')) + '</h3>';

    html += '<h4>' + _esc(_t('riskAnalysis.methodology.objective','Objectif')) + '</h4>';
    html += '<p>' + _esc(_t('riskAnalysis.methodology.objectiveText','Cette analyse vise à identifier, évaluer et traiter les risques de sécurité de l\'information conformément à la norme ISO/IEC 27005:2022.')) + '</p>';

    html += '<h4>' + _esc(_t('riskAnalysis.methodology.normativeRef','Référence normative')) + '</h4>';
    html += '<p><strong>ISO/IEC 27005:2022</strong> — ' + _esc(_t('riskAnalysis.methodology.normativeRefText','Technologies de l\'information — Gestion des risques liés à la sécurité de l\'information. Cette norme fournit les lignes directrices pour le processus de gestion des risques dans le cadre d\'un SMSI (ISO/IEC 27001).')) + '</p>';

    html += '<h4>' + _esc(_t('riskAnalysis.methodology.processTitle','Processus d\'analyse en 8 étapes')) + '</h4>';
    html += '<ol class="ra-methodology-steps">';
    var _methSteps = [
      _t('riskAnalysis.methodology.step1','Contexte & périmètre — Définition du périmètre, des parties prenantes et des critères d\'évaluation'),
      _t('riskAnalysis.methodology.step2','Classification CIA & RGPD — Évaluation de la confidentialité, intégrité, disponibilité et impact RGPD'),
      _t('riskAnalysis.methodology.step3','Continuité d\'activité — Objectifs RTO/RPO et contraintes métier'),
      _t('riskAnalysis.methodology.step4','Documentation — Architecture technique, gestion des identités, documentation'),
      _t('riskAnalysis.methodology.step5','Mesures & Contrôles — Évaluation de 24 mesures de sécurité sur 8 domaines'),
      _t('riskAnalysis.methodology.step6','Identification des risques — Sélection et évaluation des scénarios de risque'),
      _t('riskAnalysis.methodology.step7','Résultats & évaluation — Synthèse automatisée, scoring, recommandations'),
      _t('riskAnalysis.methodology.step8','Traitement du risque — Décisions de traitement (réduire, accepter, transférer, éviter)')
    ];
    _methSteps.forEach(function(s) { html += '<li>' + _esc(s) + '</li>'; });
    html += '</ol>';

    html += '<h4>' + _esc(_t('riskAnalysis.methodology.scoringTitle','Formule de scoring')) + '</h4>';
    html += '<p><code>' + _esc(_t('riskAnalysis.methodology.scoringFormula','Risque résiduel = Vraisemblance × Impact × (1 − Maturité contrôles × 0.40)')) + '</code></p>';
    html += '<p style="font-size:0.85rem;color:var(--color-text-light)">' + _esc(_t('riskAnalysis.methodology.scoringAdjust','Ajustements contextuels : +1 si exposition Internet, +1 si CIA élevé, +1 si RTO/RPO strict.')) + '</p>';

    html += '<h4>' + _esc(_t('riskAnalysis.methodology.scaleTitle','Échelle des risques')) + '</h4>';
    html += '<div class="ra-methodology-scale">';
    var _scaleItems = [
      { label: _t('riskAnalysis.thresholds.levels.minimum','Minimum'), color: '#22c55e' },
      { label: _t('riskAnalysis.thresholds.levels.faible','Faible'), color: '#84cc16' },
      { label: _t('riskAnalysis.thresholds.levels.modere','Modéré'), color: '#f97316' },
      { label: _t('riskAnalysis.thresholds.levels.eleve','Élevé'), color: '#ef4444' },
      { label: _t('riskAnalysis.thresholds.levels.tresEleve','Très élevé'), color: '#991b1b' }
    ];
    _scaleItems.forEach(function(si) {
      html += '<div class="ra-methodology-scale-item" style="background:' + si.color + '">' + _esc(si.label) + '</div>';
    });
    html += '</div>';

    html += '<h4>' + _esc(_t('riskAnalysis.methodology.pdcaTitle','Cycle PDCA')) + '</h4>';
    html += '<div class="ra-methodology-pdca">';
    html += '<div class="ra-pdca-quadrant" style="background:var(--color-info, #3b82f6)"><strong>Plan</strong><br>' + _esc(_t('riskAnalysis.methodology.pdcaPlan','Établir le contexte, identifier les risques')) + '</div>';
    html += '<div class="ra-pdca-quadrant" style="background:var(--color-success, #22c55e)"><strong>Do</strong><br>' + _esc(_t('riskAnalysis.methodology.pdcaDo','Implémenter les mesures de traitement')) + '</div>';
    html += '<div class="ra-pdca-quadrant" style="background:var(--color-danger, #ef4444)"><strong>Act</strong><br>' + _esc(_t('riskAnalysis.methodology.pdcaAct','Améliorer, mettre à jour l\'analyse')) + '</div>';
    html += '<div class="ra-pdca-quadrant" style="background:var(--color-warning, #f97316)"><strong>Check</strong><br>' + _esc(_t('riskAnalysis.methodology.pdcaCheck','Surveiller, contrôler, réévaluer')) + '</div>';
    html += '</div>';

    html += '</div>';

    // Cover + Identity
    var ident = a.identity || {};
    html += '<div class="ra-preview-section ra-preview-cover">';
    html += '<h2>' + _esc(_t('riskAnalysis.preview.reportTitle','Analyse des risques en sécurité de l\'information - Cybersécurité')) + '</h2>';
    html += '<h3>' + _esc(a.meta.title || 'Sans titre') + '</h3>';
    if (ident.beneficiary) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.identity.beneficiary','Bénéficiaire/Client')) + ':</strong> ' + _esc(ident.beneficiary) + '</div>';
    if (ident.controlBody) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.identity.controlBody','Organisme de contrôle')) + ':</strong> ' + _esc(ident.controlBody) + '</div>';
    if (ident.projectManager) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.identity.projectManager','Chef de projet')) + ':</strong> ' + _esc(ident.projectManager) + '</div>';
    if (ident.encoder) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.identity.encoder','Encodage')) + ':</strong> ' + _esc(ident.encoder) + '</div>';
    if (ident.validator) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.identity.validator','Validation')) + ':</strong> ' + _esc(ident.validator) + '</div>';
    if (a.meta.reportNumber) html += '<p>' + _esc(_t('riskAnalysis.preview.reportNumber','Numéro du rapport')) + ': ' + _esc(a.meta.reportNumber) + '</p>';
    html += '<p>' + _esc(_t('riskAnalysis.preview.reportDate','Date')) + ': ' + _esc(a.meta.reportDate || new Date().toLocaleDateString()) + '</p>';
    html += '</div>';
    // Analyse de sécurité — Preview
    if (r.spotSummary) {
      var sp = r.spotSummary;
      html += '<div class="ra-preview-section">';
      html += '<h4><i class="fas fa-traffic-light"></i> ' + _esc(_t('riskAnalysis.spot.title','Résultats de l\'évaluation')) + '</h4>';
      html += '<div class="ra-spot-header">';
      html += '<div class="ra-traffic-light-display">';
      html += '<div class="ra-tl-circle ra-tl-circle--red' + (sp.light === 'red' ? ' ra-tl-circle--active' : '') + '"></div>';
      html += '<div class="ra-tl-circle ra-tl-circle--yellow' + (sp.light === 'yellow' ? ' ra-tl-circle--active' : '') + '"></div>';
      html += '<div class="ra-tl-circle ra-tl-circle--green' + (sp.light === 'green' ? ' ra-tl-circle--active' : '') + '"></div>';
      html += '</div>';
      html += '<div class="ra-spot-avis ra-spot-avis--' + sp.light + '">' + _esc(sp.avisLabel) + '</div>';
      html += '</div>';
      // KPI cards with progress bars and explanations
      var kpis = [
        { label: _t('riskAnalysis.spot.inherentCriticality','Criticité de l\'actif (risque inhérent)'), value: sp.inherentCriticalityScore + '/5', pct: sp.inherentCriticalityScore * 20, color: sp.inherentCriticalityScore >= 4 ? 'red' : sp.inherentCriticalityScore === 3 ? 'yellow' : 'green', hint: _t('riskAnalysis.spot.inherentCriticalityHint','Évalue le niveau d\'exposition intrinsèque de l\'actif.') },
        { label: _t('riskAnalysis.spot.measuresPercent','Niveau d\'implémentation des mesures'), value: sp.measuresImplementationPercent + '%', pct: sp.measuresImplementationPercent, color: sp.measuresImplementationPercent >= 70 ? 'green' : sp.measuresImplementationPercent >= 40 ? 'yellow' : 'red', hint: _t('riskAnalysis.spot.measuresPercentHint','Pourcentage moyen d\'implémentation des contrôles de sécurité.'), invertBar: true },
        { label: _t('riskAnalysis.spot.residualScore','Niveau de risque résiduel'), value: sp.residualScore + '/5', pct: sp.residualScore * 20, color: sp.residualScore <= 2 ? 'green' : sp.residualScore === 3 ? 'yellow' : 'red', hint: _t('riskAnalysis.spot.residualScoreHint','Niveau de risque après application des mesures de sécurité.') },
        { label: _t('riskAnalysis.spot.residualPlusOpen','Risque résiduel + risque ouvert'), value: sp.residualPlusOpenScore + '/5', pct: sp.residualPlusOpenScore * 20, color: sp.residualPlusOpenScore <= 2 ? 'green' : sp.residualPlusOpenScore === 3 ? 'yellow' : 'red', hint: _t('riskAnalysis.spot.residualPlusOpenHint','Score résiduel augmenté des risques ouverts non encore traités.') }
      ];
      html += '<div class="ra-kpi-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:var(--spacing-md,12px);margin-top:var(--spacing-md,12px)">';
      kpis.forEach(function(k) {
        var barColor = k.color === 'green' ? 'var(--color-success,#22c55e)' : k.color === 'yellow' ? 'var(--color-warning,#f97316)' : 'var(--color-danger,#ef4444)';
        html += '<div class="ra-kpi-card" style="border:1px solid var(--color-border,#e9ecef);border-radius:var(--radius-md,8px);padding:var(--spacing-md,12px);background:var(--color-surface,#fff)">';
        html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px"><span style="font-weight:600;font-size:0.85rem">' + _esc(k.label) + '</span>';
        html += '<span class="ra-spot-score--' + k.color + '" style="font-weight:700;font-size:1.1rem">' + _esc(k.value) + '</span></div>';
        html += '<div style="background:var(--color-border,#e9ecef);border-radius:4px;height:8px;overflow:hidden"><div style="height:100%;width:' + k.pct + '%;background:' + barColor + ';border-radius:4px;transition:width 0.3s"></div></div>';
        html += '<div style="font-size:0.75rem;color:var(--color-text-light,#6b7280);margin-top:6px;font-style:italic">' + _esc(k.hint) + '</div>';
        html += '</div>';
      });
      html += '</div>';
      // Comments section
      if ((a.spot || {}).avisComment || sp.avisOverridden || (a.spot || {}).residualCorrectedComment) {
        html += '<table class="ra-spot-table" style="margin-top:var(--spacing-md,12px)"><tbody>';
        if ((a.spot || {}).avisComment) html += '<tr><td>' + _esc(_t('riskAnalysis.spot.avisCommentLabel','Commentaire avis')) + '</td><td>' + _esc(a.spot.avisComment) + '</td></tr>';
        if (sp.avisOverridden) html += '<tr><td>' + _esc(_t('riskAnalysis.spot.avisOverrideJustifLabel','Justification correction avis')) + '</td><td>' + _esc((a.spot || {}).avisOverrideJustification || '') + '</td></tr>';
        if ((a.spot || {}).residualCorrectedComment) html += '<tr><td>' + _esc(_t('riskAnalysis.spot.residualCorrectedLabel','Commentaire résiduel corrigé')) + '</td><td>' + _esc(a.spot.residualCorrectedComment) + '</td></tr>';
        html += '</tbody></table>';
      }
      html += '</div>';
    }
    // Context
    html += '<div class="ra-preview-section">';
    html += '<h4><i class="fas fa-info-circle"></i> ' + _esc(_t('riskAnalysis.steps.context','Contexte')) + '</h4>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.context.perimeter','Périmètre')) + ':</strong> ' + _esc(a.context.perimeter || 'N/A') + '</div>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.context.description','Description')) + ':</strong> ' + _esc(a.context.description || 'N/A') + '</div>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.context.stakeholders','Parties prenantes')) + ':</strong> ' + _esc(a.context.stakeholders || 'N/A') + '</div>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.context.sourceFramework','Référentiel source')) + ':</strong> ' + _esc(a.context.sourceFramework || 'N/A') + '</div>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.context.personalData','Données personnelles')) + ':</strong> ' + (a.context.personalData ? 'Oui' : 'Non') + '</div>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.context.dataSensitivity','Sensibilité')) + ':</strong> ' + _esc(a.context.dataSensitivity || 'N/A') + '</div>';
    if (a.impactMatrixId && window.CSI_ImpactMatrices) {
      var prevMatrix = CSI_ImpactMatrices.getMatrixById(a.impactMatrixId);
      if (prevMatrix) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.impactMatrix.matrixUsed','Matrice d\'impact utilisée')) + ':</strong> ' + _esc(prevMatrix.name) + '</div>';
    }
    if (a.meta && a.meta.nextReviewDate) {
      html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.reviewDate.title','Prochaine revue')) + ':</strong> ' + _esc(a.meta.nextReviewDate) + '</div>';
    }
    // GDPR summary in context
    if (a.gdpr) {
      var _regLbls = { yes: _t('riskAnalysis.gdpr.registryYes','Oui'), no: _t('riskAnalysis.gdpr.registryNo','Non'), planned: _t('riskAnalysis.gdpr.registryPlanned','Planifié') };
      html += '<hr class="ra-section-divider">';
      html += '<h5><i class="fas fa-user-shield"></i> ' + _esc(_t('riskAnalysis.gdpr.title','RGPD — Protection des données')) + '</h5>';
      html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.gdpr.personalData','Données personnelles')) + ':</strong> ' + (a.gdpr.personalData ? _t('common.yes','Oui') : _t('common.no','Non')) + '</div>';
      html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.gdpr.sensitiveData','Données sensibles')) + ':</strong> ' + (a.gdpr.sensitiveData ? _t('common.yes','Oui') : _t('common.no','Non')) + '</div>';
      html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.gdpr.processingRegistry','Registre des traitements')) + ':</strong> ' + _esc(_regLbls[a.gdpr.processingRegistry] || '-') + '</div>';
    }
    html += '</div>';
    // CIA & Continuity — enriched visuals
    html += '<div class="ra-preview-section">';
    html += '<h4><i class="fas fa-shield-halved"></i> ' + _esc(_t('riskAnalysis.cia.title','Classification CIA')) + ' & ' + _esc(_t('riskAnalysis.continuity.title','Continuité')) + '</h4>';
    // CIA Visual Bars
    var _ciaDims = [
      { key: 'confidentiality', label: _t('riskAnalysis.cia.confidentiality','Confidentialité'), icon: 'fas fa-lock' },
      { key: 'integrity', label: _t('riskAnalysis.cia.integrity','Intégrité'), icon: 'fas fa-shield-halved' },
      { key: 'availability', label: _t('riskAnalysis.cia.availability','Disponibilité'), icon: 'fas fa-clock' }
    ];
    var _ciaLvl = [_t('riskAnalysis.cia.level1','Faible'), _t('riskAnalysis.cia.level2','Moyen'), _t('riskAnalysis.cia.level3','Élevé')];
    html += '<div class="ra-preview-cia-bars">';
    _ciaDims.forEach(function(dim) {
      var val = a.cia[dim.key] || 2;
      var cc = val === 1 ? 'green' : val === 2 ? 'yellow' : 'red';
      html += '<div class="ra-preview-cia-row">';
      html += '<span class="ra-preview-cia-label"><i class="' + dim.icon + '"></i> ' + _esc(dim.label) + '</span>';
      html += '<div class="ra-preview-cia-track">';
      for (var s = 1; s <= 3; s++) {
        html += '<div class="ra-preview-cia-seg' + (s <= val ? ' ra-preview-cia-seg--filled ra-preview-cia-seg--' + cc : '') + '"></div>';
      }
      html += '</div>';
      html += '<span class="ra-preview-cia-value">' + val + '/3 — ' + _esc(_ciaLvl[val - 1]) + '</span>';
      html += '</div>';
    });
    html += '</div>';
    // RTO/RPO Visual Gauges
    function _prvRtoZone(h) {
      if (h <= 4) return { cls: 'strict', label: _t('riskAnalysis.continuity.strict','Très strict'), pct: 10 };
      if (h <= 8) return { cls: 'moderate', label: _t('riskAnalysis.continuity.moderate','Strict'), pct: 28 };
      if (h <= 24) return { cls: 'standard', label: _t('riskAnalysis.continuity.standard','Standard'), pct: 55 };
      return { cls: 'relaxed', label: _t('riskAnalysis.continuity.relaxed','Souple'), pct: 85 };
    }
    function _prvRtoGauge(label, hours) {
      var z = _prvRtoZone(hours);
      var o = '<div class="ra-preview-rto-gauge">';
      o += '<span class="ra-preview-rto-label"><strong>' + _esc(label) + ':</strong> ' + hours + 'h</span>';
      o += '<div class="ra-preview-rto-track">';
      o += '<div class="ra-preview-rto-zone ra-preview-rto-zone--strict" style="width:15%"></div>';
      o += '<div class="ra-preview-rto-zone ra-preview-rto-zone--moderate" style="width:25%"></div>';
      o += '<div class="ra-preview-rto-zone ra-preview-rto-zone--standard" style="width:30%"></div>';
      o += '<div class="ra-preview-rto-zone ra-preview-rto-zone--relaxed" style="width:30%"></div>';
      o += '<div class="ra-preview-rto-marker" style="left:' + z.pct + '%"></div>';
      o += '</div>';
      o += '<span class="ra-preview-rto-indicator ra-rto-indicator--' + z.cls + '">' + _esc(z.label) + '</span>';
      o += '</div>';
      return o;
    }
    html += '<div class="ra-preview-rto-section">';
    html += _prvRtoGauge('RTO', a.continuity.rtoHours || 24);
    html += _prvRtoGauge('RPO', a.continuity.rpoHours || 24);
    html += '</div>';
    if (a.continuity.comment) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.continuity.commentLabel','Commentaire continuité')) + ':</strong> ' + _esc(a.continuity.comment) + '</div>';
    if (a.cia.justification) html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.cia.justification','Justification')) + ':</strong> ' + _esc(a.cia.justification) + '</div>';
    html += '</div>';
    // Architecture
    html += '<div class="ra-preview-section">';
    html += '<h4><i class="fas fa-server"></i> ' + _esc(_t('riskAnalysis.architecture.title','Architecture')) + '</h4>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.architecture.hosting','Hébergement')) + ':</strong> ' + _esc(a.architecture.hosting || 'N/A') + '</div>';
    html += '<div class="ra-preview-kv"><strong>' + _esc(_t('riskAnalysis.architecture.internetExposed','Exposé Internet')) + ':</strong> ' + (a.architecture.internetExposed ? 'Oui' : 'Non') + '</div>';
    html += '<div class="ra-preview-kv"><strong>SSO:</strong> ' + (a.architecture.iam.sso ? 'Oui' : 'Non') + ' &nbsp; <strong>MFA:</strong> ' + (a.architecture.iam.mfa ? 'Oui' : 'Non') + ' &nbsp; <strong>' + _esc(_t('riskAnalysis.architecture.adminSeparation','Séparation admin')) + ':</strong> ' + (a.architecture.iam.adminSeparation ? 'Oui' : 'Non') + '</div>';
    html += '</div>';
    // Controls — All controls by domain
    html += '<div class="ra-preview-section">';
    html += '<h4><i class="fas fa-clipboard-check"></i> ' + _esc(_t('riskAnalysis.controls.title','Mesures & Contrôles')) + '</h4>';
    if (r.totalPlannedControls > 0) {
      html += '<div class="ra-planned-warning-box">';
      html += '<div class="ra-planned-warning-icon"><i class="fas fa-exclamation-triangle"></i></div>';
      html += '<div class="ra-planned-warning-text">' + _esc(_t('riskAnalysis.controls.improvedWarning', 'Le résultat d\'analyse des risques repose sur le fait que les mesures et contrôles planifiés seront correctement implémentés — il sera indispensable de réaliser une analyse de risque préalablement au lancement en production du projet.')) + '</div>';
      html += '</div>';
    }
    if (r.allControlsList) {
      r.allControlsList.forEach(function(dom) {
        html += '<div class="ra-preview-controls-domain">';
        html += '<h5><i class="' + dom.icon + '"></i> ' + _esc(_t('riskAnalysis.domains.' + dom.domainId, dom.label || dom.labelEn || '')) + '</h5>';
        html += '<table class="ra-controls-table"><thead><tr>';
        html += '<th style="width:55%">' + _esc(_t('riskAnalysis.pdfControl','Contrôle')) + '</th>';
        html += '<th style="width:15%">' + _esc(_t('riskAnalysis.controls.statusLabel','Statut')) + '</th>';
        html += '<th style="width:30%">' + _esc(_t('riskAnalysis.controls.comment','Commentaire')) + '</th>';
        html += '</tr></thead><tbody>';
        dom.controls.forEach(function(ctrl) {
          var statusLabel = _t('riskAnalysis.controls.' + ctrl.status, ctrl.status);
          html += '<tr>';
          html += '<td>' + _esc(ctrl.text) + '</td>';
          html += '<td><span class="ra-status-badge ra-status-badge--' + ctrl.status + '">' + _esc(statusLabel) + '</span></td>';
          html += '<td class="ra-ctrl-comment">' + _esc(ctrl.comment || '-') + '</td>';
          html += '</tr>';
        });
        html += '</tbody></table></div>';
      });
    }
    html += '</div>';
    // Domain synthesis
    html += '<div class="ra-preview-section">';
    html += '<h4><i class="fas fa-chart-bar"></i> ' + _esc(_t('riskAnalysis.results.domainSynthesis','Synthèse des domaines')) + '</h4>';
    html += '<table><thead><tr><th>' + _esc(_t('riskAnalysis.domains.IAM','Domaine')) + '</th><th>' + _esc(_t('riskAnalysis.results.maturity','Maturité')) + '</th><th>%</th><th>' + _esc(_t('riskAnalysis.listColumns.light','Feu')) + '</th></tr></thead><tbody>';
    RA_DOMAINS.forEach(function(d) {
      var ds = r.domainScores[d.id] || { maturity: 0, percent: 0, light: 'grey' };
      html += '<tr><td>' + _esc(_t('riskAnalysis.domains.' + d.id, _bi(d, 'label', 'labelEn'))) + '</td><td>' + ds.maturity + '/3</td><td>' + ds.percent + '%' + (ds.plannedCount > 0 ? ' <span class="ra-planned-badge"><i class="fas fa-clock"></i> ' + ds.plannedCount + '</span>' : '') + '</td><td><span class="ra-light ra-light--' + ds.light + '"></span></td></tr>';
    });
    html += '</tbody></table>';
    html += '<div class="ra-radar-wrapper"><canvas id="raPreviewRadar" width="400" height="400"></canvas></div>';
    html += '</div>';
    // Risk register — enriched with 5-level colors + treatment
    if (a.risks && a.risks.length > 0) {
      html += '<div class="ra-preview-section">';
      html += '<h4><i class="fas fa-triangle-exclamation"></i> ' + _esc(_t('riskAnalysis.results.riskRegister','Registre des risques')) + '</h4>';
      html += '<table><thead><tr><th>Risque</th><th>L</th><th>I</th><th>Inh.</th><th>Rés.</th><th>' + _esc(_t('riskAnalysis.results.riskLevel','Niveau')) + '</th><th>' + _esc(_t('riskAnalysis.risks.treatment','Traitement')) + '</th><th>' + _esc(_t('riskAnalysis.risks.owner','Propriétaire')) + '</th></tr></thead><tbody>';
      var _treatLabelsP = { reduce: _t('riskAnalysis.risks.treatReduce','Réduire'), accept: _t('riskAnalysis.risks.treatAccept','Accepter'), transfer: _t('riskAnalysis.risks.treatTransfer','Transférer'), avoid: _t('riskAnalysis.risks.treatAvoid','Éviter') };
      a.risks.forEach(function(rk) {
        var cat = RA_RISK_CATALOG.find(function(c) { return c.id === rk.catalogId; });
        var title = cat ? _bi(cat, 'threat', 'threatEn') : (rk.customTitle || '?');
        var rs = r.riskScores.find(function(s) { return s.id === rk.id; }) || {};
        var safeLevelColor = (rs.levelColor || '').replace(/[^a-zA-Z0-9#(),.\s%]/g, '');
        html += '<tr><td>' + _esc(title) + '</td><td>' + _esc('' + (rk.likelihood || 3)) + '</td><td>' + _esc('' + (rk.impact || 3)) + '</td><td>' + _esc('' + (rs.inherent || '-')) + '</td><td>' + _esc('' + (rs.residual || '-')) + '</td>';
        html += '<td>' + (safeLevelColor ? '<span class="ra-level-dot" style="background:' + safeLevelColor + '"></span><small>' + _esc(rs.levelLabel || '') + '</small>' : '<span class="ra-light ra-light--' + _esc(rs.light || 'grey') + '"></span>') + '</td>';
        html += '<td>' + _esc(_treatLabelsP[rk.treatmentDecision] || rk.treatmentDecision || '-') + '</td>';
        html += '<td>' + _esc(rk.owner || '-') + '</td></tr>';
      });
      html += '</tbody></table>';
      // 5×5 Risk Matrix
      html += renderResultsMatrix(a, r);
      // Treatment distribution
      var _tc = { reduce: 0, accept: 0, transfer: 0, avoid: 0 };
      a.risks.forEach(function(rk) { var td = rk.treatmentDecision || 'reduce'; if (_tc[td] !== undefined) _tc[td]++; });
      var _tIcons = { reduce: 'fas fa-screwdriver-wrench', accept: 'fas fa-check-circle', transfer: 'fas fa-share', avoid: 'fas fa-ban' };
      var _tColors = { reduce: 'var(--color-primary)', accept: 'var(--color-success)', transfer: 'var(--color-warning)', avoid: 'var(--color-danger)' };
      html += '<h4 style="margin-top:var(--spacing-lg)"><i class="fas fa-chart-pie"></i> ' + _esc(_t('riskAnalysis.preview.treatmentDistribution','Répartition des traitements')) + '</h4>';
      html += '<div class="ra-preview-treatment-grid">';
      ['reduce','accept','transfer','avoid'].forEach(function(key) {
        html += '<div class="ra-preview-treatment-card"><div class="ra-preview-treatment-count" style="color:' + _tColors[key] + '">' + _tc[key] + '</div>';
        html += '<div class="ra-preview-treatment-label"><i class="' + _tIcons[key] + '"></i> ' + _esc(_treatLabelsP[key]) + '</div></div>';
      });
      html += '</div>';
      html += '</div>';
    }
    // Auto Recommendations
    if (r.recommendations && r.recommendations.length > 0) {
      html += '<div class="ra-preview-section">';
      html += '<h4><i class="fas fa-lightbulb"></i> ' + _esc(_t('riskAnalysis.results.recommendations','Recommandations')) + '</h4>';
      html += '<table><thead><tr><th>#</th><th>P</th><th>Recommandation</th><th>Domaine</th><th>Effort</th></tr></thead><tbody>';
      r.recommendations.forEach(function(rec, idx) {
        html += '<tr><td>' + (idx + 1) + '</td><td>' + _esc(rec.priority) + '</td><td>' + _esc(rec.title) + (rec.isPlanned ? ' <span class="ra-planned-badge"><i class="fas fa-clock"></i> ' + _esc(_t('riskAnalysis.controls.planned','Planifié')) + '</span>' : '') + '</td><td>' + _esc(_t('riskAnalysis.domains.' + rec.domainId, rec.domainId)) + '</td><td>' + _esc(rec.effort) + '</td></tr>';
      });
      html += '</tbody></table></div>';
    }
    // Assumptions
    html += '<div class="ra-preview-section">';
    html += '<div class="ra-preview-disclaimer">';
    html += _esc(_t('riskAnalysis.preview.disclaimer', 'Cette analyse reflète la posture à la date de réalisation. Les scores sont indicatifs et ne remplacent pas un audit formel. Les recommandations sont générées automatiquement sur la base des réponses fournies.'));
    html += '</div></div>';
    // Key Recommendations — Priority Mitigation Measures (last section, highlighted)
    var frItems = (a.finalRecs && a.finalRecs.items) ? a.finalRecs.items.filter(function(it) { return it.title || it.details; }) : [];
    if (frItems.length > 0) {
      html += '<div class="ra-preview-section ra-preview-section--highlight">';
      html += '<h4><i class="fas fa-shield-halved"></i> ' + _esc(_t('riskAnalysis.finalRecs.mitigationTitle','Mesures de mitigation prioritaires')) + '</h4>';
      frItems.forEach(function(item, idx) {
        html += '<div class="ra-finalrec-card ra-finalrec-card--highlight"><strong>#' + (idx + 1) + ' — ' + _esc(item.title || '') + '</strong>';
        html += '<span style="float:right;font-size:var(--font-size-xs);padding:2px 8px;border-radius:12px;background:' + (item.status === 'done' ? 'var(--color-success)' : 'var(--color-warning)') + ';color:#fff;">' + _esc(item.status === 'done' ? _t('riskAnalysis.finalRecs.statusDone','Fait') : _t('riskAnalysis.finalRecs.statusTodo','À faire')) + '</span>';
        html += '<p style="margin-top:4px;font-size:var(--font-size-sm);">' + _esc(item.details || '') + '</p></div>';
      });
      html += '</div>';
    }
    html += '</div>';
    body.innerHTML = html;
    // === Build floating TOC ===
    (function() {
      var previewEl = body.querySelector('.ra-preview');
      if (!previewEl) return;
      var iconMap = {
        'fa-book-open': { id: 'ra-prev-methodology', label: _t('riskAnalysis.methodology.title','Méthodologie') },
        'fa-traffic-light': { id: 'ra-prev-spot', label: _t('riskAnalysis.spot.title','Évaluation') },
        'fa-info-circle': { id: 'ra-prev-context', label: _t('riskAnalysis.steps.context','Contexte') },
        'fa-shield-halved': { id: 'ra-prev-cia', label: _t('riskAnalysis.cia.title','CIA & Continuité') },
        'fa-server': { id: 'ra-prev-architecture', label: _t('riskAnalysis.architecture.title','Architecture') },
        'fa-clipboard-check': { id: 'ra-prev-controls', label: _t('riskAnalysis.controls.title','Contrôles') },
        'fa-chart-bar': { id: 'ra-prev-domains', label: _t('riskAnalysis.results.domainSynthesis','Synthèse') },
        'fa-triangle-exclamation': { id: 'ra-prev-risks', label: _t('riskAnalysis.results.riskRegister','Registre') },
        'fa-lightbulb': { id: 'ra-prev-recs', label: _t('riskAnalysis.results.recommendations','Recommandations') }
      };
      var sections = previewEl.querySelectorAll('.ra-preview-section');
      var tocHtml = '<nav class="ra-preview-toc"><strong>' + _esc(_t('riskAnalysis.preview.toc','Sommaire')) + '</strong>';
      sections.forEach(function(sec) {
        var entry = null;
        if (sec.classList.contains('ra-preview-methodology')) {
          entry = iconMap['fa-book-open'];
        } else if (sec.classList.contains('ra-preview-cover')) {
          entry = { id: 'ra-prev-cover', label: _t('riskAnalysis.preview.reportTitle','Couverture') };
        } else if (sec.classList.contains('ra-preview-section--highlight')) {
          entry = { id: 'ra-prev-keyrecs', label: _t('riskAnalysis.finalRecs.mitigationTitle','Mesures clefs') };
        } else if (sec.querySelector('.ra-preview-disclaimer')) {
          entry = { id: 'ra-prev-assumptions', label: _t('riskAnalysis.preview.assumptions','Hypothèses') };
        } else {
          var heading = sec.querySelector('h3, h4');
          var icon = heading ? heading.querySelector('i') : null;
          if (icon) {
            var ic = icon.className;
            Object.keys(iconMap).forEach(function(key) {
              if (ic.indexOf(key) !== -1 && !entry) entry = iconMap[key];
            });
          }
        }
        if (entry) {
          sec.id = entry.id;
          tocHtml += '<a href="#' + entry.id + '" class="ra-preview-toc__link" data-target="' + entry.id + '">' + _esc(entry.label) + '</a>';
        }
      });
      tocHtml += '</nav>';
      var infoBox = previewEl.querySelector('.ra-preview-info');
      if (infoBox) infoBox.insertAdjacentHTML('afterend', tocHtml);
      // Smooth scroll on click
      var tocLinks = previewEl.querySelectorAll('.ra-preview-toc__link');
      tocLinks.forEach(function(link) {
        link.addEventListener('click', function(e) {
          e.preventDefault();
          var target = document.getElementById(link.getAttribute('data-target'));
          if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
        });
      });
      // IntersectionObserver for active highlight
      if (tocLinks.length > 0 && typeof IntersectionObserver !== 'undefined') {
        if (_raObserver) { _raObserver.disconnect(); _raObserver = null; }
        _raObserver = new IntersectionObserver(function(entries) {
          entries.forEach(function(ent) {
            if (ent.isIntersecting) {
              tocLinks.forEach(function(l) { l.classList.remove('ra-preview-toc__link--active'); });
              var active = previewEl.querySelector('.ra-preview-toc__link[data-target="' + ent.target.id + '"]');
              if (active) active.classList.add('ra-preview-toc__link--active');
            }
          });
        }, { rootMargin: '-100px 0px -60% 0px' });
        sections.forEach(function(sec) { if (sec.id) _raObserver.observe(sec); });
      }
    })();
    // Initialize preview radar chart
    setTimeout(function() {
      var prc = document.getElementById('raPreviewRadar');
      if (!prc || typeof Chart === 'undefined') return;
      if (_raChartInstances.preview) { try { _raChartInstances.preview.destroy(); } catch(e){} _raChartInstances.preview = null; }
      var labels = [], data = [];
      RA_DOMAINS.forEach(function(d) {
        labels.push(_t('riskAnalysis.domains.' + d.id, _bi(d, 'label', 'labelEn')));
        data.push(r.domainScores[d.id] ? r.domainScores[d.id].maturity : 0);
      });
      _raChartInstances.preview = new Chart(prc, {
        type: 'radar',
        data: { labels: labels, datasets: [{ label: _t('riskAnalysis.results.maturity','Maturité'), data: data, backgroundColor: 'rgba(31,184,205,0.2)', borderColor: 'rgba(31,184,205,1)', pointBackgroundColor: 'rgba(31,184,205,1)', borderWidth: 2 }] },
        options: { scales: { r: { min: 0, max: 3, ticks: { stepSize: 1 } } }, plugins: { legend: { display: false } }, responsive: true, maintainAspectRatio: true }
      });
    }, 100);
  }

  // Step 8: Export
  function renderStepExport(body, a) {
    var secs = ['cover','methodology','spot','executive','context','cia','architecture','controls','domains','risks','recommendations','assumptions','keyRecommendations'];
    var html = '<div class="ra-export-grid">';

    // Card 1 — PDF Export
    html += '<div class="ra-export-card">';
    html += '<div class="ra-export-card__icon"><i class="fas fa-file-pdf"></i></div>';
    html += '<h4>' + _esc(_t('riskAnalysis.exportPdf.title','Export PDF')) + '</h4>';
    html += '<p>' + _esc(_t('riskAnalysis.exportPdf.sections','Le rapport contiendra :')) + '</p>';
    html += '<div class="ra-export-sections">';
    secs.forEach(function(s) {
      html += '<label><input type="checkbox" checked disabled> ' + _esc(_t('riskAnalysis.exportPdf.sectionList.' + s, s)) + '</label>';
    });
    html += '</div>';
    html += '<button class="btn btn--primary" onclick="raExportPdf(\'' + a.id + '\')" id="raExportBtn"><i class="fas fa-file-pdf"></i> ' + _esc(_t('riskAnalysis.exportPdf.generatePdf','Générer le PDF')) + '</button>';
    html += '</div>';

    // Card 2 — .analyse Export
    html += '<div class="ra-export-card">';
    html += '<div class="ra-export-card__icon"><i class="fas fa-file-export"></i></div>';
    html += '<h4>' + _esc(_t('riskAnalysis.exportPdf.analyseTitle','Export .analyse')) + '</h4>';
    html += '<p>' + _esc(_t('riskAnalysis.exportPdf.analyseDesc','Exporte l\'analyse complète au format JSON réimportable. Inclut la matrice d\'impacts et les seuils de risque liés.')) + '</p>';
    html += '<button class="btn btn--primary" onclick="raExportAnalyseFile(\'' + a.id + '\')"><i class="fas fa-file-export"></i> ' + _esc(_t('riskAnalysis.exportPdf.analyseTitle','Export .analyse')) + '</button>';
    html += '</div>';

    html += '</div>';
    body.innerHTML = html;
  }

  // ============================================================
  // SAVE CURRENT STEP
  // ============================================================
  function saveCurrentStep() {
    var a = findAnalysis(wizardState.analysisId);
    if (!a) return;
    switch (wizardState.step) {
      case 0:
        var el;
        el = document.getElementById('raTitle'); if (el) a.meta.title = el.value;
        el = document.getElementById('raPerimeter'); if (el) a.context.perimeter = el.value;
        el = document.getElementById('raDesc'); if (el) a.context.description = el.value;
        el = document.getElementById('raStake'); if (el) a.context.stakeholders = el.value;
        el = document.getElementById('raSourceFramework'); if (el) a.context.sourceFramework = el.value;
        el = document.getElementById('raSensitivity'); if (el) a.context.dataSensitivity = el.value;
        el = document.getElementById('raMatrixSelect'); if (el) a.impactMatrixId = el.value || null;
        if (!a.identity) a.identity = {};
        el = document.getElementById('raEncoder'); if (el) a.identity.encoder = el.value;
        el = document.getElementById('raValidator'); if (el) a.identity.validator = el.value;
        el = document.getElementById('raControlBody'); if (el) a.identity.controlBody = el.value;
        el = document.getElementById('raBeneficiary'); if (el) a.identity.beneficiary = el.value;
        el = document.getElementById('raProjectManager'); if (el) a.identity.projectManager = el.value;
        break;
      case 1:
        el = document.getElementById('raCiaJustif'); if (el) a.cia.justification = el.value;
        // GDPR fields
        if (!a.gdpr) a.gdpr = { personalData: false, sensitiveData: false, processingRegistry: 'no' };
        el = document.getElementById('raGdprPersonal'); if (el) a.gdpr.personalData = el.checked;
        el = document.getElementById('raGdprSensitive'); if (el) a.gdpr.sensitiveData = el.checked;
        el = document.getElementById('raGdprRegistry'); if (el) a.gdpr.processingRegistry = el.value;
        // Sync back to context for backward compat
        a.context.personalData = a.gdpr.personalData;
        break;
      case 2:
        el = document.getElementById('raRto'); if (el) a.continuity.rtoHours = Math.max(0, parseInt(el.value) || 24);
        el = document.getElementById('raRpo'); if (el) a.continuity.rpoHours = Math.max(0, parseInt(el.value) || 24);
        el = document.getElementById('raContinuityComment'); if (el) a.continuity.comment = el.value;
        break;
      case 3:
        el = document.getElementById('raHosting'); if (el) a.architecture.hosting = el.value;
        el = document.getElementById('raInternet'); if (el) a.architecture.internetExposed = el.checked;
        el = document.getElementById('raSso'); if (el) a.architecture.iam.sso = el.checked;
        el = document.getElementById('raMfa'); if (el) a.architecture.iam.mfa = el.checked;
        el = document.getElementById('raAdminSep'); if (el) a.architecture.iam.adminSeparation = el.checked;
        el = document.getElementById('raArchDoc'); if (el) a.architecture.architectureDoc = el.checked;
        break;
      case 4:
        // Controls are saved in real-time via raSetControl/raSetComment handlers
        break;
      case 5:
        // Risks are saved in real-time via raToggleRisk/raUpdateRiskField/raAddCustomRisk handlers
        break;
      case 6:
        // Save finalRecs inline (Mesures & Revue step)
        if (!a.finalRecs) a.finalRecs = { items: [] };
        a.finalRecs.items.forEach(function(item, idx) {
          el = document.getElementById('raFrTitle_' + idx); if (el) item.title = el.value;
          el = document.getElementById('raFrDetails_' + idx); if (el) item.details = el.value;
        });
        // Auto-clean empty items
        a.finalRecs.items = a.finalRecs.items.filter(function(it) { return it.title || it.details; });
        // Review date
        el = document.getElementById('raReviewEnabled');
        if (el) {
          if (el.checked) {
            var el2 = document.getElementById('raNextReview');
            a.meta.nextReviewDate = el2 ? el2.value : null;
          } else {
            a.meta.nextReviewDate = null;
          }
        }
        break;
      case 7:
        // Save Analyse de sécurité editable fields (Results step)
        if (!a.spot) a.spot = {};
        el = document.getElementById('raSpotOpenAdj'); if (el) a.spot.openRiskAdjustment = parseInt(el.value) || 0;
        el = document.getElementById('raSpotAvisComment'); if (el) a.spot.avisComment = el.value;
        el = document.getElementById('raSpotAvisOverride'); if (el) { a.spot.avisOverrideLight = el.value || null; }
        el = document.getElementById('raSpotAvisOverrideJustif'); if (el) a.spot.avisOverrideJustification = el.value;
        el = document.getElementById('raSpotResOverride'); if (el) { a.spot.residualOverrideScore = el.value ? parseInt(el.value, 10) : null; }
        el = document.getElementById('raSpotResJustif'); if (el) a.spot.residualOverrideJustification = el.value;
        el = document.getElementById('raSpotResCorrected'); if (el) a.spot.residualCorrectedComment = el.value;
        break;
    }
    a.meta.updatedAt = _now();
    _save();
  }

  // ============================================================
  // PDF EXPORT
  // ============================================================
  function generateRiskAnalysisPDF(a) {
    if (typeof jspdf === 'undefined' && typeof window.jspdf === 'undefined') {
      if (typeof window.showToast2 === 'function') window.showToast2('jsPDF non disponible.', 'error');
      return;
    }
    // Ensure results computed
    if (!a.results) a.results = computeAllResults(a);
    // Assign report number on first export
    if (!a.meta.reportNumber) a.meta.reportNumber = _generateReportNumber();
    a.meta.reportDate = new Date().toISOString().slice(0, 10);
    _save();
    var jsPDF = (window.jspdf && window.jspdf.jsPDF) || jspdf.jsPDF;
    var doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
    var M = { left: 20, right: 20, top: 28, bottom: 18 };
    var PW = 210 - M.left - M.right;
    var PH = 297;
    var y = M.top;
    var r = a.results;
    var today = a.meta.reportDate || new Date().toISOString().slice(0, 10);
    var docTitle = _t('riskAnalysis.pdfTitle', 'Analyse des risques en sécurité de l\'information');
    var sectionNum = 0;
    // Theme-aware colors from CSS custom properties
    var themeStyles = getComputedStyle(document.body || document.documentElement);
    function _readCssColor(varName, fallback) { var hex = (themeStyles.getPropertyValue(varName) || '').trim() || fallback; return _hexToRgb(hex); }
    var COLORS = {
      primary: _readCssColor('--color-primary', '#3B82F6'),
      success: _readCssColor('--color-success', '#22C55E'),
      warning: _readCssColor('--color-warning', '#F97316'),
      danger: _readCssColor('--color-danger', '#EF4444'),
      muted: [107, 114, 128],
      headerBg: [241, 245, 249],
      alternateBg: [248, 250, 252]
    };

    // --- PDF metadata ---
    doc.setProperties({
      title: docTitle + ' - ' + (a.meta.title || ''),
      author: 'Cyber-Assistant',
      subject: _t('riskAnalysis.pdfSubject', 'Rapport d\'analyse de risques'),
      creator: 'Cyber-Assistant SMSI'
    });

    // --- Utility functions ---
    function addHeader() {
      var pw = doc.internal.pageSize.getWidth();
      doc.setDrawColor(200);
      doc.setLineWidth(0.3);
      doc.line(M.left, 16, pw - M.right, 16);
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(7);
      doc.setTextColor(120);
      var headerTitle = docTitle;
      if (headerTitle.length > 55) headerTitle = headerTitle.substring(0, 52) + '...';
      doc.text(headerTitle + ' — ' + (a.meta.title || ''), M.left, 12);
      doc.text(today, pw - M.right, 12, { align: 'right' });
      doc.setTextColor(0);
    }
    function addFooter(pageNum) {
      var pw = doc.internal.pageSize.getWidth();
      var ph = doc.internal.pageSize.getHeight();
      doc.setDrawColor(200);
      doc.setLineWidth(0.3);
      doc.line(M.left, ph - 12, pw - M.right, ph - 12);
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(8);
      doc.setTextColor(120);
      var footerLeft = (a.meta.reportNumber || '') + ' — ' + today;
      doc.text(footerLeft, M.left, ph - 7);
      doc.text('Page ' + pageNum, pw / 2, ph - 7, { align: 'center' });
      doc.text('Cyber-Assistant', pw - M.right, ph - 7, { align: 'right' });
      doc.setTextColor(0);
    }
    function addPage() { doc.addPage(); y = M.top; }
    function checkPage(needed) { if (y + needed > PH - M.bottom) addPage(); }
    function sTitle(text, size) {
      sectionNum++;
      checkPage(14);
      var label = sectionNum + '. ' + text;
      doc.setFontSize(size || 14);
      doc.setFont(undefined, 'bold');
      doc.setTextColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
      doc.text(label, M.left, y);
      y += 2;
      doc.setDrawColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
      doc.setLineWidth(0.5);
      doc.line(M.left, y, M.left + PW, y);
      y += (size || 14) * 0.4 + 4;
      doc.setFont(undefined, 'normal');
      doc.setFontSize(10);
      doc.setTextColor(0);
      doc.setDrawColor(0);
    }
    function text(t, indent) {
      if (!t) return;
      checkPage(8);
      var lines = doc.splitTextToSize(String(t), PW - (indent || 0));
      doc.text(lines, M.left + (indent || 0), y);
      y += lines.length * 4.5;
    }
    function labelValue(label, value) {
      checkPage(8);
      doc.setFont(undefined, 'bold');
      doc.setFontSize(9);
      doc.text(label + ': ', M.left, y);
      var lw = doc.getTextWidth(label + ': ');
      doc.setFont(undefined, 'normal');
      var valLines = doc.splitTextToSize(String(value || 'N/A'), PW - lw);
      doc.text(valLines, M.left + lw, y);
      y += valLines.length * 4.5;
      doc.setFontSize(10);
    }
    function lightCircle(x, yc, light) {
      var colors = { green: [34,197,94], yellow: [249,115,22], red: [239,68,68], grey: [200,200,200] };
      var c = colors[light] || colors.grey;
      doc.setFillColor(c[0], c[1], c[2]);
      doc.circle(x, yc, 2.5, 'F');
    }
    function drawTrafficLightPDF(x, yStart, activeLight) {
      doc.setFillColor(30, 30, 30);
      doc.roundedRect(x, yStart, 16, 40, 3, 3, 'F');
      var centers = [
        { cy: yStart + 9, light: 'red' },
        { cy: yStart + 20, light: 'yellow' },
        { cy: yStart + 31, light: 'green' }
      ];
      var activeColors = { red: [239, 68, 68], yellow: [249, 195, 80], green: [34, 197, 94] };
      centers.forEach(function(c) {
        if (c.light === activeLight) {
          var clr = activeColors[c.light];
          doc.setFillColor(clr[0], clr[1], clr[2]);
        } else {
          doc.setFillColor(70, 70, 70);
        }
        doc.circle(x + 8, c.cy, 4.5, 'F');
      });
    }
    var tableDefaults = {
      margin: { left: M.left, right: M.right },
      tableWidth: PW,
      styles: { fontSize: 8, cellPadding: 2.5, lineColor: [220, 220, 220], lineWidth: 0.2 },
      headStyles: { fillColor: COLORS.primary, textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 8 },
      alternateRowStyles: { fillColor: COLORS.alternateBg }
    };

    // ==========================================================
    // PAGE DE GARDE PROFESSIONNELLE
    // ==========================================================
    doc.setFillColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
    doc.rect(0, 0, 210, 70, 'F');
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(20);
    doc.setTextColor(255);
    var titleLines = doc.splitTextToSize(docTitle, 170);
    var titleStartY = titleLines.length > 1 ? 28 : 35;
    doc.text(titleLines, 105, titleStartY, { align: 'center' });
    doc.setFontSize(13);
    doc.text(a.meta.title || _t('riskAnalysis.pdfUntitled', 'Sans titre'), 105, titleLines.length > 1 ? 52 : 52, { align: 'center' });

    // Optional logo (preserve aspect ratio, max 60×30mm, centered)
    var coverY = 82;
    if (window.appData && window.appData.settings && window.appData.settings.logo) {
      try {
        var logoProps = doc.getImageProperties(window.appData.settings.logo);
        var maxW = 60, maxH = 30;
        var logoRatio = Math.min(maxW / logoProps.width, maxH / logoProps.height);
        var logoW = logoProps.width * logoRatio, logoH = logoProps.height * logoRatio;
        var logoX = (210 - logoW) / 2;
        doc.addImage(window.appData.settings.logo, logoProps.fileType || 'PNG', logoX, coverY, logoW, logoH);
        coverY += logoH + 8;
      } catch(e) { /* skip */ }
    }

    // Identity block
    var ident = a.identity || {};
    doc.setTextColor(50);
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(10);
    var identFields = [
      { label: _t('riskAnalysis.pdfBeneficiary','Bénéficiaire/Client'), value: ident.beneficiary },
      { label: _t('riskAnalysis.pdfProjectManager','Chef de projet'), value: ident.projectManager },
      { label: _t('riskAnalysis.pdfControlBody','Organisme de contrôle'), value: ident.controlBody },
      { label: _t('riskAnalysis.pdfEncoder','Encodage'), value: ident.encoder },
      { label: _t('riskAnalysis.pdfValidator','Validation'), value: ident.validator }
    ];
    identFields.forEach(function(f) {
      if (f.value) {
        doc.setFont('helvetica', 'bold');
        doc.text(f.label + ' : ', M.left + 20, coverY);
        var lw = doc.getTextWidth(f.label + ' : ');
        doc.setFont('helvetica', 'normal');
        doc.text(f.value, M.left + 20 + lw, coverY);
        coverY += 7;
      }
    });
    coverY += 6;

    // Report metadata
    doc.setFontSize(10);
    doc.setTextColor(80);
    if (a.meta.reportNumber) {
      doc.text(_t('riskAnalysis.pdfReport', 'Rapport') + ' : ' + a.meta.reportNumber, 105, coverY, { align: 'center' });
      coverY += 7;
    }
    doc.text(_t('riskAnalysis.pdfDate', 'Date') + ' : ' + today, 105, coverY, { align: 'center' });
    coverY += 7;
    doc.text(_t('riskAnalysis.pdfStatus', 'Statut') + ' : ' + (a.meta.status === 'complete' ? _t('riskAnalysis.pdfComplete', 'Terminée') : _t('riskAnalysis.pdfDraft', 'Brouillon')), 105, coverY, { align: 'center' });
    coverY += 16;

    // Spot summary on cover (traffic light + avis + summary table)
    if (r.spotSummary) {
      var spCover = r.spotSummary;
      drawTrafficLightPDF(210 - M.right - 18, coverY, spCover.light);
      var avisColorsCover = { red: [239, 68, 68], yellow: [200, 150, 30], green: [34, 197, 94] };
      var acCover = avisColorsCover[spCover.light] || [0, 0, 0];
      doc.setFontSize(11);
      doc.setFont('helvetica', 'bold');
      doc.setTextColor(acCover[0], acCover[1], acCover[2]);
      var coverAvisLines = doc.splitTextToSize(spCover.avisLabel || '', PW - 30);
      doc.text(coverAvisLines, M.left, coverY + 4);
      doc.setTextColor(0);
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(10);
      coverY += coverAvisLines.length * 5 + 6;
      var spotRowsCover = [
        [_t('riskAnalysis.pdfInherentCriticality', 'Criticité de l\'actif (risque inhérent)'), spCover.inherentCriticalityScore + '/5'],
        [_t('riskAnalysis.pdfMeasuresImpl', 'Niveau d\'implémentation des mesures'), spCover.measuresImplementationPercent + '%'],
        [_t('riskAnalysis.pdfResidualLevel', 'Niveau de risque résiduel'), spCover.residualScore + '/5'],
        [_t('riskAnalysis.pdfResidualPlusOpen', 'Risque résiduel + risque ouvert'), spCover.residualPlusOpenScore + '/5']
      ];
      if ((a.spot || {}).avisComment) spotRowsCover.push([_t('riskAnalysis.pdfSecurityComment', 'Commentaire avis Sécurité'), a.spot.avisComment]);
      if (spCover.avisOverridden && (a.spot || {}).avisOverrideJustification) spotRowsCover.push([_t('riskAnalysis.spot.avisOverrideJustifLabel', 'Justification correction avis'), a.spot.avisOverrideJustification]);
      if ((a.spot || {}).residualCorrectedComment) spotRowsCover.push([_t('riskAnalysis.pdfResidualComment', 'Commentaire résiduel corrigé'), a.spot.residualCorrectedComment]);
      doc.autoTable(Object.assign({}, tableDefaults, {
        startY: coverY, body: spotRowsCover,
        tableWidth: PW - 25,
        columnStyles: { 0: { cellWidth: 80, fontStyle: 'bold' } },
        didParseCell: function(data) {
          if (data.column.index === 1 && data.section === 'body') {
            var ri = data.row.index;
            if (ri === 0) { var v0 = spCover.inherentCriticalityScore; data.cell.styles.textColor = v0 >= 4 ? COLORS.danger : v0 === 3 ? [200,150,30] : COLORS.success; }
            else if (ri === 1) { var v1 = spCover.measuresImplementationPercent; data.cell.styles.textColor = v1 >= 70 ? COLORS.success : v1 >= 40 ? [200,150,30] : COLORS.danger; }
            else if (ri === 2) { var v2 = spCover.residualScore; data.cell.styles.textColor = v2 <= 2 ? COLORS.success : v2 === 3 ? [200,150,30] : COLORS.danger; }
            else if (ri === 3) { var v3 = spCover.residualPlusOpenScore; data.cell.styles.textColor = v3 <= 2 ? COLORS.success : v3 === 3 ? [200,150,30] : COLORS.danger; }
          }
        }
      }));
    }

    // Footer
    doc.setFontSize(9);
    doc.setTextColor(150);
    doc.setFont('helvetica', 'normal');
    doc.text(_t('riskAnalysis.pdfConfidential', 'Document confidentiel'), 105, PH - 25, { align: 'center' });
    doc.setFontSize(8);
    doc.text(_t('riskAnalysis.pdfGeneratedBy', 'Généré par Cyber-Assistant'), 105, PH - 18, { align: 'center' });

    // ==========================================================
    // PAGE MÉTHODOLOGIQUE (hors-numérotation, page 2)
    // ==========================================================
    addPage();

    // Title
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(16);
    doc.setTextColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
    doc.text(_t('riskAnalysis.methodology.title', 'Cadre méthodologique'), M.left, y);
    y += 2;
    doc.setDrawColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
    doc.setLineWidth(0.5);
    doc.line(M.left, y, M.left + PW, y);
    y += 10;
    doc.setDrawColor(0);
    doc.setLineWidth(0.2);

    // Objective
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(11);
    doc.setTextColor(0);
    doc.text(_t('riskAnalysis.methodology.objective', 'Objectif'), M.left, y);
    y += 6;
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(9);
    var methObjLines = doc.splitTextToSize(_t('riskAnalysis.methodology.objectiveText', 'Cette analyse vise à identifier, évaluer et traiter les risques de sécurité de l\'information conformément à la norme ISO/IEC 27005:2022.'), PW);
    doc.text(methObjLines, M.left, y);
    y += methObjLines.length * 4 + 4;

    // Normative Reference (encadré)
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(11);
    doc.text(_t('riskAnalysis.methodology.normativeRef', 'Référence normative'), M.left, y);
    y += 6;
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(9);
    var methRefText = 'ISO/IEC 27005:2022 — ' + _t('riskAnalysis.methodology.normativeRefText', 'Technologies de l\'information — Gestion des risques liés à la sécurité de l\'information.');
    var methRefLines = doc.splitTextToSize(methRefText, PW - 8);
    doc.setFillColor(245, 247, 250);
    doc.roundedRect(M.left, y - 4, PW, methRefLines.length * 4 + 8, 2, 2, 'F');
    doc.text(methRefLines, M.left + 4, y);
    y += methRefLines.length * 4 + 8;

    // 8-step process
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(11);
    doc.setTextColor(0);
    doc.text(_t('riskAnalysis.methodology.processTitle', 'Processus d\'analyse en 8 étapes'), M.left, y);
    y += 6;
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(8);
    var methProcSteps = [
      _t('riskAnalysis.methodology.step1', 'Contexte & périmètre'),
      _t('riskAnalysis.methodology.step2', 'Classification CIA & RGPD'),
      _t('riskAnalysis.methodology.step3', 'Continuité d\'activité'),
      _t('riskAnalysis.methodology.step4', 'Documentation'),
      _t('riskAnalysis.methodology.step5', 'Mesures & Contrôles'),
      _t('riskAnalysis.methodology.step6', 'Identification des risques'),
      _t('riskAnalysis.methodology.step7', 'Résultats & évaluation'),
      _t('riskAnalysis.methodology.step8', 'Traitement du risque')
    ];
    methProcSteps.forEach(function(step, idx) {
      var stepLines = doc.splitTextToSize((idx + 1) + '. ' + step, PW - 6);
      doc.text(stepLines, M.left + 4, y);
      y += stepLines.length * 3.5 + 1.5;
    });
    y += 4;

    // Scoring formula (encadré coloré)
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(11);
    doc.text(_t('riskAnalysis.methodology.scoringTitle', 'Formule de scoring'), M.left, y);
    y += 6;
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(9);
    var methFormula = _t('riskAnalysis.methodology.scoringFormula', 'Risque résiduel = Vraisemblance × Impact × (1 − Maturité contrôles × 0.40)').replace(/\u2212/g, '-');
    var methAdjust = _t('riskAnalysis.methodology.scoringAdjust', 'Ajustements contextuels : +1 si exposition Internet, +1 si CIA élevé, +1 si RTO/RPO strict.');
    var formulaBoxH = 18;
    doc.setFillColor(239, 246, 255);
    doc.setDrawColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
    doc.setLineWidth(0.5);
    doc.roundedRect(M.left, y - 4, PW, formulaBoxH, 2, 2, 'FD');
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(9);
    doc.setTextColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
    doc.text(methFormula, M.left + 4, y + 1);
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(8);
    doc.setTextColor(80);
    doc.text(methAdjust, M.left + 4, y + 8);
    doc.setTextColor(0);
    doc.setDrawColor(0);
    doc.setLineWidth(0.2);
    y += formulaBoxH + 6;

    // Risk Scale (5 colored rectangles)
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(11);
    doc.setTextColor(0);
    doc.text(_t('riskAnalysis.methodology.scaleTitle', 'Échelle des risques'), M.left, y);
    y += 6;
    var scaleColors = [[34,197,94], [132,204,22], [249,115,22], [239,68,68], [153,27,27]];
    var scaleLabels = [
      _t('riskAnalysis.thresholds.levels.minimum', 'Minimum'),
      _t('riskAnalysis.thresholds.levels.faible', 'Faible'),
      _t('riskAnalysis.thresholds.levels.modere', 'Modéré'),
      _t('riskAnalysis.thresholds.levels.eleve', 'Élevé'),
      _t('riskAnalysis.thresholds.levels.tresEleve', 'Très élevé')
    ];
    var scaleW = (PW - 8) / 5;
    for (var si = 0; si < 5; si++) {
      doc.setFillColor(scaleColors[si][0], scaleColors[si][1], scaleColors[si][2]);
      doc.roundedRect(M.left + si * (scaleW + 2), y, scaleW, 8, 1, 1, 'F');
      doc.setFont('helvetica', 'bold');
      doc.setFontSize(7);
      doc.setTextColor(255);
      doc.text(scaleLabels[si], M.left + si * (scaleW + 2) + scaleW / 2, y + 5.5, { align: 'center' });
    }
    doc.setTextColor(0);
    y += 14;

    // PDCA Cycle (4 quadrants)
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(11);
    doc.text(_t('riskAnalysis.methodology.pdcaTitle', 'Cycle PDCA'), M.left, y);
    y += 6;
    var pdcaW = 70, pdcaH = 28;
    var pdcaX = M.left + (PW - pdcaW * 2 - 4) / 2;
    var pdcaItems = [
      { label: 'Plan', text: _t('riskAnalysis.methodology.pdcaPlan', 'Établir le contexte, identifier les risques'), color: [59,130,246], x: pdcaX, y: y },
      { label: 'Do', text: _t('riskAnalysis.methodology.pdcaDo', 'Implémenter les mesures de traitement'), color: [34,197,94], x: pdcaX + pdcaW + 4, y: y },
      { label: 'Act', text: _t('riskAnalysis.methodology.pdcaAct', 'Améliorer, mettre à jour l\'analyse'), color: [239,68,68], x: pdcaX, y: y + pdcaH + 4 },
      { label: 'Check', text: _t('riskAnalysis.methodology.pdcaCheck', 'Surveiller, contrôler, réévaluer'), color: [249,115,22], x: pdcaX + pdcaW + 4, y: y + pdcaH + 4 }
    ];
    pdcaItems.forEach(function(p) {
      doc.setFillColor(p.color[0], p.color[1], p.color[2]);
      doc.roundedRect(p.x, p.y, pdcaW, pdcaH, 2, 2, 'F');
      doc.setFont('helvetica', 'bold');
      doc.setFontSize(10);
      doc.setTextColor(255);
      doc.text(p.label, p.x + pdcaW / 2, p.y + 8, { align: 'center' });
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(7);
      var pLines = doc.splitTextToSize(p.text, pdcaW - 8);
      doc.text(pLines, p.x + pdcaW / 2, p.y + 14, { align: 'center' });
    });
    doc.setTextColor(0);

    // GDPR section in methodology page
    y += pdcaH * 2 + 14;
    if (a.gdpr) {
      doc.setFont('helvetica', 'bold');
      doc.setFontSize(11);
      doc.setTextColor(0);
      doc.text('RGPD / GDPR', M.left, y);
      y += 6;
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(9);
      labelValue(_t('riskAnalysis.gdpr.personalData', 'Données personnelles'), a.gdpr.personalData ? _t('common.yes', 'Oui') : _t('common.no', 'Non'));
      labelValue(_t('riskAnalysis.gdpr.sensitiveData', 'Données sensibles'), a.gdpr.sensitiveData ? _t('common.yes', 'Oui') : _t('common.no', 'Non'));
      var regLabels = { yes: _t('riskAnalysis.gdpr.registryYes', 'Oui'), no: _t('riskAnalysis.gdpr.registryNo', 'Non'), planned: _t('riskAnalysis.gdpr.registryPlanned', 'Planifié') };
      labelValue(_t('riskAnalysis.gdpr.processingRegistry', 'Registre des traitements'), regLabels[a.gdpr.processingRegistry] || a.gdpr.processingRegistry || '-');
    }

    // Next review date
    if (a.meta && a.meta.nextReviewDate) {
      y += 4;
      labelValue(_t('riskAnalysis.reviewDate.title', 'Prochaine revue'), a.meta.nextReviewDate);
    }

    // ==========================================================
    // 1. RÉSUMÉ EXÉCUTIF
    // ==========================================================
    if (y > M.top + 20) addPage(); else { y += 6; }
    sTitle(_t('riskAnalysis.pdfExecSummary', 'Résumé exécutif'), 14);
    var execLight = r.spotSummary ? r.spotSummary.light : r.globalLight;
    var execScore = r.spotSummary ? r.spotSummary.residualPlusOpenScore : r.globalScore;
    var execLightLabels = { green: _t('riskAnalysis.spot.avisLabels.green', 'Favorable'), yellow: _t('riskAnalysis.spot.avisLabels.yellow', 'Favorable sous réserve'), red: _t('riskAnalysis.spot.avisLabels.red', 'Défavorable') };
    labelValue(_t('riskAnalysis.pdfGlobalScore', 'Score global'), execScore + '/5');
    labelValue(_t('riskAnalysis.pdfGlobalLight', 'Feu global'), execLightLabels[execLight] || execLight);
    var avgPercent = 0, domCount = 0;
    RA_DOMAINS.forEach(function(d) { if (r.domainScores[d.id]) { avgPercent += r.domainScores[d.id].percent; domCount++; } });
    avgPercent = domCount > 0 ? Math.round(avgPercent / domCount) : 0;
    labelValue(_t('riskAnalysis.pdfControlPosture', 'Posture contrôles moyenne'), avgPercent + '%');
    y += 5;

    // ==========================================================
    // 2. CONTEXTE & PÉRIMÈTRE
    // ==========================================================
    sTitle(_t('riskAnalysis.pdfContext', 'Contexte & Périmètre'), 14);
    labelValue(_t('riskAnalysis.pdfPerimeter', 'Périmètre'), a.context.perimeter);
    labelValue(_t('riskAnalysis.pdfDescription', 'Description'), a.context.description);
    labelValue(_t('riskAnalysis.pdfStakeholders', 'Parties prenantes'), a.context.stakeholders);
    labelValue(_t('riskAnalysis.pdfSourceFramework', 'Référentiel source'), a.context.sourceFramework);
    labelValue(_t('riskAnalysis.pdfPersonalData', 'Données personnelles'), a.context.personalData ? _t('common.yes', 'Oui') : _t('common.no', 'Non'));
    labelValue(_t('riskAnalysis.pdfSensitivity', 'Sensibilité'), a.context.dataSensitivity);
    if (a.impactMatrixId && window.CSI_ImpactMatrices) {
      var pdfMatrix = CSI_ImpactMatrices.getMatrixById(a.impactMatrixId);
      if (pdfMatrix) labelValue(_t('riskAnalysis.pdfImpactMatrix', 'Matrice d\'impact'), pdfMatrix.name);
    }
    if (a.meta && a.meta.nextReviewDate) {
      labelValue(_t('riskAnalysis.reviewDate.title', 'Prochaine revue'), a.meta.nextReviewDate);
    }
    y += 5;

    // ==========================================================
    // 3. CIA & CONTINUITÉ
    // ==========================================================
    sTitle(_t('riskAnalysis.pdfCIA', 'Classification CIA & Continuité'), 14);
    // CIA visual bars
    var ciaPdfDims = [
      { label: _t('riskAnalysis.pdfConfidentiality', 'Confidentialité'), val: a.cia.confidentiality || 2 },
      { label: _t('riskAnalysis.pdfIntegrity', 'Intégrité'), val: a.cia.integrity || 2 },
      { label: _t('riskAnalysis.pdfAvailability', 'Disponibilité'), val: a.cia.availability || 2 }
    ];
    var ciaBarColors = [[34,197,94], [249,115,22], [239,68,68]]; // green/orange/red for level 1/2/3
    var ciaLblPdf = [_t('riskAnalysis.cia.level1','Faible'), _t('riskAnalysis.cia.level2','Moyen'), _t('riskAnalysis.cia.level3','Élevé')];
    ciaPdfDims.forEach(function(dim) {
      checkPage(12);
      doc.setFont(undefined, 'bold'); doc.setFontSize(9);
      doc.text(dim.label + ':', M.left, y + 4);
      var barX = M.left + 42;
      for (var s = 1; s <= 3; s++) {
        if (s <= dim.val) { var c = ciaBarColors[dim.val - 1]; doc.setFillColor(c[0], c[1], c[2]); }
        else { doc.setFillColor(230, 230, 230); }
        doc.rect(barX + (s - 1) * 22, y, 20, 6, 'F');
      }
      doc.setFont(undefined, 'normal'); doc.setFontSize(8);
      doc.text(dim.val + '/3 — ' + ciaLblPdf[dim.val - 1], barX + 3 * 22 + 4, y + 4);
      y += 10;
    });
    y += 2;
    // RTO/RPO visual gauge
    var rtoPdf = [
      { label: 'RTO', hours: a.continuity.rtoHours || 24 },
      { label: 'RPO', hours: a.continuity.rpoHours || 24 }
    ];
    var rtoZoneColors = [COLORS.danger, COLORS.warning, [249,195,80], COLORS.success];
    var rtoZonePcts = [0.15, 0.25, 0.30, 0.30];
    var rtoZoneLabels = [_t('riskAnalysis.continuity.strict','Très strict'), _t('riskAnalysis.continuity.moderate','Strict'), _t('riskAnalysis.continuity.standard','Standard'), _t('riskAnalysis.continuity.relaxed','Souple')];
    rtoPdf.forEach(function(item) {
      checkPage(14);
      doc.setFont(undefined, 'bold'); doc.setFontSize(9);
      doc.text(item.label + ': ' + item.hours + 'h', M.left, y + 4);
      var gx = M.left + 32, gw = PW - 36;
      var cx = gx;
      for (var zi = 0; zi < 4; zi++) {
        var zw = gw * rtoZonePcts[zi];
        doc.setFillColor(rtoZoneColors[zi][0], rtoZoneColors[zi][1], rtoZoneColors[zi][2]);
        doc.rect(cx, y, zw, 5, 'F');
        cx += zw;
      }
      var mPct = item.hours <= 4 ? 0.10 : item.hours <= 8 ? 0.25 : item.hours <= 24 ? 0.50 : 0.80;
      var mx = gx + gw * mPct;
      doc.setFillColor(30, 30, 30);
      doc.rect(mx - 0.5, y - 1, 1, 7, 'F');
      var zLbl = item.hours <= 4 ? rtoZoneLabels[0] : item.hours <= 8 ? rtoZoneLabels[1] : item.hours <= 24 ? rtoZoneLabels[2] : rtoZoneLabels[3];
      doc.setFont(undefined, 'normal'); doc.setFontSize(7);
      doc.text(zLbl, mx - doc.getTextWidth(zLbl) / 2, y + 9);
      y += 14;
    });
    if (a.continuity && a.continuity.comment) { doc.setFontSize(9); labelValue(_t('riskAnalysis.continuity.commentLabel', 'Commentaire continuité'), a.continuity.comment); }
    if (a.cia.justification) { doc.setFontSize(9); labelValue(_t('riskAnalysis.pdfJustification', 'Justification'), a.cia.justification); }
    y += 5;

    // ==========================================================
    // 4. DOCUMENTATION
    // ==========================================================
    sTitle(_t('riskAnalysis.pdfArchitecture', 'Documentation'), 14);
    var docItems = [
      [_t('riskAnalysis.pdfHosting', 'Hébergement'), a.architecture.hosting],
      [_t('riskAnalysis.pdfInternetExposed', 'Exposé Internet'), a.architecture.internetExposed ? _t('common.yes', 'Oui') : _t('common.no', 'Non')],
      ['SSO', a.architecture.iam.sso ? _t('common.yes', 'Oui') : _t('common.no', 'Non')],
      ['MFA', a.architecture.iam.mfa ? _t('common.yes', 'Oui') : _t('common.no', 'Non')],
      [_t('riskAnalysis.pdfAdminSep', 'Séparation admin'), a.architecture.iam.adminSeparation ? _t('common.yes', 'Oui') : _t('common.no', 'Non')],
      [_t('riskAnalysis.pdfArchitectureDoc', 'Dossier d\'architecture'), a.architecture.architectureDoc ? _t('common.yes', 'Oui') : _t('common.no', 'Non')]
    ];
    var colW = PW / 2, docLineH = 5;
    var docStartY = y;
    var half = Math.ceil(docItems.length / 2);
    doc.setFontSize(9);
    for (var di = 0; di < docItems.length; di++) {
      var col = di < half ? 0 : 1;
      var row = di < half ? di : di - half;
      var dx = M.left + col * colW;
      var dy = docStartY + row * docLineH;
      doc.setFont(undefined, 'bold');
      doc.text(docItems[di][0] + ': ', dx, dy);
      var dlw = doc.getTextWidth(docItems[di][0] + ': ');
      doc.setFont(undefined, 'normal');
      doc.text(String(docItems[di][1] || 'N/A'), dx + dlw, dy);
    }
    y = docStartY + half * docLineH + 2;
    doc.setFontSize(10);

    // ==========================================================
    // 5. MESURES & CONTRÔLES
    // ==========================================================
    addPage();
    sTitle(_t('riskAnalysis.controls.pdfControls', 'Mesures & Contrôles'), 14);

    // Planned controls warning box
    if (r.totalPlannedControls > 0) {
      checkPage(25);
      var warnText = _t('riskAnalysis.controls.improvedWarning', 'Le résultat d\'analyse des risques repose sur le fait que les mesures et contrôles planifiés seront correctement implémentés — il sera indispensable de réaliser une analyse de risque préalablement au lancement en production du projet.');
      var warnLines = doc.splitTextToSize(warnText, PW - 16);
      var warnH = warnLines.length * 4.5 + 18;
      doc.setFillColor(255, 243, 205);
      doc.setDrawColor(249, 115, 22);
      doc.setLineWidth(0.5);
      doc.roundedRect(M.left, y, PW, warnH, 2, 2, 'FD');
      doc.setFont(undefined, 'bold');
      doc.setFontSize(9);
      doc.setTextColor(180, 83, 9);
      doc.text(_t('riskAnalysis.controls.warningTitle', 'Attention — Contrôles planifiés'), M.left + 4, y + 6);
      doc.setFont(undefined, 'normal');
      doc.setFontSize(8);
      doc.setTextColor(120, 53, 0);
      doc.text(warnLines, M.left + 4, y + 14);
      doc.setTextColor(0);
      doc.setDrawColor(0);
      doc.setLineWidth(0.2);
      y += warnH + 6;
    }

    // Status color map for PDF
    var ctrlStatusColors = {
      implemented: COLORS.success,
      partial:     COLORS.warning,
      planned:     COLORS.primary,
      toVerify:    [147, 51, 234],
      no:          COLORS.danger,
      na:          COLORS.muted
    };
    var ctrlStatusLabels = {
      implemented: _t('riskAnalysis.controls.implemented', 'Implémenté'),
      partial:     _t('riskAnalysis.controls.partial', 'Partiel'),
      planned:     _t('riskAnalysis.controls.planned', 'Planifié'),
      toVerify:    _t('riskAnalysis.controls.toVerify', 'À vérifier'),
      no:          _t('riskAnalysis.controls.no', 'Non'),
      na:          _t('riskAnalysis.controls.na', 'N/A')
    };

    if (r.allControlsList) {
      r.allControlsList.forEach(function(dom) {
        checkPage(30);
        doc.setFontSize(10); doc.setFont(undefined, 'bold');
        doc.setTextColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
        doc.text(_t('riskAnalysis.domains.' + dom.domainId, dom.label), M.left, y);
        doc.setTextColor(0); doc.setFont(undefined, 'normal');
        y += 5;
        var ctrlRows = [];
        dom.controls.forEach(function(ctrl) {
          ctrlRows.push([ctrl.text, ctrlStatusLabels[ctrl.status] || ctrl.status, ctrl.comment || '-']);
        });
        doc.autoTable(Object.assign({}, tableDefaults, {
          startY: y,
          head: [[_t('riskAnalysis.pdfControl', 'Contrôle'), _t('riskAnalysis.controls.statusLabel', 'Statut'), _t('riskAnalysis.controls.comment', 'Commentaire')]],
          body: ctrlRows,
          columnStyles: { 0: { cellWidth: 75 }, 1: { cellWidth: 22, halign: 'center' }, 2: { cellWidth: 73 } },
          didParseCell: function(data) {
            if (data.section === 'body' && data.column.index === 1) {
              var sKey = Object.keys(ctrlStatusLabels).find(function(k) { return ctrlStatusLabels[k] === data.cell.raw; });
              if (sKey && ctrlStatusColors[sKey]) {
                data.cell.styles.textColor = ctrlStatusColors[sKey];
                data.cell.styles.fontStyle = 'bold';
              }
            }
          }
        }));
        y = doc.lastAutoTable.finalY + 4;
      });
    }
    y += 4;

    // ==========================================================
    // 6. SYNTHÈSE DES DOMAINES + RADAR CHART
    // ==========================================================
    addPage();
    sTitle(_t('riskAnalysis.pdfDomainSynthesis', 'Synthèse des domaines'), 14);

    // Radar chart export (B4)
    // FIX-RA-PDF-RADAR (audit 2026-06-10) : #raRadarChart n'existe qu'à l'étape 7 du wizard
    // (le DOM du body est remplacé à chaque changement d'étape) → au moment de l'export
    // (étape 9 ou liste, wizard fermé) getElementById renvoyait null et le radar
    // n'apparaissait JAMAIS dans le PDF. On capture le canvas vivant s'il existe encore,
    // sinon on re-rend le radar hors écran avec Chart.js (sans animation → rendu synchrone).
    var radarImg = null;
    var radarCanvas = document.getElementById('raRadarChart');
    if (radarCanvas && radarCanvas.getContext) {
      try { radarImg = radarCanvas.toDataURL('image/png'); } catch(e) { /* canvas tainted — skip */ }
    }
    if (!radarImg && typeof Chart !== 'undefined' && r.domainScores) {
      try {
        var offCanvas = document.createElement('canvas');
        offCanvas.width = 400;
        offCanvas.height = 400;
        var offLabels = [], offData = [];
        RA_DOMAINS.forEach(function(d) {
          offLabels.push(_t('riskAnalysis.domains.' + d.id, _bi(d, 'label', 'labelEn')));
          offData.push(r.domainScores[d.id] ? r.domainScores[d.id].maturity : 0);
        });
        var offChart = new Chart(offCanvas, {
          type: 'radar',
          data: {
            labels: offLabels,
            datasets: [{ label: _t('riskAnalysis.results.maturity','Maturité'), data: offData, backgroundColor: 'rgba(31,184,205,0.2)', borderColor: 'rgba(31,184,205,1)', pointBackgroundColor: 'rgba(31,184,205,1)', borderWidth: 2 }]
          },
          options: { scales: { r: { min: 0, max: 3, ticks: { stepSize: 1 } } }, plugins: { legend: { display: false } }, responsive: false, animation: false, devicePixelRatio: 2 }
        });
        radarImg = offChart.toBase64Image();
        offChart.destroy();
      } catch(e) { radarImg = null; }
    }
    // FIX-RA-PDF-RADAR-GUARD (revue audit 2026-06-11) : addImage hors d'un try faisait
    // échouer TOUT l'export si l'image était dégénérée (canvas de dimension nulle →
    // toDataURL renvoie 'data:,' truthy, ou format refusé par jsPDF). On rejette les
    // data-URL trop courtes et on encadre addImage : en cas d'échec, on saute le radar
    // (comportement d'origine) au lieu de perdre tout le PDF.
    if (radarImg && radarImg.length > 100) {
      try {
        var radarW = 100;
        var radarH = 100;
        var radarX = M.left + (PW - radarW) / 2;
        checkPage(radarH + 10);
        doc.addImage(radarImg, 'PNG', radarX, y, radarW, radarH);
        y += radarH + 8;
      } catch (e) {
        void 0;
      }
    }

    var domRows = [];
    RA_DOMAINS.forEach(function(d) {
      var ds = r.domainScores[d.id] || {};
      var pctLabel = (ds.percent || 0) + '%';
      if (ds.plannedCount > 0) pctLabel += ' (' + ds.plannedCount + ' ' + _t('riskAnalysis.controls.planned', 'Planifié') + ')';
      domRows.push([_bi(d, 'label', 'labelEn'), (ds.maturity || 0) + '/3', pctLabel, ds.light || 'grey']);
    });
    doc.autoTable(Object.assign({}, tableDefaults, {
      startY: y,
      head: [[_t('riskAnalysis.pdfDomain', 'Domaine'), _t('riskAnalysis.pdfMaturity', 'Maturité'), '%', _t('riskAnalysis.pdfLight', 'Feu')]],
      body: domRows,
      bodyStyles: { minCellHeight: 10 },
      columnStyles: {
        0: { cellWidth: 70 },
        1: { cellWidth: 25, halign: 'center' },
        2: { cellWidth: 55 },
        3: { cellWidth: 20, halign: 'center' }
      },
      didParseCell: function(data) {
        if (data.section === 'body' && data.column.index === 3) {
          data.cell.text = [];
        }
      },
      didDrawCell: function(data) {
        if (data.section === 'body' && data.column.index === 2) {
          var pct = parseInt(data.cell.raw) || 0;
          var barX = data.cell.x + 2;
          var barY = data.cell.y + data.cell.height - 2.5;
          var barW = data.cell.width - 4;
          var barH = 1.5;
          doc.setFillColor(230, 230, 230);
          doc.rect(barX, barY, barW, barH, 'F');
          var fillColor = pct >= 70 ? COLORS.success : pct >= 40 ? COLORS.warning : COLORS.danger;
          doc.setFillColor(fillColor[0], fillColor[1], fillColor[2]);
          doc.rect(barX, barY, barW * pct / 100, barH, 'F');
        }
        if (data.section === 'body' && data.column.index === 3) {
          lightCircle(data.cell.x + data.cell.width / 2, data.cell.y + data.cell.height / 2, data.cell.raw);
        }
      }
    }));
    y = doc.lastAutoTable.finalY + 8;

    // ==========================================================
    // 7. REGISTRE DES RISQUES
    // ==========================================================
    if (a.risks && a.risks.length > 0) {
      checkPage(30);
      sTitle(_t('riskAnalysis.pdfRiskRegister', 'Registre des risques'), 14);
      var riskRows = [];
      a.risks.forEach(function(rk) {
        var cat = RA_RISK_CATALOG.find(function(c) { return c.id === rk.catalogId; });
        var rs = r.riskScores.find(function(s) { return s.id === rk.id; }) || {};
        var _treatLblPdf = { reduce: _t('riskAnalysis.risks.treatReduce','Réduire'), accept: _t('riskAnalysis.risks.treatAccept','Accepter'), transfer: _t('riskAnalysis.risks.treatTransfer','Transférer'), avoid: _t('riskAnalysis.risks.treatAvoid','Éviter') };
        riskRows.push([cat ? _bi(cat, 'threat', 'threatEn') : (rk.customTitle || '?'), rk.likelihood||3, rk.impact||3, rs.inherent||'-', rs.residual||'-', rs.light||'grey', _treatLblPdf[rk.treatmentDecision] || rk.treatmentDecision || '-', rk.owner || '-']);
      });
      doc.autoTable(Object.assign({}, tableDefaults, {
        startY: y,
        head: [[_t('riskAnalysis.pdfRisk', 'Risque'), 'L', 'I', _t('riskAnalysis.pdfInherent', 'Inh.'), _t('riskAnalysis.pdfResidual', 'Rés.'), _t('riskAnalysis.pdfLight', 'Feu'), _t('riskAnalysis.pdfTreatment', 'Traitement'), _t('riskAnalysis.risks.owner', 'Propriétaire')]],
        body: riskRows,
        columnStyles: { 0: { cellWidth: 42 }, 1: { cellWidth: 10, halign: 'center' }, 2: { cellWidth: 10, halign: 'center' }, 3: { cellWidth: 14, halign: 'center' }, 4: { cellWidth: 14, halign: 'center' }, 5: { cellWidth: 14, halign: 'center' }, 6: { cellWidth: 30 }, 7: { cellWidth: 36 } },
        didParseCell: function(data) {
          if (data.section === 'body') {
            // Color inherent/residual scores
            if (data.column.index === 3 || data.column.index === 4) {
              var val = parseFloat(data.cell.raw);
              if (!isNaN(val)) {
                data.cell.styles.textColor = val >= 15 ? COLORS.danger : val >= 8 ? COLORS.warning : COLORS.success;
                data.cell.styles.fontStyle = 'bold';
              }
            }
            // Hide text in Feu column (replaced by circle in didDrawCell)
            if (data.column.index === 5) {
              data.cell.text = [''];
            }
          }
        },
        didDrawCell: function(data) {
          if (data.section === 'body' && data.column.index === 5) {
            var rk3 = data.row.index >= 0 ? a.risks[data.row.index] : null;
            var rs3 = (rk3 && r.riskScores.find(function(s) { return s.id === rk3.id; })) || {};
            if (rs3.levelColor) {
              var lc = _hexToRgb(rs3.levelColor);
              doc.setFillColor(lc[0], lc[1], lc[2]);
              doc.circle(data.cell.x + data.cell.width / 2, data.cell.y + data.cell.height / 2, 2.5, 'F');
            } else {
              lightCircle(data.cell.x + data.cell.width / 2, data.cell.y + data.cell.height / 2, data.cell.raw);
            }
          }
        }
      }));
      y = doc.lastAutoTable.finalY + 8;
      // Impact breakdown footnotes
      if (a.impactMatrixId && window.CSI_ImpactMatrices) {
        var pdfM = CSI_ImpactMatrices.getMatrixById(a.impactMatrixId);
        if (pdfM) {
          var hasBreakdown = a.risks.some(function(rk) { return rk.impactBreakdown && Object.keys(rk.impactBreakdown).length > 0; });
          if (hasBreakdown) {
            checkPage(20);
            doc.setFontSize(7);
            doc.setFont(undefined, 'italic');
            doc.setTextColor(COLORS.muted[0], COLORS.muted[1], COLORS.muted[2]);
            doc.text(_t('riskAnalysis.pdfImpactDetail', 'Détail évaluation impact') + ' (' + _t('riskAnalysis.pdfMatrix', 'matrice') + ': ' + pdfM.name + '):', M.left, y);
            y += 4;
            a.risks.forEach(function(rk) {
              if (!rk.impactBreakdown) return;
              var cat2 = RA_RISK_CATALOG.find(function(c) { return c.id === rk.catalogId; });
              var rTitle = cat2 ? _bi(cat2, 'threat', 'threatEn') : (rk.customTitle || '?');
              var parts = [];
              (pdfM.dimensions || []).forEach(function(dim) {
                if (rk.impactBreakdown[dim.id]) parts.push((dim.label || dim.name) + ':' + rk.impactBreakdown[dim.id]);
              });
              if (parts.length > 0) {
                checkPage(5);
                // FIX-RA-PDF-WINANSI (audit 2026-06-10) : '→' (U+2192) n'existe pas dans
                // l'encodage WinAnsi des polices standard jsPDF → glyphe corrompu. Remplacé
                // par '->' (même correctif que le '−' de la formule de scoring).
                doc.text('  • ' + rTitle + ' -> ' + parts.join(', '), M.left, y);
                y += 3.5;
              }
            });
            doc.setFont(undefined, 'normal');
            doc.setFontSize(10);
            doc.setTextColor(0);
            y += 4;
          }
        }
      }
      // 5×5 Risk Matrix in PDF
      checkPage(70);
      doc.setFontSize(10); doc.setFont(undefined, 'bold');
      doc.text(_t('riskAnalysis.thresholds.matrixTitle','Matrice de risque'), M.left, y);
      y += 6;
      var pdfT = getActiveThreshold();
      var pdfRiskMap = {};
      (a.risks || []).forEach(function(rk) { var pk = (rk.likelihood || 3) + '_' + (rk.impact || 3); pdfRiskMap[pk] = (pdfRiskMap[pk] || 0) + 1; });
      var mcs = 16;
      var msx = M.left + 20;
      var msy = y;
      doc.setFontSize(6); doc.setFont(undefined, 'bold');
      for (var mg = 1; mg <= 5; mg++) {
        doc.text(String(mg), msx + (mg - 1) * mcs + mcs / 2, msy - 2, { align: 'center' });
      }
      for (var mp = 5; mp >= 1; mp--) {
        var mry = msy + (5 - mp) * mcs;
        doc.text(String(mp), msx - 4, mry + mcs / 2 + 1, { align: 'right' });
        for (var mg2 = 1; mg2 <= 5; mg2++) {
          var mkey = mp + '_' + mg2;
          var mlid = pdfT.cells[mkey] || 'minimum';
          var mlvl = pdfT.levels.find(function(l) { return l.id === mlid; });
          var mc = _hexToRgb(mlvl ? mlvl.color : '#cccccc');
          doc.setFillColor(mc[0], mc[1], mc[2]);
          var mcx = msx + (mg2 - 1) * mcs;
          doc.rect(mcx, mry, mcs, mcs, 'F');
          var mcnt = pdfRiskMap[mkey] || 0;
          if (mcnt > 0) {
            doc.setFontSize(9); doc.setFont(undefined, 'bold');
            doc.setTextColor(255, 255, 255);
            doc.text(String(mcnt), mcx + mcs / 2, mry + mcs / 2 + 2, { align: 'center' });
            doc.setTextColor(0);
          }
        }
      }
      doc.setFontSize(7); doc.setFont(undefined, 'normal');
      // FIX-RA-PDF-WINANSI (audit 2026-06-10) : '\u2192' (flèche droite) et '\u2191' (flèche haut)
      // n'existent pas dans l'encodage WinAnsi des polices standard jsPDF (glyphes corrompus
      // dans le PDF final). Remplacés par '->' et '^' (même correctif que le '\u2212' ligne ~2900).
      doc.text(_t('riskAnalysis.thresholds.gravity','Gravité') + ' ->', msx + 25, msy - 6);
      doc.text('^ ' + _t('riskAnalysis.thresholds.probability','Probabilité'), msx - 18, msy + 35);
      y = msy + 5 * mcs + 4;
      var lgx = msx;
      doc.setFontSize(6);
      pdfT.levels.forEach(function(lv) {
        var lc = _hexToRgb(lv.color);
        doc.setFillColor(lc[0], lc[1], lc[2]);
        doc.rect(lgx, y, 4, 4, 'F');
        var _lvLbl = _bi(lv, 'label', 'labelEn');
        doc.text(_lvLbl, lgx + 5, y + 3);
        lgx += doc.getTextWidth(_lvLbl) + 10;
      });
      y += 10;
      // Treatment distribution summary
      checkPage(16);
      doc.setFontSize(9); doc.setFont(undefined, 'bold');
      doc.text(_t('riskAnalysis.preview.treatmentDistribution','Répartition des traitements') + ':', M.left, y);
      y += 5;
      var tCpdf = { reduce: 0, accept: 0, transfer: 0, avoid: 0 };
      a.risks.forEach(function(rk) { var td = rk.treatmentDecision || 'reduce'; if (tCpdf[td] !== undefined) tCpdf[td]++; });
      var tLpdf = { reduce: _t('riskAnalysis.risks.treatReduce','Réduire'), accept: _t('riskAnalysis.risks.treatAccept','Accepter'), transfer: _t('riskAnalysis.risks.treatTransfer','Transférer'), avoid: _t('riskAnalysis.risks.treatAvoid','Éviter') };
      var tColPdf = { reduce: COLORS.primary, accept: COLORS.success, transfer: COLORS.warning, avoid: COLORS.danger };
      doc.setFont(undefined, 'normal'); doc.setFontSize(8);
      var totalRisks = a.risks.length;
      if (totalRisks > 0) {
        var barStartX = M.left;
        var barFullW = PW;
        var barH = 10;
        ['reduce','accept','transfer','avoid'].forEach(function(key) {
          var count = tCpdf[key];
          if (count === 0) return;
          var w = barFullW * (count / totalRisks);
          var tc = tColPdf[key];
          doc.setFillColor(tc[0], tc[1], tc[2]);
          doc.rect(barStartX, y, w, barH, 'F');
          if (w > 25) {
            doc.setTextColor(255, 255, 255); doc.setFont(undefined, 'bold');
            doc.text(tLpdf[key] + ' (' + count + ')', barStartX + 2, y + barH - 3);
          }
          barStartX += w;
        });
        doc.setTextColor(0); doc.setFont(undefined, 'normal');
      }
      y += 14;
      doc.setFontSize(10); doc.setFont(undefined, 'normal');
    }

    // ==========================================================
    // 8. RECOMMANDATIONS AUTOMATIQUES
    // ==========================================================
    if (r.recommendations && r.recommendations.length > 0) {
      addPage();
      sTitle(_t('riskAnalysis.pdfRecommendations', 'Recommandations'), 14);
      var recRows = [];
      r.recommendations.forEach(function(rec) {
        var recTitle = rec.title;
        if (rec.isPlanned) recTitle += ' [' + _t('riskAnalysis.controls.planned', 'Planifié') + ']';
        recRows.push([rec.priority, recTitle, rec.domainId, rec.effort, rec.rationale || '']);
      });
      doc.autoTable(Object.assign({}, tableDefaults, {
        startY: y,
        head: [[_t('riskAnalysis.pdfPriority', 'Priorité'), _t('riskAnalysis.pdfRecommendation', 'Recommandation'), _t('riskAnalysis.pdfDomain', 'Domaine'), _t('riskAnalysis.pdfEffort', 'Effort'), _t('riskAnalysis.pdfRationale', 'Rationale')]],
        body: recRows,
        columnStyles: { 0: { cellWidth: 18, halign: 'center' }, 1: { cellWidth: 55 }, 4: { cellWidth: 45 } },
        didParseCell: function(data) {
          if (data.section === 'body' && data.column.index === 0) {
            var p = String(data.cell.raw).toLowerCase();
            if (p === 'haute' || p === 'high') data.cell.styles.textColor = COLORS.danger;
            else if (p === 'moyenne' || p === 'medium') data.cell.styles.textColor = COLORS.warning;
            else data.cell.styles.textColor = COLORS.success;
            data.cell.styles.fontStyle = 'bold';
          }
        }
      }));
      y = doc.lastAutoTable.finalY + 8;
    }

    // ==========================================================
    // 9. HYPOTHÈSES & LIMITES
    // ==========================================================
    checkPage(30);
    sTitle(_t('riskAnalysis.pdfAssumptions', 'Hypothèses & limites'), 14);
    text(_t('riskAnalysis.pdfAssumptionsText1', 'Cette analyse reflète la posture à la date de réalisation. Les scores sont indicatifs et ne remplacent pas un audit formel.'));
    text(_t('riskAnalysis.pdfAssumptionsText2', 'Les recommandations sont générées automatiquement sur la base des réponses fournies.'));
    if (r.totalPlannedControls > 0) {
      text(_t('riskAnalysis.controls.improvedWarning', 'Le résultat d\'analyse des risques repose sur le fait que les mesures et contrôles planifiés seront correctement implémentés — il sera indispensable de réaliser une analyse de risque préalablement au lancement en production du projet.'));
    }

    // ==========================================================
    // 10. MESURES DE MITIGATION PRIORITAIRES (dernière section)
    // ==========================================================
    var frItemsPdf = (a.finalRecs && a.finalRecs.items) ? a.finalRecs.items.filter(function(it) { return it.title || it.details; }) : [];
    if (frItemsPdf.length > 0) {
      addPage();
      sTitle(_t('riskAnalysis.finalRecs.mitigationTitle', 'Mesures de mitigation prioritaires'), 14);
      // Highlight box
      checkPage(16);
      doc.setFillColor(239, 246, 255);
      doc.setDrawColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
      doc.setLineWidth(0.8);
      doc.roundedRect(M.left, y, PW, 10, 2, 2, 'FD');
      doc.setFont(undefined, 'bold');
      doc.setFontSize(9);
      doc.setTextColor(COLORS.primary[0], COLORS.primary[1], COLORS.primary[2]);
      doc.text(_t('riskAnalysis.finalRecs.mitigationSubtitle', 'Actions prioritaires identifiées pour réduire le niveau de risque'), M.left + 4, y + 6);
      doc.setTextColor(0);
      doc.setFont(undefined, 'normal');
      doc.setDrawColor(0);
      doc.setLineWidth(0.2);
      y += 14;
      var frRows = [];
      frItemsPdf.forEach(function(item, i) {
        frRows.push([(i + 1).toString(), item.title || '', item.details || '', item.status === 'done' ? _t('riskAnalysis.pdfDone', 'Fait') : _t('riskAnalysis.pdfTodo', 'À faire')]);
      });
      doc.autoTable(Object.assign({}, tableDefaults, {
        startY: y,
        head: [['#', _t('riskAnalysis.pdfRecTitle', 'Titre'), _t('riskAnalysis.pdfRecDetails', 'Détails'), _t('riskAnalysis.pdfRecStatus', 'Statut')]],
        body: frRows,
        columnStyles: { 0: { cellWidth: 10, halign: 'center' }, 1: { cellWidth: 42 }, 2: { cellWidth: 88 } },
        headStyles: { fillColor: COLORS.primary, textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 8 },
        didParseCell: function(data) {
          if (data.section === 'body' && data.column.index === 3) {
            data.cell.styles.textColor = data.cell.raw === _t('riskAnalysis.pdfDone', 'Fait') ? COLORS.success : COLORS.warning;
            data.cell.styles.fontStyle = 'bold';
          }
        }
      }));
      y = doc.lastAutoTable.finalY + 8;
    }

    // ==========================================================
    // POST-PROCESSING: HEADERS & FOOTERS
    // ==========================================================
    var totalPages = doc.internal.getNumberOfPages();

    // Add headers & footers on all pages except cover (page 1)
    for (var p = 2; p <= totalPages; p++) {
      doc.setPage(p);
      addHeader();
      addFooter(p);
    }

    // ==========================================================
    // SAVE
    // ==========================================================
    doc.setTextColor(0);
    var titleSlug = (a.meta.title || 'export').replace(/[^a-zA-Z0-9]/g, '_').substring(0, 40);
    var filename = (a.meta.reportNumber || 'RA') + '_' + titleSlug + '_' + today + '.pdf';
    doc.save(filename);
    if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.pdfGenerated', 'PDF généré') + ': ' + filename, 'success');
  }

  // ============================================================
  // IMPACT MATRIX PICKER MODAL
  // ============================================================
  var _pickerRiskIdx = null;

  window.raOpenImpactPicker = function(riskIdx) {
    var a = findAnalysis(wizardState.analysisId);
    if (!a || !a.risks[riskIdx] || !a.impactMatrixId || !window.CSI_ImpactMatrices) return;
    var matrix = CSI_ImpactMatrices.getMatrixById(a.impactMatrixId);
    if (!matrix) {
      if (typeof window.showToast2 === 'function') window.showToast2(_t('riskAnalysis.impactMatrix.matrixNotFound','Matrice introuvable'), 'error');
      return;
    }
    _pickerRiskIdx = riskIdx;
    var r = a.risks[riskIdx];
    var cat = RA_RISK_CATALOG.find(function(c) { return c.id === r.catalogId; });
    var riskTitle = cat ? _bi(cat, 'threat', 'threatEn') : (r.customTitle || _t('riskAnalysis.risks.customTitle','Risque personnalisé'));
    var bd = r.impactBreakdown || {};

    var html = '<div class="ra-impact-modal-overlay" id="raImpactModal">';
    html += '<div class="ra-impact-modal">';
    // Header
    html += '<div class="ra-impact-modal__header"><h3><i class="fas fa-table-cells"></i> ' + _esc(_t('riskAnalysis.impactMatrix.modalTitle','Évaluation d\'impact')) + '</h3>';
    html += '<button class="ra-impact-modal__close" onclick="raCloseImpactPicker()" title="' + _esc(_t('riskAnalysis.impactMatrix.cancel','Annuler')) + '">&times;</button></div>';
    // Body
    html += '<div class="ra-impact-modal__body">';
    html += '<div class="ra-impact-modal__hint"><strong>' + _esc(riskTitle) + '</strong><br>';
    html += _esc(_t('riskAnalysis.impactMatrix.modalHint','Pour chaque dimension, sélectionnez le niveau d\'impact qui correspond le mieux à ce risque. Le texte descriptif vous aide à choisir le bon niveau. L\'impact global est calculé automatiquement (maximum des niveaux sélectionnés).'));
    html += '</div>';

    // Dimensions
    (matrix.dimensions || []).forEach(function(dim) {
      html += '<div class="ra-impact-dim-row" data-dim="' + _esc(dim.id) + '">';
      html += '<div class="ra-impact-dim-label">' + _esc(dim.label || dim.name) + '</div>';
      html += '<div class="ra-impact-dim-levels">';
      (matrix.levels || []).forEach(function(lv) {
        var sel = bd[dim.id] === lv.level ? ' ra-impact-level-btn--selected' : '';
        html += '<button type="button" class="ra-impact-level-btn' + sel + '" data-dim="' + _esc(dim.id) + '" data-level="' + lv.level + '" onclick="raPickerSelectLevel(this)" title="' + _esc(lv.label) + '">' + lv.level + '</button>';
      });
      html += '</div>';
      // Description area
      var descText = '';
      if (bd[dim.id] && matrix.cells && matrix.cells[dim.id]) {
        descText = matrix.cells[dim.id][String(bd[dim.id])] || '';
      }
      html += '<div class="ra-impact-dim-desc" id="raPickDesc_' + _esc(dim.id) + '">' + _esc(descText) + '</div>';
      html += '</div>';
    });
    html += '</div>';

    // Footer
    var computedImpact = _pickerComputeImpact(bd);
    var aggLabel = _t('riskAnalysis.impactMatrix.aggregationMax','le niveau maximum parmi les dimensions évaluées');
    html += '<div class="ra-impact-modal__footer">';
    html += '<div class="ra-impact-computed">' + _esc(_t('riskAnalysis.impactMatrix.computedImpact','Impact global')) + ' : <strong id="raPickerScore">' + (computedImpact || '–') + '</strong><br><span style="font-size:0.75rem;font-weight:400;color:var(--color-text-light,#9ca3af)">' + _esc(_t('riskAnalysis.impactMatrix.computedMethod','Méthode de calcul')) + ' : ' + _esc(aggLabel) + '</span></div>';
    html += '<div><button class="btn" onclick="raCloseImpactPicker()">' + _esc(_t('riskAnalysis.impactMatrix.cancel','Annuler')) + '</button> ';
    html += '<button class="btn btn--primary" onclick="raApplyImpactPicker()">' + _esc(_t('riskAnalysis.impactMatrix.apply','Appliquer')) + '</button></div>';
    html += '</div></div></div>';

    // Insert into DOM
    var existing = document.getElementById('raImpactModal');
    if (existing) existing.remove();
    var wrapper = document.createElement('div');
    wrapper.innerHTML = html;
    document.body.appendChild(wrapper.firstElementChild);
  };

  window.raPickerSelectLevel = function(btn) {
    var dimId = btn.getAttribute('data-dim');
    var level = parseInt(btn.getAttribute('data-level'));
    // Toggle: if already selected, deselect
    var wasSelected = btn.classList.contains('ra-impact-level-btn--selected');
    // Remove selection on sibling buttons
    var parent = btn.parentElement;
    var siblings = parent.querySelectorAll('.ra-impact-level-btn');
    for (var i = 0; i < siblings.length; i++) siblings[i].classList.remove('ra-impact-level-btn--selected');
    if (!wasSelected) {
      btn.classList.add('ra-impact-level-btn--selected');
    }
    // Update description
    _pickerUpdateDesc(dimId);
    // Update computed score
    _pickerUpdateScore();
  };

  function _pickerUpdateDesc(dimId) {
    var a = findAnalysis(wizardState.analysisId);
    if (!a || !a.impactMatrixId || !window.CSI_ImpactMatrices) return;
    var matrix = CSI_ImpactMatrices.getMatrixById(a.impactMatrixId);
    if (!matrix) return;
    var descEl = document.getElementById('raPickDesc_' + dimId);
    if (!descEl) return;
    var modal = document.getElementById('raImpactModal');
    if (!modal) return;
    var selBtn = null;
    var allSel = modal.querySelectorAll('.ra-impact-level-btn--selected');
    for (var si = 0; si < allSel.length; si++) { if (allSel[si].getAttribute('data-dim') === dimId) { selBtn = allSel[si]; break; } }
    if (selBtn && matrix.cells && matrix.cells[dimId]) {
      descEl.textContent = matrix.cells[dimId][selBtn.getAttribute('data-level')] || '';
    } else {
      descEl.textContent = '';
    }
  }

  function _pickerUpdateScore() {
    var bd = _pickerReadBreakdown();
    var score = _pickerComputeImpact(bd);
    var scoreEl = document.getElementById('raPickerScore');
    if (scoreEl) scoreEl.textContent = score || '–';
  }

  function _pickerReadBreakdown() {
    var modal = document.getElementById('raImpactModal');
    if (!modal) return {};
    var bd = {};
    var selected = modal.querySelectorAll('.ra-impact-level-btn--selected');
    for (var i = 0; i < selected.length; i++) {
      bd[selected[i].getAttribute('data-dim')] = parseInt(selected[i].getAttribute('data-level'));
    }
    return bd;
  }

  function _pickerComputeImpact(breakdown) {
    var vals = [];
    for (var k in breakdown) { if (breakdown.hasOwnProperty(k)) vals.push(breakdown[k]); }
    if (vals.length === 0) return 0;
    return Math.max.apply(null, vals);
  }

  window.raApplyImpactPicker = function() {
    var a = findAnalysis(wizardState.analysisId);
    if (!a || _pickerRiskIdx === null || !a.risks[_pickerRiskIdx]) return;
    var bd = _pickerReadBreakdown();
    var impact = _pickerComputeImpact(bd);
    if (impact > 0) {
      a.risks[_pickerRiskIdx].impact = impact;
      a.risks[_pickerRiskIdx].impactBreakdown = bd;
    } else {
      a.risks[_pickerRiskIdx].impactBreakdown = null;
    }
    _save();
    raCloseImpactPicker();
    renderStepRisks(document.getElementById('raWizardBody'), a, '');
  };

  window.raCloseImpactPicker = function() {
    var modal = document.getElementById('raImpactModal');
    if (modal) modal.remove();
    _pickerRiskIdx = null;
  };

  // === TEST HARNESS (active uniquement si __RA_TEST_MODE__) ===
  if (window.__RA_TEST_MODE__) {
    window._raInternal = {
      computeDomainMaturity: computeDomainMaturity,
      computeAllDomainScores: computeAllDomainScores,
      computeInherentScore: computeInherentScore,
      computeResidualScore: computeResidualScore,
      riskTrafficLight: riskTrafficLight,
      computeGlobalTrafficLight: computeGlobalTrafficLight,
      computeGlobalScore: computeGlobalScore,
      computeAllResults: computeAllResults,
      resolvePath: resolvePath,
      evaluateCondition: evaluateCondition,
      evaluateRule: evaluateRule,
      generateRecommendations: generateRecommendations,
      createBlankAnalysis: createBlankAnalysis,
      RA_DOMAINS: RA_DOMAINS,
      RA_CONTROLS_SEED: RA_CONTROLS_SEED,
      RA_RISK_CATALOG: RA_RISK_CATALOG,
      RA_RECOMMENDATIONS_RULES: RA_RECOMMENDATIONS_RULES,
      _esc: _esc,
      _clamp: _clamp,
      computeSpotSummary: computeSpotSummary,
      _generateReportNumber: _generateReportNumber
    };
  }

})();
