/* global docx */
// ============================================================================
// EX-ANTE — Native .docx export (Office Open XML, via the docx library)
// ============================================================================
// Generates a proper Word document with:
//   - Title page (centered, large)
//   - Decision banner (coloured paragraph)
//   - Auto-numbered headings (H1/H2/H3)
//   - Real Word tables (not HTML-faked)
//   - Page breaks between major sections
//   - Header on every page after the cover
// The `docx` UMD bundle loaded in MELR.html exposes `window.docx`.
//
// Exposes window.exporteExanteDocx(payload) — same payload shape as
// ExanteReport / ExanteReportModal. Triggers a browser download.
// ============================================================================

(function () {
  // Guard: docx might not be loaded yet (offline, blocked CDN, etc.). Fall
  // back to a no-op that the UI can detect via the existence of the function.
  if (typeof docx === "undefined" || !docx) {
    window.exportExanteDocx = function () {
      window.alert("La librairie docx n'a pas pu être chargée (CDN inaccessible). Utilisez l'export Word (.doc) ou PDF à la place.\nThe docx library could not be loaded (CDN unreachable). Use the Word (.doc) or PDF export instead.");
    };
    window.exportExanteDocxAvailable = false;
    return;
  }
  window.exportExanteDocxAvailable = true;

  const {
    Document, Packer,
    Paragraph, TextRun, HeadingLevel, AlignmentType,
    Table, TableRow, TableCell, WidthType,
    BorderStyle, ShadingType,
    Header, Footer, PageBreak,
    LevelFormat,
    ImageRun,
    PageOrientation,
  } = docx;

  // ---------- formatters ----------
  function fmtNum(v) { if (v == null || !isFinite(v)) return "—"; return Math.round(v).toLocaleString(); }
  function fmtPct(v) { if (v == null || !isFinite(v)) return "—"; return (v * 100).toFixed(1) + "%"; }
  function fmtRatio(v) { if (v == null || !isFinite(v)) return "—"; if (v >= 999) return "∞"; return Number(v).toFixed(2); }
  function fmtCcy(v, ccy) { if (v == null || !isFinite(v)) return "—"; return Math.round(v).toLocaleString() + " " + (ccy || "EUR"); }
  function safe(v) { return v == null || v === "" ? "—" : String(v); }

  // ---------- threshold-aware shading colours ----------
  // Tile shading colours — match the HTML report's pastel palette so the
  // rendered Word file looks like a screenshot of the live dashboard.
  const SHADE = {
    ok:      { fill: "DCFCE7", fg: "15803D" },
    bad:     { fill: "FEE2E2", fg: "B91C1C" },
    amber:   { fill: "FEF3C7", fg: "A16207" },
    neutral: { fill: "F3F4F6", fg: "374151" },
    accent:  { fill: "E0E7FF", fg: "1E3A8A" },
  };
  function statusFor(value, threshold, dir) {
    if (value == null || !isFinite(value)) return "neutral";
    if (dir === "<") return value <= threshold ? "ok" : "bad";
    return value >= threshold ? "ok" : "bad";
  }
  function pctStatus(value) {
    if (value == null || !isFinite(value)) return "neutral";
    if (value >= 0.75) return "ok";
    if (value >= 0.60) return "amber";
    return "bad";
  }
  // RGBA-like fill for the sensitivity heatmap — converts a fraction
  // around the threshold into a soft green/red shading hex code.
  function heatmapFill(value, threshold, dir) {
    if (value == null || !isFinite(value)) return "F3F4F6";
    let delta;
    if (dir === "<") delta = (threshold - value) / Math.max(Math.abs(threshold), 1);
    else delta = (value - threshold) / Math.max(Math.abs(threshold), 1);
    delta = Math.max(-1, Math.min(1, delta));
    if (delta >= 0) {
      // green: from D1FAE5 (very pale) to 86EFAC (medium)
      const t = Math.min(1, delta * 1.2);
      const g = Math.round(0xFA - t * (0xFA - 0xEF));     // 250 -> 239
      const r = Math.round(0xD1 - t * (0xD1 - 0x86));     // 209 -> 134
      const b = Math.round(0xE5 - t * (0xE5 - 0xAC));     // 229 -> 172
      return r.toString(16).padStart(2, "0").toUpperCase()
           + g.toString(16).padStart(2, "0").toUpperCase()
           + b.toString(16).padStart(2, "0").toUpperCase();
    } else {
      // red: from FEE2E2 to FCA5A5
      const t = Math.min(1, -delta * 1.2);
      const r = Math.round(0xFE - t * (0xFE - 0xFC));
      const g = Math.round(0xE2 - t * (0xE2 - 0xA5));
      const b = Math.round(0xE2 - t * (0xE2 - 0xA5));
      return r.toString(16).padStart(2, "0").toUpperCase()
           + g.toString(16).padStart(2, "0").toUpperCase()
           + b.toString(16).padStart(2, "0").toUpperCase();
    }
  }

  // ---------- low-level helpers ----------
  function P(text, opts) {
    opts = opts || {};
    const runs = Array.isArray(text)
      ? text.map((t) => (t instanceof TextRun ? t : new TextRun(typeof t === "string" ? t : t)))
      : [new TextRun({ text: text || "", bold: opts.bold, italics: opts.italics, size: opts.size, color: opts.color })];
    return new Paragraph({
      children: runs,
      heading: opts.heading,
      alignment: opts.alignment,
      spacing: opts.spacing || { before: 0, after: 80 },
      pageBreakBefore: opts.pageBreakBefore,
      shading: opts.shading,
    });
  }

  function H1(text) { return P(text, { heading: HeadingLevel.HEADING_1, spacing: { before: 240, after: 120 }, pageBreakBefore: true }); }
  function H1NoBreak(text) { return P(text, { heading: HeadingLevel.HEADING_1, spacing: { before: 240, after: 120 } }); }
  function H2(text) { return P(text, { heading: HeadingLevel.HEADING_2, spacing: { before: 160, after: 80 } }); }
  function H3(text) { return P(text, { heading: HeadingLevel.HEADING_3, spacing: { before: 120, after: 60 } }); }

  // Make a quick table with header row + data rows.
  // columns = [{ label, width:number (pct of 100), align: "left"|"right"|"center" }]
  // rows = array of cell arrays (string | { text, bold?, color? })
  function buildTable(columns, rows) {
    const headerCells = columns.map((c) => new TableCell({
      width: { size: c.width || (100 / columns.length), type: WidthType.PERCENTAGE },
      shading: { type: ShadingType.CLEAR, fill: "F3F4F6" },
      children: [new Paragraph({
        children: [new TextRun({ text: c.label, bold: true, size: 18 })],
        alignment: c.align === "right" ? AlignmentType.RIGHT : c.align === "center" ? AlignmentType.CENTER : AlignmentType.LEFT,
        spacing: { before: 40, after: 40 },
      })],
    }));
    const dataRows = rows.map((cells) => new TableRow({
      children: columns.map((c, i) => {
        const cell = cells[i];
        const text = cell == null ? "—" : (typeof cell === "string" || typeof cell === "number" ? String(cell) : (cell.text != null ? String(cell.text) : "—"));
        const bold = typeof cell === "object" && cell !== null && cell.bold;
        const color = typeof cell === "object" && cell !== null ? cell.color : undefined;
        const shading = typeof cell === "object" && cell !== null && cell.shading
          ? { type: ShadingType.CLEAR, fill: cell.shading }
          : undefined;
        return new TableCell({
          width: { size: c.width || (100 / columns.length), type: WidthType.PERCENTAGE },
          shading,
          children: [new Paragraph({
            children: [new TextRun({ text, bold, color, size: 18 })],
            alignment: c.align === "right" ? AlignmentType.RIGHT : c.align === "center" ? AlignmentType.CENTER : AlignmentType.LEFT,
            spacing: { before: 30, after: 30 },
          })],
        });
      }),
    }));
    return new Table({
      width: { size: 100, type: WidthType.PERCENTAGE },
      rows: [new TableRow({ tableHeader: true, children: headerCells }), ...dataRows],
    });
  }

  // Build a row of "KPI tiles" as a borderless Word table — one cell per
  // tile with header-fill colour matching the live dashboard.
  // tiles = [{ label, value, status: "ok"|"bad"|"amber"|"neutral", sub }]
  function buildKpiTileRow(tiles, perRow) {
    perRow = perRow || 4;
    const rows = [];
    for (let i = 0; i < tiles.length; i += perRow) {
      const slice = tiles.slice(i, i + perRow);
      const cells = slice.map((tile) => {
        const c = SHADE[tile.status || "neutral"];
        return new TableCell({
          width: { size: 100 / perRow, type: WidthType.PERCENTAGE },
          shading: { type: ShadingType.CLEAR, fill: c.fill },
          children: [
            new Paragraph({
              children: [new TextRun({ text: tile.label, bold: true, color: c.fg, size: 16 })],
              spacing: { before: 60, after: 30 },
            }),
            new Paragraph({
              children: [new TextRun({ text: tile.value, bold: true, color: c.fg, size: 28 })],
              spacing: { before: 0, after: 30 },
            }),
            tile.sub ? new Paragraph({
              children: [new TextRun({ text: tile.sub, color: c.fg, size: 14 })],
              spacing: { before: 0, after: 60 },
            }) : new Paragraph({ children: [], spacing: { after: 60 } }),
          ],
        });
      });
      // Pad if last row not full
      while (cells.length < perRow) {
        cells.push(new TableCell({ width: { size: 100 / perRow, type: WidthType.PERCENTAGE }, children: [new Paragraph("")] }));
      }
      rows.push(new TableRow({ children: cells }));
    }
    return new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, rows });
  }

  // Word decision pill — a small shaded paragraph rendered inline-ish via a
  // single-cell table.
  function decisionPill(decision, lang) {
    const s = decision === "consistent" ? SHADE.ok
            : decision === "with_observations" ? SHADE.amber
            : SHADE.bad;
    const label = decision === "consistent" ? "CONSISTENT"
                : decision === "with_observations" ? (window.L("AVEC OBSERVATIONS", "WITH OBSERVATIONS", "CON OBSERVACIONES"))
                : (window.L("REJET", "REJECT", "RECHAZO"));
    return new Paragraph({
      shading: { type: ShadingType.CLEAR, fill: s.fill },
      alignment: AlignmentType.LEFT,
      spacing: { before: 60, after: 60 },
      children: [new TextRun({ text: " " + label + " ", bold: true, color: s.fg, size: 18 })],
    });
  }

  // Helper: convert a data:image/png;base64 URL to an ArrayBuffer for ImageRun.
  function dataUrlToBytes(dataUrl) {
    try {
      const b64 = String(dataUrl).split(",")[1] || "";
      const bin = atob(b64);
      const bytes = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
      return bytes.buffer;
    } catch (e) { return null; }
  }

  // Helper: wrap an ArrayBuffer (PNG) as a centered paragraph w/ ImageRun.
  function imgParagraph(buf, w, h) {
    if (!buf) return null;
    return new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { before: 120, after: 120 },
      children: [new ImageRun({ data: buf, transformation: { width: w, height: h } })],
    });
  }

  // Trace un diagramme de Gantt sur un canvas à partir des données du calendrier
  // (phases + activités), indépendamment du DOM. Retourne { buf, w, h } ou null.
  function buildGanttPng(calendar, lang) {
    try {
      const acts = (calendar && calendar.activities) || [];
      if (!acts.length || typeof document === "undefined") return null;
      const phases = (calendar && calendar.phases) || [];
      const parse = (d) => { const v = Date.parse(d); return isNaN(v) ? null : v; };
      let min = Infinity, max = -Infinity;
      acts.forEach((a) => {
        const s = parse(a.start_date), e = parse(a.end_date);
        [s, e].forEach((v) => { if (v != null) { if (v < min) min = v; if (v > max) max = v; } });
      });
      const palette = ["#2563eb", "#15803d", "#b45309", "#7c3aed", "#be185d", "#0f766e"];
      const phColor = {};
      phases.forEach((p, i) => { phColor[p.id] = (p.color && String(p.color)[0] === "#") ? p.color : palette[i % palette.length]; });
      const padL = 270, padR = 24, padT = 46, rowH = 26, padB = 16;
      const W = 1100, H = padT + acts.length * rowH + padB;
      const cv = document.createElement("canvas"); cv.width = W; cv.height = H;
      const ctx = cv.getContext("2d");
      ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, W, H);
      const plotW = W - padL - padR;
      const hasDates = isFinite(min) && isFinite(max) && max > min;
      const span = hasDates ? (max - min) : 1;
      const xFor = (t) => padL + ((t - min) / span) * plotW;
      // Titre
      ctx.fillStyle = "#0f2740"; ctx.font = "bold 13px Arial"; ctx.textBaseline = "alphabetic";
      ctx.fillText(lang === "es" ? "Cronograma de ejecución (Gantt)" : lang === "en" ? "Implementation schedule (Gantt)" : "Calendrier de mise en oeuvre (Gantt)", 8, 22);
      // Quadrillage trimestriel + libellés d'année/trimestre
      if (hasDates) {
        const sd = new Date(min);
        let d = new Date(sd.getFullYear(), Math.floor(sd.getMonth() / 3) * 3, 1);
        ctx.font = "10px Arial";
        while (d.getTime() <= max) {
          const x = xFor(d.getTime());
          if (x >= padL && x <= W - padR) {
            ctx.strokeStyle = "#eef2f7"; ctx.beginPath(); ctx.moveTo(x, padT - 6); ctx.lineTo(x, H - padB); ctx.stroke();
            ctx.fillStyle = "#6b7280";
            ctx.fillText(["T1", "T2", "T3", "T4"][Math.floor(d.getMonth() / 3)] + " " + d.getFullYear(), x + 3, padT - 10);
          }
          d = new Date(d.getFullYear(), d.getMonth() + 3, 1);
        }
      }
      // Lignes d'activités
      ctx.textBaseline = "middle";
      acts.forEach((a, i) => {
        const y = padT + i * rowH, cy = y + rowH / 2;
        ctx.fillStyle = "#111827"; ctx.font = "11px Arial";
        const nm = (a.milestone ? "◆ " : "") + (a.name_fr || a.name_en || "");
        ctx.fillText(nm.length > 46 ? nm.slice(0, 44) + "…" : nm, 8, cy);
        const col = phColor[a.phase_id] || "#2563eb";
        const s = parse(a.start_date), e = parse(a.end_date);
        if (hasDates && s != null && e != null && e >= s) {
          const x1 = xFor(s), x2 = Math.max(x1 + 3, xFor(e));
          ctx.fillStyle = col + "33"; ctx.fillRect(x1, cy - 7, x2 - x1, 14);
          ctx.fillStyle = col; ctx.fillRect(x1, cy - 7, (x2 - x1) * Math.min(1, (a.progress || 0) / 100), 14);
          ctx.strokeStyle = col; ctx.lineWidth = 1; ctx.strokeRect(x1, cy - 7, x2 - x1, 14);
        } else if (hasDates && s != null) {
          const x = xFor(s); ctx.fillStyle = col;
          ctx.beginPath(); ctx.moveTo(x, cy - 7); ctx.lineTo(x + 7, cy); ctx.lineTo(x, cy + 7); ctx.lineTo(x - 7, cy); ctx.closePath(); ctx.fill();
        } else {
          // Pas de dates : barre indicative pleine largeur selon l'avancement.
          ctx.fillStyle = col + "33"; ctx.fillRect(padL, cy - 7, plotW, 14);
          ctx.fillStyle = col; ctx.fillRect(padL, cy - 7, plotW * Math.min(1, (a.progress || 0) / 100), 14);
        }
      });
      return { buf: dataUrlToBytes(cv.toDataURL("image/png")), w: W, h: H };
    } catch (e) { return null; }
  }

  // ---------- the big builder ----------
  async function build(payload) {
    const {
      lang, dossier, ident, inputs, scen, calendar,
      capexLines, opexLines, revenueLines, finSources,
      pubTransfers, mprTransfers, externalities,
      qualityItems, mcItems,
      // Phase 7 additions
      stakeholders, stakeholderAccounts, conversionFactors, institutional,
      exanteComputed, mprResult, qualityResult, mcResult, finalDecision,
      // Image du Gantt capturée à l'écran (data URL PNG) + dimensions canvas
      calendarGanttPng, calendarGanttW, calendarGanttH,
      // Cadres « VIII. Sensibilité » & « XIII. Recommandations » (valeurs réelles)
      reportTornado, reportScenarios, reportReco, reportTornadoUnit, tornadoPng, tornadoW, tornadoH,
      // Mode « rapport détaillé » : annexes de flux année par année.
      detailed,
    } = payload;
    const t = (fr, en, es) => lang === "es" ? (es != null ? es : en) : lang === "en" ? en : fr;
    // Traduit une clé d'énumération (catégorie, type…) en libellé FR/EN
    // via la table partagée du moteur. Repli sur la clé brute si absente.
    const xL = (group, value) => {
      const E = window.exanteEngine;
      if (E && typeof E.exLabel === "function") return E.exLabel(group, value, lang);
      return value == null || value === "" ? "—" : String(value);
    };
    const ccy = (inputs && inputs.currency) || "EUR";
    const projectCode = (dossier && dossier.projects && dossier.projects.code) || (ident && ident.intitule) || "Project";
    const today = new Date().toLocaleDateString(window.L("fr-FR", "en-US", "es-ES"), { year: "numeric", month: "long", day: "numeric" });

    // ===== Precompute chart PNG buffers (canvas → ArrayBuffer) =====
    let chartBufs = {};
    if (window.exanteCharts && window.exanteEngine) {
      try {
        const E = window.exanteEngine;
        const cf = exanteComputed && exanteComputed.cashFlow;
        const cashflowOpts = cf ? {
          fcfProject: cf.fcfProject, cumProject: cf.cumProject,
          fcfEquity: cf.fcfEquity, cumEquity: cf.cumEquity, ccy,
        } : null;
        const plOpts = (exanteComputed && exanteComputed.pl && exanteComputed.pl.length) ? exanteComputed.pl : null;
        // Donut from positive externalities by category
        let donutOpts = null;
        if (externalities && externalities.length > 0) {
          const buckets = {};
          externalities.filter((e) => e.kind === "positive").forEach((e) => {
            const k = e.category || "other";
            buckets[k] = (buckets[k] || 0) + (Number(e.amount_year1) || 0);
          });
          const arr = Object.entries(buckets).map(([name, value]) => ({ name: xL("extCat", name), value }));
          if (arr.length > 0) donutOpts = { stakeholders: arr };
        }
        // Sensitivity matrix
        let sensitivityOpts = null;
        if (cf) {
          const baseOpts = {
            inputs, scen,
            capexLines: capexLines || [], opexLines: opexLines || [],
            revenueLines: revenueLines || [], financingSources: finSources || [],
          };
          const mprBaseOpts = {
            inputs, capexLines: capexLines || [],
            opexByYear: exanteComputed.opexByYear,
            revenueByYear: exanteComputed.revenueByYear,
            debtSchedule: exanteComputed.debt,
            mprTransfers: mprTransfers || [], externalities: externalities || [],
            conversionFactors: null,
          };
          const variables = [
            { v: "capex",    fr: "CAPEX",                en: "CAPEX", es: "CAPEX" },
            { v: "volumes",  fr: "Volumes",              en: "Volumes", es: "Volúmenes" },
            { v: "tariffs",  fr: "Tarifs",               en: "Tariffs", es: "Tarifas" },
            { v: "opex",     fr: "OPEX",                 en: "OPEX", es: "OPEX" },
            { v: "discount", fr: "Taux d'actualisation", en: "Discount rate", es: "Tasa de descuento" },
          ];
          const deltas = [-0.20, -0.10, 0, 0.10, 0.20];
          const matrix = variables.map((v) => ({
            v,
            cells: deltas.map((d) => E.economicSensitivityShock(baseOpts, mprBaseOpts, v.v, d)),
          }));
          sensitivityOpts = { matrix, variables, deltas };
        }
        chartBufs = await window.exanteCharts.arrayBuffers({
          cashflow: cashflowOpts, pl: plOpts, donut: donutOpts, sensitivity: sensitivityOpts, ccy,
        });
      } catch (e) {
        console.error("[exante-docx] chart prep failed:", e);
        chartBufs = {};
      }
    }

    // ===== Build all children =====
    const children = [];

    // --- Cover page ---
    children.push(
      P([new TextRun({ text: t("ÉVALUATION EX ANTE", "EX-ANTE APPRAISAL", "EVALUACIÓN EX ANTE"), bold: true, size: 56 })],
        { alignment: AlignmentType.CENTER, spacing: { before: 2400, after: 240 } }),
      P([new TextRun({ text: safe(ident && ident.intitule), size: 28 })],
        { alignment: AlignmentType.CENTER, spacing: { before: 120, after: 480 } }),
      P([new TextRun({ text: t("Code projet : ", "Project code: ", "Código del proyecto: ") + projectCode, size: 22 })],
        { alignment: AlignmentType.CENTER }),
      P([new TextRun({ text: t("Tutelle : ", "Authority: ", "Autoridad de tutela: ") + safe(ident && ident.ministry), size: 22 })],
        { alignment: AlignmentType.CENTER }),
      P([new TextRun({ text: t("Document généré le : ", "Generated on: ", "Documento generado el: ") + today, size: 22 })],
        { alignment: AlignmentType.CENTER, spacing: { before: 60, after: 800 } }),
      P([new TextRun({ text: t("Dossier d'évaluation ex ante", "Ex-ante appraisal dossier", "Expediente de evaluación ex ante"), italics: true, size: 18, color: "6B7280" })],
        { alignment: AlignmentType.CENTER })
    );

    // --- Decision banner ---
    if (finalDecision) {
      const decUi = {
        go:          { label: t("GO — APPROUVÉ POUR FINANCEMENT", "GO — APPROVED FOR FUNDING", "GO — APROBADO PARA FINANCIAMIENTO"), color: "16A34A", fill: "DCFCE7" },
        conditional: { label: t("CONDITIONNEL — AVEC OBSERVATIONS", "CONDITIONAL — WITH OBSERVATIONS", "CONDICIONAL — CON OBSERVACIONES"), color: "D97706", fill: "FEF3C7" },
        no_go:       { label: t("NO-GO — À RÉVISER OU REJETER", "NO-GO — TO REVISE OR REJECT", "NO-GO — A REVISAR O RECHAZAR"), color: "DC2626", fill: "FEE2E2" },
      }[finalDecision.decision];
      children.push(
        H1NoBreak(t("Décision du comité", "Investment committee decision", "Decisión del comité")),
        new Paragraph({
          shading: { type: ShadingType.CLEAR, fill: decUi.fill },
          alignment: AlignmentType.CENTER,
          spacing: { before: 100, after: 100 },
          children: [new TextRun({ text: decUi.label, bold: true, color: decUi.color, size: 28 })],
        }),
        P(finalDecision.reasons.map((r) => typeof r === "string" ? r : t(r.fr, r.en)).join(" · "), { italics: true, color: "374151", spacing: { after: 200 } })
      );
    }

    // --- Executive summary ---
    children.push(H1NoBreak(t("I. Synthèse exécutive", "I. Executive summary", "I. Síntesis ejecutiva")));
    if (ident) {
      children.push(
        P([new TextRun({ text: t("Objectif général : ", "General objective: ", "Objetivo general: "), bold: true }), new TextRun({ text: safe(ident.general_objective) })]),
        P([new TextRun({ text: t("Coût global : ", "Global cost: ", "Costo global: "), bold: true }), new TextRun({ text: fmtCcy(ident.global_cost_native, ident.currency) })]),
        P([new TextRun({ text: t("Horizon : ", "Horizon: ", "Horizonte: "), bold: true }), new TextRun({ text: (ident.total_horizon_years || "—") + " " + t("ans", "yrs", "años") })]),
        P([new TextRun({ text: t("Cible directe : ", "Direct target: ", "Meta directa: "), bold: true }), new TextRun({ text: safe(ident.direct_targets) + " (" + (ident.direct_target_size ? Number(ident.direct_target_size).toLocaleString() : "—") + ")" })])
      );
    }

    // Indicators as colored KPI tiles
    if (exanteComputed && exanteComputed.indicators) {
      const ind = exanteComputed.indicators;
      const discFin = (inputs && inputs.discount_rate_financial) || 0.09;
      const discEco = (inputs && inputs.discount_rate_economic)  || 0.09;
      const tiles = [
        { label: "VAN " + t("projet", "project", "proyecto"), value: fmtCcy(ind.npvProject, ccy), status: statusFor(ind.npvProject, 0, ">"), sub: "≥ 0" },
        { label: "TRI " + t("projet", "project", "proyecto"), value: fmtPct(ind.irrProject), status: statusFor(ind.irrProject, discFin, ">"), sub: "> " + fmtPct(discFin) },
        { label: t("DSCR min", "Min DSCR", "DSCR mín."), value: fmtRatio(ind.dscrMin), status: statusFor(ind.dscrMin, 1.2, ">"), sub: "≥ 1,2" },
        { label: t("Marge EBITDA", "EBITDA margin", "Margen EBITDA"), value: fmtPct(ind.ebitdaMarginAvg), status: statusFor(ind.ebitdaMarginAvg, 0, ">"), sub: t("Moyenne", "Average", "Promedio") },
      ];
      if (mprResult && mprResult.indicators) {
        tiles.push(
          { label: "ENPV", value: fmtCcy(mprResult.indicators.enpv, ccy), status: statusFor(mprResult.indicators.enpv, 0, ">"), sub: "≥ 0" },
          { label: "EIRR", value: fmtPct(mprResult.indicators.eirr), status: statusFor(mprResult.indicators.eirr, discEco, ">"), sub: "> " + fmtPct(discEco) },
          { label: t("Ratio B/C", "B/C ratio", "Ratio B/C"), value: fmtRatio(mprResult.indicators.bcRatio), status: statusFor(mprResult.indicators.bcRatio, 1, ">"), sub: "≥ 1" }
        );
      }
      if (qualityResult) tiles.push({ label: t("Qualité", "Quality", "Calidad"), value: fmtPct(qualityResult.pct), status: statusFor(qualityResult.pct, 0.6, ">"), sub: "≥ 60%" });
      if (mcResult)      tiles.push({ label: t("Multicritère", "Multi-criteria", "Multicriterio"), value: fmtPct(mcResult.globalPct), status: statusFor(mcResult.globalPct, 0.75, ">"), sub: "≥ 75%" });
      children.push(buildKpiTileRow(tiles, 4));
    }

    // Cashflow chart at end of exec summary
    if (chartBufs.cashflow) {
      const img = imgParagraph(chartBufs.cashflow, 600, 274);
      if (img) children.push(img);
    }

    // --- II. Identification ---
    children.push(H1(t("II. Identification du projet (Tableau 8)", "II. Project identification (Table 8)", "II. Identificación del proyecto (Tabla 8)")));
    if (ident) {
      children.push(H2(t("1. Identification générale", "1. General identification", "1. Identificación general")));
      children.push(
        P([new TextRun({ text: t("Intitulé : ", "Title: ", "Título: "), bold: true }), new TextRun({ text: safe(ident.intitule) })]),
        P([new TextRun({ text: t("Sous-secteur : ", "Sub-sector: ", "Subsector: "), bold: true }), new TextRun({ text: safe(ident.sub_sector) })]),
        P([new TextRun({ text: t("Ministère : ", "Ministry: ", "Ministerio: "), bold: true }), new TextRun({ text: safe(ident.ministry) })])
      );
      children.push(H2(t("2. Situation initiale", "2. Initial situation", "2. Situación inicial")));
      children.push(
        P([new TextRun({ text: t("Situation initiale : ", "Initial situation: ", "Situación inicial: "), bold: true }), new TextRun({ text: safe(ident.initial_situation) })]),
        P([new TextRun({ text: t("Situation sans projet : ", "Without project: ", "Situación sin proyecto: "), bold: true }), new TextRun({ text: safe(ident.without_project_situation) })]),
        P([new TextRun({ text: t("Problème central : ", "Central problem: ", "Problema central: "), bold: true }), new TextRun({ text: safe(ident.central_problem) })])
      );
      children.push(H2(t("3. Objectifs", "3. Objectives", "3. Objetivos")));
      children.push(P([new TextRun({ text: t("Objectif général : ", "General objective: ", "Objetivo general: "), bold: true }), new TextRun({ text: safe(ident.general_objective) })]));
      if (ident.specific_objective_1) children.push(P("OS 1 : " + ident.specific_objective_1));
      if (ident.specific_objective_2) children.push(P("OS 2 : " + ident.specific_objective_2));
      if (ident.specific_objective_3) children.push(P("OS 3 : " + ident.specific_objective_3));

      children.push(H2(t("4. Composantes", "4. Components", "4. Componentes")));
      const compRows = [];
      for (let i = 1; i <= 6; i++) {
        if (ident["component_" + i + "_label"]) {
          compRows.push([ident["component_" + i + "_label"], fmtCcy(ident["component_" + i + "_amount"], ident.currency)]);
        }
      }
      if (compRows.length) children.push(buildTable(
        [{ label: t("Composante", "Component", "Componente"), width: 75 }, { label: t("Budget", "Budget", "Presupuesto"), width: 25, align: "right" }],
        compRows
      ));

      children.push(H2(t("5. Risques", "5. Risks", "5. Riesgos")));
      children.push(
        P([new TextRun({ text: t("Risques : ", "Risks: ", "Riesgos: "), bold: true }), new TextRun({ text: safe(ident.main_risks) })]),
        P([new TextRun({ text: t("Conditions préalables : ", "Preconditions: ", "Condiciones previas: "), bold: true }), new TextRun({ text: safe(ident.preconditions) })])
      );
    }

    // --- III. Parameters & scenarios ---
    children.push(H1(t("III. Paramètres et scénarios", "III. Parameters and scenarios", "III. Parámetros y escenarios")));
    if (inputs) {
      children.push(buildTable(
        [{ label: t("Paramètre", "Parameter", "Parámetro"), width: 60 }, { label: t("Valeur", "Value", "Valor"), width: 40, align: "right" }],
        [
          [t("Scénario actif", "Active scenario", "Escenario activo"), safe(inputs.scenario)],
          [t("Horizon", "Horizon", "Horizonte"), (inputs.horizon_years || "—") + " " + t("ans", "yrs", "años")],
          [t("Taux actu. financier", "Financial discount", "Tasa actu. financiera"), fmtPct(inputs.discount_rate_financial)],
          [t("Taux actu. économique", "Economic discount", "Tasa actu. económica"), fmtPct(inputs.discount_rate_economic)],
          [t("Inflation annuelle", "Annual inflation", "Inflación anual"), fmtPct(inputs.inflation_annual)],
          [t("Taux d'impôt", "Tax rate", "Tasa impositiva"), fmtPct(inputs.tax_rate_effective)],
          [t("Part dette / CAPEX", "Debt / CAPEX share", "Parte deuda / CAPEX"), fmtPct(inputs.debt_share_capex)],
          [t("Taux d'intérêt dette", "Debt rate", "Tasa de interés deuda"), fmtPct(inputs.debt_interest_rate)],
          [t("Imprévus CAPEX", "Contingencies", "Imprevistos CAPEX"), fmtPct(inputs.contingencies_capex)],
        ]
      ));
    }

    // Calendar
    if (calendar && calendar.activities && calendar.activities.length > 0) {
      children.push(H2(t("Calendrier", "Schedule", "Calendario")));
      children.push(buildTable(
        [
          { label: t("Phase", "Phase", "Fase"), width: 18 },
          { label: t("Action", "Action", "Acción"), width: 42 },
          { label: t("Début", "Start", "Inicio"), width: 14 },
          { label: t("Fin", "End", "Fin"), width: 14 },
          { label: "%", width: 12, align: "right" },
        ],
        calendar.activities.map((a) => {
          const ph = calendar.phases.find((p) => p.id === a.phase_id);
          return [ph ? ph.name_fr : "—", (a.milestone ? "◆ " : "") + a.name_fr, a.start_date || "—", a.end_date || "—", (a.progress || 0) + "%"];
        })
      ));
      // Diagramme de Gantt sous le tableau : on privilégie la capture écran si
      // elle est valide (image large), sinon on TRACE le Gantt depuis les données
      // (méthode fiable, indépendante du DOM).
      let g = null;
      // Source préférée : helper partagé (canvas lisible) ; sinon capture écran ;
      // sinon tracé local. Garantit le même Gantt lisible que l'aperçu.
      try {
        if (window.melr && window.melr.exanteGanttDataUrl) {
          const shared = window.melr.exanteGanttDataUrl(calendar, lang, { width: 1500 });
          if (shared && shared.url) { const b = dataUrlToBytes(shared.url); if (b) g = { buf: b, w: shared.w, h: shared.h }; }
        }
      } catch (e) { /* fallback */ }
      if (!g && calendarGanttPng && calendarGanttW && calendarGanttW > 100) {
        const b = dataUrlToBytes(calendarGanttPng);
        if (b) g = { buf: b, w: calendarGanttW, h: calendarGanttH };
      }
      if (!g) g = buildGanttPng(calendar, lang);
      if (g && g.buf) {
        const gw = 920;   // page paysage → largeur utile ~25 cm
        const gh = (g.w && g.h) ? Math.min(560, Math.round(gw * g.h / g.w)) : 360;
        children.push(P(t("Diagramme de Gantt", "Gantt chart", "Diagrama de Gantt"), { italics: true, spacing: { before: 60, after: 20 } }));
        const gp = imgParagraph(g.buf, gw, gh);
        if (gp) children.push(gp);
      }
    }

    // --- IV. Financial ---
    children.push(H1(t("IV. Analyse financière", "IV. Financial analysis", "IV. Análisis financiero")));
    children.push(H2("CAPEX (" + t("Tableau", "Table", "Tabla") + " 13)"));
    if (capexLines && capexLines.length > 0) {
      const capexRows = capexLines.map((l) => [
        l.rubric,
        xL("capexCat", l.category),
        fmtNum(l.quantity || 1),
        fmtNum(l.unit_cost),
        { text: fmtNum((l.quantity || 1) * (l.unit_cost || 0)), bold: true },
        String(l.amort_years || "—"),
      ]);
      if (exanteComputed && exanteComputed.capex) {
        capexRows.push(
          [{ text: t("Sous-total", "Subtotal", "Subtotal"), bold: true, shading: "F3F4F6" }, "", "", "", { text: fmtNum(exanteComputed.capex.subtotal), bold: true, shading: "F3F4F6" }, { text: "", shading: "F3F4F6" }],
          [t("Imprévus", "Contingencies", "Imprevistos"), "", "", "", fmtNum(exanteComputed.capex.contingencies), ""],
          [{ text: t("CAPEX TOTAL", "TOTAL CAPEX", "CAPEX TOTAL"), bold: true, shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }, { text: fmtNum(exanteComputed.capex.total), bold: true, shading: "DCFCE7" }, { text: "", shading: "DCFCE7" }],
        );
      }
      children.push(buildTable(
        [
          { label: t("Rubrique", "Item", "Rubro"), width: 30 },
          { label: t("Catégorie", "Category", "Categoría"), width: 17 },
          { label: t("Qté", "Qty", "Cant."), width: 8, align: "right" },
          { label: t("Coût unit.", "Unit cost", "Costo unit."), width: 15, align: "right" },
          { label: t("Total", "Total", "Total"), width: 18, align: "right" },
          { label: t("Amort.", "Amort.", "Amort."), width: 12, align: "right" },
        ],
        capexRows
      ));
    }

    children.push(H2("OPEX (" + t("Tableau", "Table", "Tabla") + " 14)"));
    if (opexLines && opexLines.length > 0 && window.exanteEngine) {
      const opexRows = opexLines.map((l) => {
        const series = window.exanteEngine.projectOpexLine(l, 25, inputs, scen);
        return [l.rubric, xL("opexCat", l.inflation_category), fmtNum(l.year1_amount), fmtNum(series[4]), fmtNum(series[9])];
      });
      children.push(buildTable(
        [
          { label: t("Rubrique", "Item", "Rubro"), width: 40 },
          { label: t("Catégorie", "Category", "Categoría"), width: 20 },
          { label: t("An 1", "Y1", "Año 1"), width: 13, align: "right" },
          { label: t("An 5", "Y5", "Año 5"), width: 13, align: "right" },
          { label: t("An 10", "Y10", "Año 10"), width: 14, align: "right" },
        ],
        opexRows
      ));
    }

    children.push(H2(t("Revenus", "Revenue", "Ingresos") + " (" + t("Tableau", "Table", "Tabla") + " 15)"));
    if (revenueLines && revenueLines.length > 0) {
      children.push(buildTable(
        [
          { label: t("Rubrique", "Item", "Rubro"), width: 40 },
          { label: t("Volume An 1", "Volume Y1", "Volumen Año 1"), width: 18, align: "right" },
          { label: t("Tarif An 1", "Tariff Y1", "Tarifa Año 1"), width: 18, align: "right" },
          { label: t("Revenu An 1", "Revenue Y1", "Ingreso Año 1"), width: 24, align: "right" },
        ],
        revenueLines.map((l) => [
          l.rubric,
          l.is_fixed ? "—" : fmtNum(l.year1_volume),
          fmtNum(l.year1_tariff),
          { text: fmtNum(l.is_fixed ? l.year1_tariff : (l.year1_volume || 0) * (l.year1_tariff || 0)), bold: true },
        ])
      ));
    }

    // --- Besoin en fonds de roulement (BFR) — placé AVANT le plan de financement ---
    if (exanteComputed && exanteComputed.bfr && (exanteComputed.bfr.perYear || []).length) {
      const perB = exanteComputed.bfr.perYear;
      const dltB = exanteComputed.bfr.deltas || [];
      const nB = perB.length;
      const yrWB = Math.max(6, Math.floor(76 / nB));
      const MB = (v) => (v == null || !isFinite(v)) ? "—" : Math.round(v / 1e6).toLocaleString();
      const colsB = [{ label: t("Poste (M " + ccy + ")", "Item (M " + ccy + ")"), width: 24 }]
        .concat(perB.map((_, i) => ({ label: (t("An ", "Y", "Año ")) + (i + 1), width: yrWB, align: "right" })));
      const rowB = (label, arr, bold) => [bold ? { text: label, bold: true } : label]
        .concat(arr.map((v) => bold ? { text: MB(v), bold: true } : MB(v)));
      children.push(H2(t("Besoin en fonds de roulement (BFR)", "Working capital requirement (WCR)", "Necesidades operativas de fondos (NOF)")));
      children.push(P(t("Créances clients + stocks − dettes fournisseurs. La variation (Δ) du BFR impacte la trésorerie.",
                        "Receivables + inventory − payables. The change (Δ) in WCR affects cash flow.", "Cuentas por cobrar + existencias − cuentas por pagar. La variación (Δ) de las NOF afecta el flujo de caja."), { spacing: { after: 80 } }));
      children.push(buildTable(colsB, [
        rowB(t("Créances clients", "Receivables", "Cuentas por cobrar"), perB.map((p) => p.receivables)),
        rowB(t("Stocks / inventaire", "Inventory", "Existencias / inventario"), perB.map((p) => p.inventory)),
        rowB(t("Dettes fournisseurs", "Payables", "Cuentas por pagar"), perB.map((p) => p.payables)),
        rowB("BFR", perB.map((p) => p.bfr), true),
        rowB(t("Δ BFR (variation)", "Δ WCR (change)", "Δ NOF (variación)"), dltB),
      ]));
    }

    children.push(H2(t("Plan de financement", "Financing plan", "Plan de financiamiento") + " (" + t("Tableau", "Table", "Tabla") + " 17)"));
    if (finSources && finSources.length > 0) {
      children.push(buildTable(
        [
          { label: t("Source", "Source", "Fuente"), width: 50 },
          { label: t("Nature", "Kind", "Naturaleza"), width: 25 },
          { label: t("Montant", "Amount", "Importe"), width: 25, align: "right" },
        ],
        finSources.map((f) => [f.source, xL("finKind", f.kind), fmtCcy(f.amount, f.currency)])
      ));
    }

    if (exanteComputed && exanteComputed.indicators) {
      children.push(H2(t("Indicateurs financiers consolidés", "Consolidated financial indicators", "Indicadores financieros consolidados")));
      const ind = exanteComputed.indicators;
      const discFin = (inputs && inputs.discount_rate_financial) || 0.09;
      children.push(buildKpiTileRow([
        { label: t("VAN projet", "Project NPV", "VAN del proyecto"),       value: fmtCcy(ind.npvProject, ccy), status: statusFor(ind.npvProject, 0, ">"), sub: "≥ 0" },
        { label: t("TRI projet", "Project IRR", "TIR del proyecto"),       value: fmtPct(ind.irrProject),      status: statusFor(ind.irrProject, discFin, ">"), sub: "> " + fmtPct(discFin) },
        { label: t("DSCR min", "Min DSCR", "DSCR mín."),            value: fmtRatio(ind.dscrMin),       status: statusFor(ind.dscrMin, 1.2, ">"), sub: "≥ 1,2" },
        { label: t("VAN actionnaire", "Equity NPV", "VAN del accionista"),   value: fmtCcy(ind.npvEquity, ccy),  status: statusFor(ind.npvEquity, 0, ">"), sub: "≥ 0" },
        { label: t("TRI actionnaire", "Equity IRR", "TIR del accionista"),   value: fmtPct(ind.irrEquity),       status: statusFor(ind.irrEquity, discFin, ">"), sub: "> " + fmtPct(discFin) },
        { label: t("Marge EBITDA", "EBITDA margin", "Margen EBITDA"),   value: fmtPct(ind.ebitdaMarginAvg), status: statusFor(ind.ebitdaMarginAvg, 0, ">"), sub: t("Moyenne", "Average", "Promedio") },
      ], 3));
      // P&L chart right after the financial indicators tiles
      if (chartBufs.pl) {
        const img = imgParagraph(chartBufs.pl, 600, 292);
        if (img) children.push(img);
      }
    }

    // Stakeholder accounts (Phase 7.1)
    if (stakeholderAccounts && stakeholderAccounts.rows && stakeholderAccounts.rows.length > 0) {
      children.push(H2(t("Comptes par acteur (Tableau 22)", "Stakeholder accounts (Table 22)", "Cuentas por actor (Tabla 22)")));
      children.push(P(t(
        "Revenus, coûts et impact net par acteur (avec vs sans projet), cumulés sur l'horizon.",
        "Revenue, costs and net impact per actor (with vs without project), cumulated over the horizon."
      , "Ingresos, costos e impacto neto por actor (con vs sin proyecto), acumulados sobre el horizonte.")));
      const rows = stakeholderAccounts.rows.map((r) => [
        { text: r.name, bold: true },
        String(r.beneficiary_count || 0),
        fmtNum(r.cumWithRevenue),
        { text: "(" + fmtNum(r.cumWithCost) + ")", color: "DC2626" },
        { text: fmtNum(r.cumWithNet), bold: true },
        { text: fmtNum(r.cumBaselineNet), color: "6B7280" },
        {
          text: (r.cumImpact >= 0 ? "+" : "") + fmtNum(r.cumImpact),
          bold: true,
          color: r.cumImpact >= 0 ? "15803D" : "B91C1C",
          shading: r.cumImpact >= 0 ? "DCFCE7" : "FEE2E2",
        },
      ]);
      // TOTAL row
      rows.push([
        { text: "TOTAL", bold: true, shading: "F3F4F6" }, { text: "", shading: "F3F4F6" },
        { text: fmtNum(stakeholderAccounts.total.cumWithRevenue), bold: true, shading: "F3F4F6" },
        { text: "(" + fmtNum(stakeholderAccounts.total.cumWithCost) + ")", bold: true, shading: "F3F4F6" },
        { text: fmtNum(stakeholderAccounts.total.cumWithNet), bold: true, shading: "F3F4F6" },
        { text: fmtNum(stakeholderAccounts.total.cumBaselineNet), bold: true, shading: "F3F4F6" },
        {
          text: (stakeholderAccounts.total.cumImpact >= 0 ? "+" : "") + fmtNum(stakeholderAccounts.total.cumImpact),
          bold: true, shading: "F3F4F6",
          color: stakeholderAccounts.total.cumImpact >= 0 ? "15803D" : "B91C1C",
        },
      ]);
      children.push(buildTable(
        [
          { label: t("Acteur", "Stakeholder", "Actor"), width: 22 },
          { label: t("Bénéf.", "Benef.", "Benef."), width: 8, align: "right" },
          { label: t("Revenu cum.", "Cumul. rev.", "Ingr. acum."), width: 14, align: "right" },
          { label: t("Coût cum.", "Cumul. cost", "Costo acum."), width: 14, align: "right" },
          { label: t("Net (avec)", "Net (with)", "Neto (con)"), width: 14, align: "right" },
          { label: t("Net (sans)", "Net (without)", "Neto (sin)"), width: 14, align: "right" },
          { label: t("Impact", "Impact", "Impacto"), width: 14, align: "right" },
        ],
        rows
      ));
    }

    // --- V. Economic (MPR) ---
    children.push(H1(t("V. Analyse économique (MPR)", "V. Economic analysis (MPR)", "V. Análisis económico (MPR)")));

    children.push(H2(t("Phase 1 — Élimination des transferts", "Phase 1 — Transfer elimination", "Fase 1 — Eliminación de las transferencias")));
    if (mprTransfers && mprTransfers.length > 0) {
      children.push(buildTable(
        [
          { label: t("Libellé", "Label", "Denominación"), width: 50 },
          { label: t("Catégorie", "Category", "Categoría"), width: 25 },
          { label: t("Montant/an", "Amount/yr", "Importe/año"), width: 25, align: "right" },
        ],
        mprTransfers.map((t2) => [t2.label, xL("mprCat", t2.category), fmtNum(t2.amount_per_year)])
      ));
    } else {
      children.push(P(t("Les intérêts de la dette sont automatiquement déduits depuis le plan de financement.", "Debt interest is automatically deducted from the financing plan.", "Los intereses de la deuda se deducen automáticamente del plan de financiamiento.")));
    }

    children.push(H2(t("Phase 2 — Externalités", "Phase 2 — Externalities", "Fase 2 — Externalidades")));
    if (externalities && externalities.length > 0) {
      children.push(buildTable(
        [
          { label: t("Libellé", "Label", "Denominación"), width: 38 },
          { label: t("Nature", "Kind", "Naturaleza"), width: 16 },
          { label: t("Catégorie", "Category", "Categoría"), width: 22 },
          { label: t("An 1", "Y1", "Año 1"), width: 24, align: "right" },
        ],
        externalities.map((e) => [
          e.label,
          xL("extKind", e.kind),
          xL("extCat", e.category),
          { text: (e.kind === "negative" ? "−" : "+") + fmtNum(e.amount_year1), color: e.kind === "negative" ? "DC2626" : "16A34A" },
        ])
      ));
    }

    if (mprResult && mprResult.indicators) {
      children.push(H2(t("Indicateurs économiques", "Economic indicators", "Indicadores económicos")));
      const eind = mprResult.indicators;
      const discEco = (inputs && inputs.discount_rate_economic) || 0.09;
      children.push(buildKpiTileRow([
        { label: "ENPV",                              value: fmtCcy(eind.enpv, ccy),       status: statusFor(eind.enpv, 0, ">"),  sub: "≥ 0" },
        { label: "EIRR",                              value: fmtPct(eind.eirr),            status: statusFor(eind.eirr, discEco, ">"), sub: "> " + fmtPct(discEco) },
        { label: t("Ratio B/C", "B/C ratio", "Ratio B/C"),         value: fmtRatio(eind.bcRatio),       status: statusFor(eind.bcRatio, 1, ">"), sub: "≥ 1" },
        { label: t("NPV bénéfices", "NPV benefits", "VAN beneficios"),  value: fmtCcy(eind.npvBenefits, ccy), status: "neutral", sub: t("Actualisé", "Discounted", "Actualizado") },
        { label: t("NPV coûts", "NPV costs", "VAN costos"),         value: fmtCcy(eind.npvCosts, ccy),    status: "neutral", sub: t("Actualisé", "Discounted", "Actualizado") },
      ], 3));
      if (chartBufs.donut) {
        const img = imgParagraph(chartBufs.donut, 460, 260);
        if (img) children.push(img);
      }
    }

    // Conversion factors — MPR Phase 3 (Phase 7.2)
    if (conversionFactors && conversionFactors.length > 0) {
      children.push(H2(t("Phase 3 — Facteurs de conversion", "Phase 3 — Conversion factors", "Fase 3 — Factores de conversión")));
      children.push(P(t(
        "Coefficients de prix fictifs appliqués automatiquement par le moteur dans le calcul de l'ENPV/EIRR/B/C (1.0 = prix de marché).",
        "Shadow-price coefficients automatically applied to ENPV/EIRR/B/C (1.0 = market price)."
      , "Coeficientes de precios ficticios aplicados automáticamente por el motor en el cálculo de la VANE/TIRE/B/C (1.0 = precio de mercado).")));
      const labels = {
        capex_local: t("CAPEX local", "Local CAPEX", "CAPEX local"),
        capex_imported: t("CAPEX importé", "Imported CAPEX", "CAPEX importado"),
        opex_personnel: t("OPEX — Personnel", "OPEX — Personnel", "OPEX — Personal"),
        opex_materials: t("OPEX — Matières", "OPEX — Materials", "OPEX — Materias"),
        revenue: t("Revenus", "Revenue", "Ingresos"),
        other: t("Autres", "Other", "Otros"),
      };
      children.push(buildTable(
        [
          { label: t("Input", "Input", "Insumo"), width: 30 },
          { label: t("Facteur", "Factor", "Factor"), width: 15, align: "right" },
          { label: "Δ vs 1.0", width: 15, align: "right" },
          { label: t("Justification", "Justification", "Justificación"), width: 40 },
        ],
        conversionFactors.map((f) => {
          const v = Number(f.factor) || 1;
          const delta = (v - 1) * 100;
          return [
            labels[f.input_kind] || f.input_kind,
            { text: v.toFixed(2), bold: true, color: Math.abs(v - 1) < 0.02 ? "374151" : v < 1 ? "A16207" : "15803D" },
            (delta >= 0 ? "+" : "") + delta.toFixed(1) + "%",
            f.notes || "—",
          ];
        })
      ));
    }

    // --- VI. Quality + Multi-criteria ---
    children.push(H1(t("VI. Contrôle qualité & analyse multicritère", "VI. Quality grid & multi-criteria", "VI. Control de calidad y análisis multicriterio")));

    if (qualityResult) {
      children.push(
        H2(t("Grille de contrôle qualité (Tableau 9)", "Quality grid (Table 9)", "Cuadrícula de control de calidad (Tabla 9)")),
        decisionPill(qualityResult.decision, lang),
        P([
          new TextRun({ text: t("Score global : ", "Global score: ", "Puntuación global: "), bold: true }),
          new TextRun({ text: fmtPct(qualityResult.pct), bold: true, color: SHADE[pctStatus(qualityResult.pct)].fg, size: 24 }),
          new TextRun({ text: " · " + qualityResult.scoredItems + "/" + qualityResult.totalItems + " " + t("critères évalués", "criteria scored", "criterios evaluados"), color: "6B7280" }),
        ])
      );
      const qRows = Object.entries(qualityResult.byDim).map(([dim, v]) => {
        const s = pctStatus(v.pct);
        return [dim, v.avg.toFixed(2) + "/3", { text: fmtPct(v.pct), bold: true, color: SHADE[s].fg, shading: SHADE[s].fill }];
      });
      children.push(buildTable(
        [
          { label: t("Dimension", "Dimension", "Dimensión"), width: 60 },
          { label: t("Moyenne", "Average", "Promedio"), width: 20, align: "right" },
          { label: "%", width: 20, align: "right" },
        ],
        qRows
      ));
    }

    if (mcResult) {
      children.push(
        H2(t("Analyse multicritère (Tableau 21)", "Multi-criteria analysis (Table 21)", "Análisis multicriterio (Tabla 21)")),
        decisionPill(mcResult.decision, lang),
        P([
          new TextRun({ text: t("Score pondéré global : ", "Weighted global score: ", "Puntuación ponderada global: "), bold: true }),
          new TextRun({ text: fmtPct(mcResult.globalPct), bold: true, color: SHADE[pctStatus(mcResult.globalPct)].fg, size: 24 }),
          new TextRun({ text: " · " + mcResult.scoredItems + "/" + mcResult.totalItems + " " + t("sous-critères évalués", "sub-criteria scored", "subcriterios evaluados"), color: "6B7280" }),
        ])
      );
      const mcRows = Object.entries(mcResult.byDim).map(([dim, v]) => {
        const s = pctStatus(v.pct);
        return [dim, v.weightedAvg.toFixed(2) + "/3", { text: fmtPct(v.pct), bold: true, color: SHADE[s].fg, shading: SHADE[s].fill }];
      });
      children.push(buildTable(
        [
          { label: t("Dimension", "Dimension", "Dimensión"), width: 60 },
          { label: t("Moy. pondérée", "Weighted avg", "Prom. ponderado"), width: 20, align: "right" },
          { label: "%", width: 20, align: "right" },
        ],
        mcRows
      ));
    }

    // --- VII. Environnement, Social & Genre (placé AVANT l'institutionnel) ---
    if (institutional && ["env_social_category","env_impacts","env_mitigation","eies_status",
        "social_impacts","resettlement_plan","grievance_mechanism","gender_analysis",
        "gender_measures","gender_targets","vulnerable_groups","climate_resilience"]
        .some((k) => institutional[k] && String(institutional[k]).trim())) {
      const xe = institutional;
      children.push(H1(t("VII. Environnement, Social & Genre", "VII. Environmental, Social & Gender", "VII. Medio ambiente, Social y Género")));
      const blkE = (title, fields) => {
        const ne = fields.filter(([_, v]) => v && String(v).trim());
        if (!ne.length) return;
        children.push(H2(title));
        ne.forEach(([label, v]) => children.push(P([
          new TextRun({ text: label + " : ", bold: true, size: 24 }),
          new TextRun({ text: String(v), size: 24 }),
        ], { spacing: { after: 100 } })));
      };
      blkE(t("Sauvegardes environnementales", "Environmental safeguards", "Salvaguardas ambientales"), [
        [t("Catégorie E&S", "E&S category", "Categoría A&S"), xe.env_social_category],
        [t("Statut EIES / NIES", "EIA / IEE status", "Estado EIA / EIES"), xe.eies_status],
        [t("Impacts environnementaux", "Environmental impacts", "Impactos ambientales"), xe.env_impacts],
        [t("Mesures d'atténuation (PGES)", "Mitigation measures (ESMP)", "Medidas de mitigación (PGAS)"), xe.env_mitigation],
        [t("Résilience / adaptation climatique", "Climate resilience / adaptation", "Resiliencia / adaptación climática"), xe.climate_resilience],
      ]);
      blkE(t("Sauvegardes sociales", "Social safeguards", "Salvaguardas sociales"), [
        [t("Impacts sociaux", "Social impacts", "Impactos sociales"), xe.social_impacts],
        [t("Réinstallation / sécurisation foncière", "Resettlement / land tenure", "Reasentamiento / seguridad de la tenencia de la tierra"), xe.resettlement_plan],
        [t("Mécanisme de gestion des plaintes", "Grievance redress mechanism", "Mecanismo de gestión de quejas"), xe.grievance_mechanism],
        [t("Groupes vulnérables", "Vulnerable groups", "Grupos vulnerables"), xe.vulnerable_groups],
      ]);
      blkE(t("Analyse genre", "Gender analysis", "Análisis de género"), [
        [t("Diagnostic genre", "Gender diagnosis", "Diagnóstico de género"), xe.gender_analysis],
        [t("Mesures sensibles au genre", "Gender-responsive measures", "Medidas sensibles al género"), xe.gender_measures],
        [t("Cibles / indicateurs genre", "Gender targets / indicators", "Metas / indicadores de género"), xe.gender_targets],
      ]);
    }

    // --- VII bis. Institutional analysis (Phase 7.3) ---
    if (institutional && (institutional.legal_framework || institutional.management_model || institutional.carrier_organizations || institutional.sustainability_mechanisms)) {
      const inst = institutional;
      const mgmtModelLabels = {
        regie: t("Régie publique directe", "Direct public management", "Gestión pública directa"),
        concession: t("Concession", "Concession", "Concesión"),
        ppp: t("Partenariat Public-Privé", "Public-Private Partnership", "Asociación Público-Privada"),
        delegation: t("Délégation de service public", "Public service delegation", "Delegación de servicio público"),
        community: t("Gestion communautaire", "Community-based", "Gestión comunitaria"),
        mixed: t("Modèle mixte", "Mixed model", "Modelo mixto"),
        other: t("Autre", "Other", "Otro"),
      };
      children.push(H1(t("VII bis. Analyse institutionnelle", "VII bis. Institutional analysis", "VII bis. Análisis institucional")));

      const addBlock = (title, fields) => {
        const nonEmpty = fields.filter(([_, v]) => v && String(v).trim());
        if (!nonEmpty.length) return;
        children.push(H2(title));
        nonEmpty.forEach(([label, v]) => {
          // Narratif institutionnel agrandi (12pt) pour la lisibilité ; les
          // tableaux conservent 9pt pour rester dans la largeur de page.
          children.push(P([
            new TextRun({ text: label + " : ", bold: true, size: 24 }),
            new TextRun({ text: String(v), size: 24 }),
          ], { spacing: { after: 100 } }));
        });
      };

      addBlock(t("1. Cadre juridique & réglementaire", "1. Legal & regulatory framework", "1. Marco jurídico y reglamentario"), [
        [t("Cadre légal applicable", "Applicable legal framework", "Marco legal aplicable"), inst.legal_framework],
        [t("Contraintes / autorisations", "Constraints / permits", "Restricciones / autorizaciones"), inst.juridical_constraints],
        [t("Normes techniques applicables", "Applicable technical norms", "Normas técnicas aplicables"), inst.applicable_norms],
      ]);
      addBlock(t("2. Modèle de gestion", "2. Management model", "2. Modelo de gestión"), [
        [t("Modèle retenu", "Selected model", "Modelo seleccionado"), mgmtModelLabels[inst.management_model] || inst.management_model],
        [t("Détails du modèle", "Model details", "Detalles del modelo"), inst.management_model_details],
        [t("Opérateur identifié", "Identified operator", "Operador identificado"), inst.operator_identification],
        [t("Arrangements contractuels", "Contractual arrangements", "Acuerdos contractuales"), inst.contractual_arrangements],
      ]);
      addBlock(t("3. Capacités institutionnelles des porteurs", "3. Carrier institutional capacities", "3. Capacidades institucionales de las entidades responsables"), [
        [t("Organisations porteuses", "Carrier organisations", "Organizaciones responsables"), inst.carrier_organizations],
        [t("Capacités actuelles", "Existing capacities", "Capacidades actuales"), inst.existing_capacities],
        [t("Écarts identifiés", "Capacity gaps", "Brechas identificadas"), inst.capacity_gaps],
        [t("Plan de renforcement", "Capacity-building plan", "Plan de fortalecimiento"), inst.capacity_building_plan],
      ]);
      addBlock(t("4. Coordination & gouvernance", "4. Coordination & governance", "4. Coordinación y gobernanza"), [
        [t("Mécanismes de coordination", "Coordination mechanisms", "Mecanismos de coordinación"), inst.coordination_mechanisms],
        [t("Instances de décision", "Decision-making bodies", "Instancias de decisión"), inst.decision_making_bodies],
        [t("Dispositif institutionnel de S&E", "Institutional M&E arrangements", "Disposiciones institucionales de SyE"), inst.monitoring_arrangements],
      ]);
      addBlock(t("5. Durabilité institutionnelle", "5. Institutional sustainability", "5. Sostenibilidad institucional"), [
        [t("Mécanismes de durabilité", "Sustainability mechanisms", "Mecanismos de sostenibilidad"), inst.sustainability_mechanisms],
        [t("Transfert de propriété post-projet", "Post-project ownership transfer", "Transferencia de propiedad posproyecto"), inst.ownership_transfer],
        [t("Ressources O&M futures", "Future O&M resources", "Recursos de O&M futuros"), inst.post_project_resources],
      ]);
      addBlock(t("6. Risques institutionnels", "6. Institutional risks", "6. Riesgos institucionales"), [
        [t("Risques identifiés", "Identified risks", "Riesgos identificados"), inst.institutional_risks],
        [t("Mesures de mitigation", "Mitigation measures", "Medidas de mitigación"), inst.risk_mitigation_measures],
      ]);
    }

    // --- VIII. Analyse de sensibilité (heatmap + scénarios + tornado) ---
    let sensitivityHeadingDone = false;
    if (exanteComputed && mprResult && window.exanteEngine) {
      const E = window.exanteEngine;
      const baseOpts = {
        inputs, scen,
        capexLines: capexLines || [], opexLines: opexLines || [],
        revenueLines: revenueLines || [], financingSources: finSources || [],
      };
      const mprBaseOpts = {
        inputs, capexLines: capexLines || [],
        opexByYear: exanteComputed.opexByYear,
        revenueByYear: exanteComputed.revenueByYear,
        debtSchedule: exanteComputed.debt,
        mprTransfers: mprTransfers || [], externalities: externalities || [],
        conversionFactors: null,
      };
      const vars = [
        { v: "capex",    fr: "CAPEX",                en: "CAPEX", es: "CAPEX" },
        { v: "volumes",  fr: "Volumes",              en: "Volumes", es: "Volúmenes" },
        { v: "tariffs",  fr: "Tarifs",               en: "Tariffs", es: "Tarifas" },
        { v: "opex",     fr: "OPEX",                 en: "OPEX", es: "OPEX" },
        { v: "discount", fr: "Taux d'actualisation", en: "Discount rate", es: "Tasa de descuento" },
      ];
      const deltas = [-0.20, -0.10, 0, 0.10, 0.20];
      let matrix = null;
      try {
        matrix = vars.map((v) => ({
          v,
          cells: deltas.map((d) => E.economicSensitivityShock(baseOpts, mprBaseOpts, v.v, d)),
        }));
      } catch (e) { /* fall through */ }

      if (matrix) {
        children.push(H1(t("VIII. Analyse de sensibilité (VAN projet)", "VIII. Sensitivity analysis (Project NPV)", "VIII. Análisis de sensibilidad (VAN del proyecto)")));
        sensitivityHeadingDone = true;
        children.push(P(t("Heatmap de la VAN projet pour 5 variables × 5 chocs (−20% à +20%). Vert = VAN ≥ 0 (viable), rouge = VAN < 0.",
                          "Project NPV heatmap for 5 variables × 5 shocks (−20% to +20%). Green = NPV ≥ 0 (viable), red = NPV < 0.", "Mapa de calor de la VAN del proyecto para 5 variables × 5 choques (−20% a +20%). Verde = VAN ≥ 0 (viable), rojo = VAN < 0.")));
        // Header row
        const headerCells = [
          new TableCell({
            width: { size: 30, type: WidthType.PERCENTAGE },
            shading: { type: ShadingType.CLEAR, fill: "F3F4F6" },
            children: [new Paragraph({ children: [new TextRun({ text: t("Variable", "Variable", "Variable"), bold: true, size: 18 })] })],
          }),
          ...deltas.map((d) => new TableCell({
            width: { size: 14, type: WidthType.PERCENTAGE },
            shading: { type: ShadingType.CLEAR, fill: "F3F4F6" },
            children: [new Paragraph({
              alignment: AlignmentType.RIGHT,
              children: [new TextRun({ text: d === 0 ? "Base" : ((d > 0 ? "+" : "") + Math.round(d * 100) + "%"), bold: true, size: 18 })],
            })],
          })),
        ];
        const dataRows = matrix.map((row) => new TableRow({
          children: [
            new TableCell({
              width: { size: 30, type: WidthType.PERCENTAGE },
              children: [new Paragraph({ children: [new TextRun({ text: t(row.v.fr, row.v.en), bold: true, size: 18 })] })],
            }),
            ...row.cells.map((res, i) => {
              const value = res.fin && res.fin.npvProject;
              const fill = heatmapFill(value, 0, ">");
              return new TableCell({
                width: { size: 14, type: WidthType.PERCENTAGE },
                shading: { type: ShadingType.CLEAR, fill },
                children: [new Paragraph({
                  alignment: AlignmentType.RIGHT,
                  children: [new TextRun({ text: fmtNum(value), bold: i === 2, size: 18 })],
                })],
              });
            }),
          ],
        }));
        children.push(new Table({
          width: { size: 100, type: WidthType.PERCENTAGE },
          rows: [new TableRow({ tableHeader: true, children: headerCells }), ...dataRows],
        }));
        if (chartBufs.sensitivity) {
          const img = imgParagraph(chartBufs.sensitivity, 600, 240);
          if (img) children.push(img);
        }
      }
    }

    // --- VIII (suite). Scénarios multicritères + Tornado (synthèse) ---
    if ((reportScenarios && reportScenarios.length) || (reportTornado && reportTornado.length) || tornadoPng) {
      if (!sensitivityHeadingDone) {
        children.push(H1(t("VIII. Analyse de sensibilité", "VIII. Sensitivity analysis", "VIII. Análisis de sensibilidad")));
        sensitivityHeadingDone = true;
      }
      if (reportScenarios && reportScenarios.length) {
        children.push(H2(t("Scénarios multicritères", "Multi-criteria scenarios", "Escenarios multicriterio")));
        const verdict = (k) => k === "ok" ? { text: t("Acceptable", "Acceptable", "Aceptable"), color: "15803D" }
                             : k === "warn" ? { text: t("Attention", "Caution", "Atención"), color: "B45309" }
                             : { text: t("Non viable", "Not viable", "No viable"), color: "B91C1C" };
        children.push(buildTable(
          [
            { label: t("Scénario", "Scenario", "Escenario"), width: 34 },
            { label: "VAN", width: 16, align: "right" },
            { label: "TRI", width: 14, align: "right" },
            { label: "DSCR", width: 14, align: "right" },
            { label: t("Verdict", "Verdict", "Veredicto"), width: 22 },
          ],
          reportScenarios.map((r) => {
            const vd = verdict(r.k);
            return [{ text: r.s, bold: true }, r.v, r.t, r.d, { text: vd.text, color: vd.color, bold: true }];
          })
        ));
      }
      // Tornado : image PNG nette si disponible, sinon repli en tableau.
      let tb = null;
      if (tornadoPng) { const b = dataUrlToBytes(tornadoPng); if (b) tb = { buf: b, w: tornadoW, h: tornadoH }; }
      if (tb && tb.buf) {
        children.push(P(t("Tornado · sensibilité VAN", "Tornado · NPV sensitivity", "Tornado · sensibilidad VAN"), { italics: true, spacing: { before: 100, after: 20 } }));
        const tw = 600, th = (tb.w && tb.h) ? Math.min(360, Math.round(tw * tb.h / tb.w)) : 240;
        const tp = imgParagraph(tb.buf, tw, th);
        if (tp) children.push(tp);
      } else if (reportTornado && reportTornado.length) {
        const unit = reportTornadoUnit || "M€";
        children.push(H2(t("Tornado · sensibilité VAN (" + unit + ")", "Tornado · NPV sensitivity (" + unit + ")")));
        children.push(buildTable(
          [
            { label: t("Variable", "Variable", "Variable"), width: 52 },
            { label: t("Baisse VAN", "Downside NPV", "Caída VAN"), width: 24, align: "right" },
            { label: t("Hausse VAN", "Upside NPV", "Alza VAN"), width: 24, align: "right" },
          ],
          reportTornado.map((d) => [d.n, { text: String(d.neg), color: "B91C1C" }, { text: "+" + d.pos, color: "15803D" }])
        ));
      }
    }

    // --- IX. Recommandations, conditions de mise en œuvre & risques résiduels ---
    {
      const splitItems = (txt) => String(txt || "")
        .split(/\r?\n|;|•/).map((s) => s.trim()).filter((s) => s.length > 1);
      let conditions = []
        .concat(splitItems(ident && ident.preconditions))
        .concat(splitItems(institutional && institutional.sustainability_mechanisms));
      const residual = []
        .concat(splitItems(ident && ident.main_risks))
        .concat(splitItems(institutional && institutional.institutional_risks));
      const mitig = splitItems(institutional && institutional.risk_mitigation_measures);
      // Repli illustratif si le dossier n'a pas (encore) renseigné ces champs,
      // afin que la section « Recommandations » figure toujours dans le rapport.
      if (!conditions.length && reportReco && reportReco.conditions && reportReco.conditions.length) {
        conditions = reportReco.conditions.slice();
      }
      const usingRecoRisks = !residual.length && reportReco && reportReco.risks && reportReco.risks.length;

      children.push(H1(t("IX. Recommandations, conditions & risques résiduels", "IX. Recommendations, conditions & residual risks", "IX. Recomendaciones, condiciones y riesgos residuales")));
      // Avis de synthèse (bannière)
      if (reportReco && reportReco.text) {
        children.push(P(reportReco.title, { bold: true, color: "15803D", spacing: { before: 0, after: 40 } }));
        children.push(P(reportReco.text, { spacing: { after: 120 } }));
      }
      if (finalDecision && finalDecision.decision) {
        const dl = { go: "GO", no_go: "NO-GO", conditional: t("CONDITIONNEL", "CONDITIONAL", "CONDICIONAL") }[finalDecision.decision] || finalDecision.decision;
        children.push(P([
          new TextRun({ text: t("Décision du comité : ", "Committee decision: ", "Decisión del comité: "), bold: true, size: 20 }),
          new TextRun({ text: dl, size: 20 }),
        ], { spacing: { after: 120 } }));
      }
      if (conditions.length) {
        children.push(H2(t("Conditions de mise en œuvre", "Implementation conditions", "Condiciones de implementación")));
        conditions.forEach((c) => children.push(P("• " + c, { spacing: { after: 40 } })));
      }
      if (residual.length || mitig.length) {
        children.push(H2(t("Risques résiduels à suivre", "Residual risks to monitor", "Riesgos residuales a seguir")));
        residual.forEach((r) => children.push(P("• " + r, { spacing: { after: 40 } })));
        if (mitig.length) {
          children.push(P(t("Mesures d'atténuation :", "Mitigation measures:", "Medidas de atenuación:"), { bold: true, spacing: { before: 80, after: 40 } }));
          mitig.forEach((m) => children.push(P("– " + m, { spacing: { after: 40 } })));
        }
      } else if (usingRecoRisks) {
        children.push(H2(t("Risques résiduels à suivre", "Residual risks to monitor", "Riesgos residuales a seguir")));
        children.push(buildTable(
          [
            { label: t("Risque", "Risk", "Riesgo"), width: 60 },
            { label: t("Probabilité", "Probability", "Probabilidad"), width: 20 },
            { label: t("Impact", "Impact", "Impacto"), width: 20 },
          ],
          reportReco.risks.map((r) => [r.r, r.p, r.i])
        ));
      }
    }

    // --- X. Annexes détaillées (flux année par année) — mode « rapport détaillé » ---
    if (detailed && exanteComputed) {
      const pl = exanteComputed.pl || [];
      const cf = exanteComputed.cashFlow || {};
      const dbt = exanteComputed.debt || {};
      const n = pl.length;
      if (n > 0) {
        const yrW = Math.max(6, Math.floor(76 / n));
        const yrCols = (firstLabel) => [{ label: firstLabel, width: 24 }].concat(
          Array.from({ length: n }, (_, i) => ({ label: "An " + (i + 1), width: yrW, align: "right" })));
        const op = (arr) => (arr || []).slice(1, n + 1);   // années d'exploitation (l'index 0 = an 0)
        const row = (label, arr) => [{ text: label, bold: true }].concat((arr || []).slice(0, n).map((v) => fmtNum(v)));

        children.push(H1(t("X. Annexes détaillées — flux sur " + n + " ans", "X. Detailed annexes — " + n + "-year flows")));

        // Compte de résultat prévisionnel
        children.push(H2(t("Compte de résultat prévisionnel (" + ccy + ")", "Projected income statement (" + ccy + ")")));
        children.push(buildTable(yrCols(t("Poste", "Item", "Partida")), [
          row(t("Revenus", "Revenue", "Ingresos"),               pl.map((r) => r.revenue)),
          row("OPEX",                                  pl.map((r) => r.opex)),
          row("EBITDA",                                pl.map((r) => r.ebitda)),
          row(t("Amortissement", "Depreciation", "Amortización"),      pl.map((r) => r.amortization)),
          row("EBIT",                                  pl.map((r) => r.ebit)),
          row(t("Intérêts", "Interest", "Intereses"),               pl.map((r) => r.interest)),
          row(t("Résultat avant impôt", "Pre-tax income", "Resultado antes de impuestos"), pl.map((r) => r.ebt)),
          row(t("Impôt", "Tax", "Impuesto"),                       pl.map((r) => r.tax)),
          row(t("Résultat net", "Net income", "Resultado neto"),         pl.map((r) => r.netIncome)),
        ]));

        // Flux de trésorerie
        children.push(H2(t("Flux de trésorerie (" + ccy + ")", "Cash flow (" + ccy + ")")));
        children.push(buildTable(yrCols(t("Flux", "Flow", "Flujo")), [
          row(t("FCF projet", "Project FCF", "FCF del proyecto"),                op(cf.fcfProject)),
          row(t("FCF projet cumulé", "Cumulative project FCF", "FCF del proyecto acumulado"), op(cf.cumProject)),
          row(t("FCF fonds propres", "Equity FCF", "FCF de fondos propios"),          op(cf.fcfEquity)),
          row(t("FCF f.p. cumulé", "Cumulative equity FCF", "FCF f.p. acumulado"), op(cf.cumEquity)),
        ]));

        // Échéancier de la dette (seulement s'il y a de la dette)
        const debtSvc = op(dbt.debtService);
        if (debtSvc.some((v) => Math.abs(Number(v) || 0) > 0.5)) {
          children.push(H2(t("Échéancier de la dette (" + ccy + ")", "Debt schedule (" + ccy + ")")));
          children.push(buildTable(yrCols(t("Poste", "Item", "Partida")), [
            row(t("Encours (ouverture)", "Opening balance", "Saldo (apertura)"), op(dbt.opening)),
            row(t("Intérêts", "Interest", "Intereses"),                    op(dbt.interest)),
            row(t("Principal", "Principal", "Principal"),                  op(dbt.principal)),
            row(t("Service de la dette", "Debt service", "Servicio de la deuda"),     debtSvc),
            row(t("Encours (clôture)", "Closing balance", "Saldo (cierre)"),    op(dbt.closing)),
          ]));
        }

        // DSCR par année
        if (exanteComputed.dscrSeries && exanteComputed.dscrSeries.length) {
          children.push(H2("DSCR"));
          children.push(buildTable(yrCols("DSCR"), [
            [{ text: "DSCR", bold: true }].concat(
              exanteComputed.dscrSeries.slice(0, n).map((v) =>
                (v == null || !isFinite(v)) ? "—" : (v >= 999 ? "∞" : Number(v).toFixed(2)))),
          ]));
        }
      }
    }

    // --- Footer note ---
    children.push(
      P("", { spacing: { before: 320 } }),
      P([new TextRun({ text: t("Document généré automatiquement par MELR — ", "Generated automatically by MELR — ", "Documento generado automáticamente por MELR — ") + today, italics: true, size: 16, color: "6B7280" })],
        { alignment: AlignmentType.CENTER })
    );

    // ===== Build the document =====
    const doc = new Document({
      creator: "MELR",
      title: t("Évaluation ex-ante", "Ex-ante appraisal", "Evaluación ex ante") + " — " + projectCode,
      description: "Generated by the MELR app",
      styles: {
        default: {
          document: { run: { font: "Calibri", size: 22 } },
          heading1: { run: { size: 32, bold: true, color: "1F2937" }, paragraph: { spacing: { before: 240, after: 120 }, border: { bottom: { color: "1F2937", style: BorderStyle.SINGLE, size: 12, space: 1 } } } },
          heading2: { run: { size: 26, bold: true, color: "374151" }, paragraph: { spacing: { before: 160, after: 80 } } },
          heading3: { run: { size: 22, bold: true, color: "4B5563" }, paragraph: { spacing: { before: 120, after: 60 } } },
        },
      },
      sections: [{
        properties: { page: { size: { orientation: PageOrientation.LANDSCAPE } } },
        headers: {
          default: new Header({
            children: [P([
              new TextRun({ text: t("Évaluation ex-ante — ", "Ex-ante appraisal — ", "Evaluación ex ante — "), color: "6B7280", size: 18 }),
              new TextRun({ text: projectCode, color: "6B7280", size: 18, bold: true }),
            ], { alignment: AlignmentType.RIGHT })],
          }),
        },
        footers: {
          default: new Footer({
            children: [P([new TextRun({ text: "MELR · " + today, color: "9CA3AF", size: 16 })], { alignment: AlignmentType.CENTER })],
          }),
        },
        children,
      }],
    });

    return doc;
  }

  // ---------- Public entry point ----------
  window.exportExanteDocx = async function (payload) {
    try {
      const doc = await build(payload);
      const blob = await Packer.toBlob(doc);
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      const code = (payload.dossier && payload.dossier.projects && payload.dossier.projects.code) || "exante";
      const date = new Date().toISOString().slice(0, 10);
      a.href = url;
      a.download = "exante-" + code + "-" + date + ".docx";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (e) {
      console.error("[exante-docx] Export failed:", e);
      const enErr = payload && payload.lang === "en";
      window.alert((enErr ? ".docx generation error: " : "Erreur de génération .docx : ") + e.message
        + (enErr ? "\nUse the Word (.doc) or PDF export instead." : "\nUtilisez l'export Word (.doc) ou PDF à la place."));
    }
  };
})();
