// React hooks available via global React object
const { useState, useEffect, useRef, useMemo } = React;

/* ============================================================
   CORVENDIS — Smart RFQ Analyzer · Quick + Standard modes
   Personal use first. Depth-configurable. One professional,
   any depth of work — invoice, RFQ, tender, proposal.
   ============================================================ */

const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,600;9..144,700&family=Public+Sans:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
:root{
  --ink:#0B1B16; --engine:#0E2B24; --engine2:#123729; --pine:#1F5C4D;
  --moss:#7BA893; --sage:#A9C4B7; --mist:#D9E6DF;
  --copper:#D97A3D; --copper-hot:#E8935C; --copper-deep:#B25E2A;
  --paper:#F6F4EC; --paper-edge:#E7E3D4; --paper-ink:#22302B;
  --ok:#68B984; --warn:#E3B23C; --low:#E07B5A;
}
*{box-sizing:border-box}
.cv-root{font-family:'Public Sans',system-ui,sans-serif; background:
  radial-gradient(1200px 800px at 85% -10%, #16453833 0%, transparent 60%),
  radial-gradient(900px 700px at -10% 110%, #B25E2A14 0%, transparent 55%),
  linear-gradient(160deg,#0B1F19 0%, #0E2B24 55%, #0B1B16 100%);
  color:var(--mist); min-height:100vh;}
.cv-serif{font-family:'Fraunces',Georgia,serif}
.cv-mono{font-family:'IBM Plex Mono',ui-monospace,monospace}
.traced{border-bottom:1.5px dotted var(--copper); cursor:pointer; transition:all .18s ease; padding-bottom:1px}
.traced:hover{color:var(--copper-hot); border-bottom-color:var(--copper-hot); background:#D97A3D14}
.paper{background:linear-gradient(178deg,var(--paper) 0%, #EFECDF 100%); color:var(--paper-ink);
  box-shadow:0 1px 0 var(--paper-edge) inset, 0 18px 50px -18px #00000088;}
.paper-hl{background:#E8935C55; box-shadow:0 0 0 3px #E8935C55; border-radius:2px; animation:hlpulse 1.6s ease infinite}
@keyframes hlpulse{0%,100%{box-shadow:0 0 0 3px #E8935C44}50%{box-shadow:0 0 0 6px #E8935C22}}
.stage-dot{width:10px;height:10px;border-radius:50%;background:#2E5347;transition:all .3s ease}
.stage-dot.active{background:var(--copper);box-shadow:0 0 0 5px #D97A3D2E, 0 0 18px #D97A3D66}
.stage-dot.done{background:var(--moss)}
.cvcard{background:linear-gradient(180deg,#12352B 0%, #0F2C23 100%); border:1px solid #24493E;
  border-radius:14px; box-shadow:0 14px 40px -18px #00000099; transition:transform .35s cubic-bezier(.2,.9,.3,1.2), box-shadow .3s ease, border-color .3s ease}
.cvcard:hover{border-color:#3A6B5C}
.cvcard.winner{border-color:var(--copper); box-shadow:0 0 0 1px var(--copper), 0 22px 60px -20px #D97A3D55}
.chip{font-size:10px; letter-spacing:.08em; padding:2px 8px; border-radius:999px; font-weight:600}
.chip.hi{background:#68B98422;color:#8FD8A8;border:1px solid #68B98444}
.chip.md{background:#E3B23C22;color:#EFCB74;border:1px solid #E3B23C44}
.chip.lo{background:#E07B5A22;color:#F2A183;border:1px solid #E07B5A44}
.chip.verified{background:#D97A3D22;color:var(--copper-hot);border:1px solid #D97A3D55}
.chip.anom{background:#E07B5A22;color:#F2A183;border:1px solid #E07B5A66;animation:pulse 2s ease infinite}
@keyframes pulse{50%{background:#E07B5A44}}
input[type=range]{-webkit-appearance:none;appearance:none;height:4px;border-radius:2px;background:linear-gradient(90deg,var(--copper) var(--fill,50%),#274B40 var(--fill,50%));outline:none;width:100%}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--copper-hot);border:2px solid #0E2B24;box-shadow:0 0 0 3px #D97A3D33;cursor:grab}
input[type=range]::-webkit-slider-thumb:active{cursor:grabbing;box-shadow:0 0 0 6px #D97A3D33}
.editfield{background:#0B221C;border:1px solid #26493E;border-radius:8px;color:var(--mist);padding:5px 9px;font-size:13px;width:100%;transition:border .2s}
.editfield:focus{outline:none;border-color:var(--copper)}
.console-line{opacity:0;animation:fadeup .4s ease forwards}
@keyframes fadeup{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@keyframes cardin{from{opacity:0;transform:translateY(14px) scale(.985)}to{opacity:1;transform:none}}
.cardin{animation:cardin .5s cubic-bezier(.2,.9,.3,1) both}
@keyframes drawerin{from{transform:translateX(40px);opacity:0}to{transform:none;opacity:1}}
.drawerin{animation:drawerin .35s cubic-bezier(.2,.9,.3,1) both}
@keyframes scan{0%{transform:translateY(-100%)}100%{transform:translateY(400%)}}
.scanline{animation:scan 1.8s linear infinite}
@keyframes breathe{0%,100%{opacity:.5}50%{opacity:1}}
.breathe{animation:breathe 2.2s ease infinite}
.cv-scroll::-webkit-scrollbar{width:8px;height:8px}
.cv-scroll::-webkit-scrollbar-thumb{background:#2E5347;border-radius:4px}
.cv-scroll::-webkit-scrollbar-track{background:transparent}
.btn-copper{background:linear-gradient(180deg,var(--copper-hot),var(--copper-deep));color:#1A0F07;font-weight:700;
  border-radius:10px;border:1px solid #E8935C;box-shadow:0 8px 24px -8px #D97A3D88, 0 1px 0 #FFCFA6 inset;transition:all .2s}
.btn-copper:hover{transform:translateY(-1px);box-shadow:0 12px 30px -8px #D97A3DAA, 0 1px 0 #FFCFA6 inset}
.btn-ghost{border:1px solid #2E5347;border-radius:10px;color:var(--sage);transition:all .2s;background:transparent}
.btn-ghost:hover{border-color:var(--moss);color:var(--mist);background:#1A3A30}
.btn-locked{opacity:.55;cursor:not-allowed;filter:grayscale(.3)}
.modecard{cursor:pointer;transition:all .25s ease}
.modecard:hover{transform:translateY(-2px)}
.modecard.selected{border-color:var(--copper) !important;box-shadow:0 0 0 1px var(--copper), 0 22px 60px -20px #D97A3D55}
.scoregauge{background:#0B221C;border-radius:4px;height:5px;overflow:hidden;position:relative}
.scorefill{height:100%;background:linear-gradient(90deg,var(--moss),var(--copper-hot));border-radius:4px;transition:width .5s cubic-bezier(.2,.9,.3,1)}
@media (prefers-reduced-motion: reduce){*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important}}
`;

/* ================= MODES ================= */
const MODES = {
  quick: {
    id: "quick",
    name: "Quick",
    tag: "Fast decision",
    time: "~2 minutes",
    desc: "One or a few documents. Extract, rank, and get negotiation levers. For when time is the constraint and the call is straightforward.",
    fits: ["Single invoice review", "2–3 quote comparison", "Spot-buy decisions"],
    stages: ["Observe", "Understand", "Recommend", "Learn"],
    export: "Quick decision brief",
  },
  standard: {
    id: "standard",
    name: "Standard",
    tag: "Structured evaluation",
    time: "~20 minutes",
    desc: "Multi-criteria decision analysis with technical, capability, delivery, risk & ESG scoring. Every input traceable; every weight yours.",
    fits: ["RFQ & tender evaluations", "Proposal comparisons", "Auditable decisions"],
    stages: ["Observe", "Understand", "Evaluate", "Score", "Recommend", "Learn"],
    export: "Full evaluation report",
  },
  full: {
    id: "full",
    name: "Full",
    tag: "Deep individual analysis",
    time: "~45 minutes",
    desc: "For decisions that face scrutiny. Sensitivity testing, scenario modeling, alternative award structures, and your own due-diligence tracking — all still one professional, going further.",
    fits: ["Sensitivity & scenario testing", "Split-award & backup strategies", "Board-grade memos"],
    stages: ["Observe", "Understand", "Evaluate", "Score", "Model", "Recommend", "Learn"],
    export: "Full decision memo",
  },
};

/* ================= FX ================= */
const FX = { EUR: 1, USD: 0.91, GBP: 1.17, PLN: 0.235, CHF: 1.04, CNY: 0.126, GHS: 0.058 };
const FX_DATE = "14 Jul 2026 · ECB reference";
const BASE_CHOICES = ["EUR", "USD", "GBP", "PLN", "CHF", "GHS"];
const convert = (v, from, base) => (v * (FX[from] ?? 1)) / (FX[base] ?? 1);
const crossRate = (from, base) => (FX[from] ?? 1) / (FX[base] ?? 1);
const fmtMoney = (v, base) => new Intl.NumberFormat(base === "USD" ? "en-US" : "de-DE", { style: "currency", currency: base, maximumFractionDigits: 0 }).format(v);

/* ================= CATEGORY WEIGHT PRESETS (Template §9.2) ================= */
/* Six top-level criteria weights: price, technical, capability, delivery, risk, innovation */
const PRESETS = {
  goods:     { name: "Standard Goods",         w: { price: 50, technical: 25, capability: 10, delivery: 10, risk: 5,  innovation: 0 } },
  services:  { name: "Strategic Services",     w: { price: 30, technical: 25, capability: 25, delivery: 10, risk: 5,  innovation: 5 } },
  highrisk:  { name: "High-Risk / Complex",    w: { price: 35, technical: 25, capability: 15, delivery: 10, risk: 10, innovation: 5 } },
  sustain:   { name: "Sustainability-Focused", w: { price: 40, technical: 20, capability: 10, delivery: 10, risk: 15, innovation: 5 } },
};

/* ================= SAMPLE DATA ================= */
/* Every field carries provenance: {value, conf, page, snippet}. Standard-mode scores
   are 0-10 defaults that a real user would confirm/adjust — never model-invented. */
const SUPPLIERS_SEED = [
  {
    id: "klima", name: "Klimatech GmbH", country: "Germany", flag: "DE", currency: "EUR",
    fields: {
      unit_price:   { value: 1485, conf: 0.97, page: 1, snippet: "Unit price: EUR 1,485.00 per system, ex works incl. DAP delivery Berlin" },
      quantity:     { value: 40,   conf: 0.99, page: 1, snippet: "Position 1 — 40 × KT-5200i split climate systems" },
      freight:      { value: 0,    conf: 0.95, page: 1, snippet: "Delivery DAP Berlin site — included in unit pricing" },
      installation: { value: 0,    conf: 0.93, page: 2, snippet: "Installation and commissioning by certified Klimatech engineers: included" },
      training:     { value: 600,  conf: 0.90, page: 2, snippet: "Operator training (1 day, on site): EUR 600.00" },
      maintenance_annual: { value: 1900, conf: 0.92, page: 2, snippet: "Annual maintenance contract: EUR 1,900.00 per year (optional, recommended)" },
      lead_time_days: { value: 21, conf: 0.96, page: 1, snippet: "Delivery time: 3 weeks from order confirmation" },
      warranty_months:{ value: 36, conf: 0.97, page: 2, snippet: "Warranty: 36 months full parts & labour" },
      payment_terms:  { value: "Net 30", advance: 0, conf: 0.95, page: 2, snippet: "Payment: 30 days net from date of invoice" },
      incoterm:       { value: "DAP Berlin", conf: 0.96, page: 1, snippet: "Delivery DAP Berlin site — included in unit pricing" },
    },
    // Standard-mode: user-set / integration-populated fields (defaults reasonable, all editable)
    scores: {
      technical:    { specs_met: true,   iso: true,  quality_score: 8,  docs_score: 9 },
      capability:   { years: 34, refs_score: 9, capacity_score: 8, financial_score: 9 },
      delivery:     { on_time_history: 96, lead_score: 9, tracking_score: 8 },
      quality:      { warranty_scope: 9, aftersales: 9, defect_rate: 0.4 },
      risk:         { financial: "Low", operational: "Low", compliance: "Low", geo: "Low" },
      compliance:   { biz_reg: true, tax: true, insurance: true, dpa: true },
      esg:          { environmental: 8, social: 8, governance: 9 },
    },
    doc: [
      `KLIMATECH GmbH · Industriestraße 44, 68199 Mannheim\n\nQUOTATION Q-26-0977 — RFQ-2026-114\n14 June 2026\n\nPosition 1 — 40 × KT-5200i split climate systems\nUnit price: EUR 1,485.00 per system, ex works incl. DAP delivery Berlin\nDelivery DAP Berlin site — included in unit pricing\nDelivery time: 3 weeks from order confirmation\n\nSubtotal goods: EUR 59,400.00`,
      `Page 2 — Commercial terms\n\nInstallation and commissioning by certified Klimatech engineers: included\nOperator training (1 day, on site): EUR 600.00\nAnnual maintenance contract: EUR 1,900.00 per year (optional, recommended)\nWarranty: 36 months full parts & labour\nPayment: 30 days net from date of invoice\nQuotation valid until: 30 September 2026`,
    ],
  },
  {
    id: "hanwei", name: "Hanwei Cooling Co.", country: "China", flag: "CN", currency: "USD",
    fields: {
      unit_price:   { value: 1120, conf: 0.95, page: 1, snippet: "FOB Shanghai price: USD 1,120.00 / set, MOQ 40 sets" },
      quantity:     { value: 40,   conf: 0.99, page: 1, snippet: "Quantity: 40 sets HW-C48 commercial split units" },
      freight:      { value: 3900, conf: 0.71, page: 1, snippet: "Sea freight + insurance to Hamburg, est. USD 3,900 (subject to booking)" },
      installation: { value: 5275, conf: 0.62, page: 2, snippet: "Installation: not included — buyer to arrange local contractor", note: "Estimated: local contractor, converted" },
      training:     { value: 0,    conf: 0.88, page: 2, snippet: "Technical documentation and remote video support included" },
      maintenance_annual: { value: 2400, conf: 0.68, page: 2, snippet: "Spare parts kit available; suggested annual service budget USD 2,600" },
      lead_time_days: { value: 55, conf: 0.90, page: 1, snippet: "Lead time: 35 days production + approx. 20 days sea transit" },
      warranty_months:{ value: 12, conf: 0.94, page: 2, snippet: "Warranty: 12 months on compressor and parts (return to factory)" },
      payment_terms:  { value: "50% advance, 50% at B/L", advance: 0.5, conf: 0.93, page: 2, snippet: "Payment: 50% T/T advance with order, 50% against copy of B/L" },
      incoterm:       { value: "FOB Shanghai", conf: 0.95, page: 1, snippet: "FOB Shanghai price: USD 1,120.00 / set, MOQ 40 sets" },
    },
    scores: {
      technical:    { specs_met: true,   iso: true,  quality_score: 7, docs_score: 6 },
      capability:   { years: 12, refs_score: 6, capacity_score: 9, financial_score: 6 },
      delivery:     { on_time_history: 82, lead_score: 5, tracking_score: 7 },
      quality:      { warranty_scope: 5, aftersales: 5, defect_rate: 1.8 },
      risk:         { financial: "Med", operational: "Med", compliance: "Med", geo: "Med" },
      compliance:   { biz_reg: true, tax: true, insurance: false, dpa: false },
      esg:          { environmental: 5, social: 5, governance: 6 },
    },
    doc: [
      `HANWEI COOLING CO., LTD · Ningbo, Zhejiang\n\nPROFORMA OFFER PI-26-3341 — Ref RFQ-2026-114\n\nQuantity: 40 sets HW-C48 commercial split units\nFOB Shanghai price: USD 1,120.00 / set, MOQ 40 sets\nSea freight + insurance to Hamburg, est. USD 3,900 (subject to booking)\nLead time: 35 days production + approx. 20 days sea transit`,
      `Page 2 — Terms\n\nInstallation: not included — buyer to arrange local contractor\nTechnical documentation and remote video support included\nSpare parts kit available; suggested annual service budget USD 2,600\nWarranty: 12 months on compressor and parts (return to factory)\nPayment: 50% T/T advance with order, 50% against copy of B/L\nValidity: 45 days`,
    ],
  },
  {
    id: "nowak", name: "Chłodnia Nowak Sp. z o.o.", country: "Poland", flag: "PL", currency: "PLN",
    fields: {
      unit_price:   { value: 5590, conf: 0.96, page: 1, snippet: "Cena jednostkowa / unit price: 5,590.00 PLN net per unit, DAP Berlin" },
      quantity:     { value: 40,   conf: 0.99, page: 1, snippet: "40 units NW-Klima 480 PRO" },
      freight:      { value: 0,    conf: 0.94, page: 1, snippet: "Transport DAP Berlin included (own fleet, 2 deliveries)" },
      installation: { value: 14800, conf: 0.91, page: 2, snippet: "Installation package (optional): 14,800.00 PLN net, certified team, 6 working days" },
      training:     { value: 0,    conf: 0.89, page: 2, snippet: "Handover training included in installation package" },
      maintenance_annual: { value: 8900, conf: 0.90, page: 2, snippet: "Service agreement: 8,900.00 PLN / year, incl. 2 inspections" },
      lead_time_days: { value: 30, conf: 0.95, page: 1, snippet: "Delivery: 30 calendar days from order" },
      warranty_months:{ value: 24, conf: 0.96, page: 2, snippet: "Warranty: 24 months, on-site response within 72h" },
      payment_terms:  { value: "Net 14 · 2% early-pay disc.", advance: 0, earlyDiscount: 0.02, conf: 0.66, page: 2, snippet: "Payment 14 days; 2% discount for payment within 5 days (skonto)" },
      incoterm:       { value: "DAP Berlin", conf: 0.95, page: 1, snippet: "Cena jednostkowa / unit price: 5,590.00 PLN net per unit, DAP Berlin" },
    },
    scores: {
      technical:    { specs_met: true,   iso: true,  quality_score: 8, docs_score: 8 },
      capability:   { years: 18, refs_score: 8, capacity_score: 7, financial_score: 7 },
      delivery:     { on_time_history: 93, lead_score: 7, tracking_score: 7 },
      quality:      { warranty_scope: 7, aftersales: 8, defect_rate: 0.9 },
      risk:         { financial: "Low", operational: "Low", compliance: "Low", geo: "Low" },
      compliance:   { biz_reg: true, tax: true, insurance: true, dpa: true },
      esg:          { environmental: 7, social: 7, governance: 7 },
    },
    doc: [
      `CHŁODNIA NOWAK Sp. z o.o. · ul. Przemysłowa 12, Poznań\n\nOFERTA / QUOTATION NR 118/06/2026 — RFQ-2026-114\n\n40 units NW-Klima 480 PRO\nCena jednostkowa / unit price: 5,590.00 PLN net per unit, DAP Berlin\nTransport DAP Berlin included (own fleet, 2 deliveries)\nDelivery: 30 calendar days from order`,
      `Page 2 — Warunki / Terms\n\nInstallation package (optional): 14,800.00 PLN net, certified team, 6 working days\nHandover training included in installation package\nService agreement: 8,900.00 PLN / year, incl. 2 inspections\nWarranty: 24 months, on-site response within 72h\nPayment 14 days; 2% discount for payment within 5 days (skonto)\nOferta ważna / valid: 60 days`,
    ],
  },
];

const TWIN = {
  lastAnalysis: { label: "Q1 2026 · chiller retrofit, similar category", bestUnitEUR: 985, chosen: "domestic supplier", reason: "you weighted lead time & warranty over price" },
  usualWeights: { price: 30, lead: 32, warranty: 24, payment: 14 },
  usualPreset: "goods",
};

const COST_OF_CASH = 0.08;

/* ================= DETERMINISTIC ENGINE ================= */
function computeTCO(s, base = "EUR") {
  const f = s.fields, ccy = s.currency;
  const goods = convert(f.unit_price.value * f.quantity.value, ccy, base);
  const freight = convert(f.freight.value, ccy, base);
  const install = f.installation.note ? convert(f.installation.value, "EUR", base) : convert(f.installation.value, ccy, base);
  const training = convert(f.training.value, ccy, base);
  const maint3 = convert(f.maintenance_annual.value, ccy, base) * 3;
  const advance = f.payment_terms.advance || 0;
  const financing = advance > 0 ? advance * (goods + freight) * COST_OF_CASH * (f.lead_time_days.value / 365) : 0;
  const discount = f.payment_terms.earlyDiscount ? -(f.payment_terms.earlyDiscount * goods) : 0;
  const total = goods + freight + install + training + maint3 + financing + discount;
  return { goods, freight, install, training, maint3, financing, discount, total };
}

function paymentScore(f) {
  const adv = f.payment_terms.advance || 0;
  const disc = f.payment_terms.earlyDiscount || 0;
  let s = 0.7 - adv * 0.7 + disc * 6;
  if (/net 30/i.test(f.payment_terms.value)) s += 0.15;
  return Math.max(0, Math.min(1, s));
}

/* Quick-mode ranking (four sliders — the original mixer) */
function rankQuick(suppliers, weights, base) {
  const tcos = suppliers.map(s => ({ s, tco: computeTCO(s, base) }));
  const minT = Math.min(...tcos.map(x => x.tco.total)), maxT = Math.max(...tcos.map(x => x.tco.total));
  const minL = Math.min(...suppliers.map(s => s.fields.lead_time_days.value)), maxL = Math.max(...suppliers.map(s => s.fields.lead_time_days.value));
  const maxW = Math.max(...suppliers.map(s => s.fields.warranty_months.value));
  const wSum = weights.price + weights.lead + weights.warranty + weights.payment || 1;
  return tcos.map(({ s, tco }) => {
    const nPrice = maxT === minT ? 1 : 1 - (tco.total - minT) / (maxT - minT);
    const nLead = maxL === minL ? 1 : 1 - (s.fields.lead_time_days.value - minL) / (maxL - minL);
    const nWar = s.fields.warranty_months.value / maxW;
    const nPay = paymentScore(s.fields);
    const score = (weights.price * nPrice + weights.lead * nLead + weights.warranty * nWar + weights.payment * nPay) / wSum;
    return { s, tco, score, parts: { nPrice, nLead, nWar, nPay } };
  }).sort((a, b) => b.score - a.score);
}

/* Standard-mode MCDA: six criteria weights (Template §9.1) */
function riskInverse(scores) {
  const map = { Low: 90, Med: 60, High: 30 };
  const arr = [scores.risk.financial, scores.risk.operational, scores.risk.compliance, scores.risk.geo].map(k => map[k] ?? 60);
  return arr.reduce((a, b) => a + b, 0) / arr.length;
}
function pricePoints(tcos) {
  // Score price relative to the cheapest, penalising the % premium proportionally.
  // 0% premium = 100, 10% premium = 80, 25% premium = 50, 50%+ premium = 0.
  // Prevents a modest cost gap from wiping out other well-scored criteria.
  const minT = Math.min(...tcos.map(x => x.tco.total));
  return tcos.map(x => {
    const premium = minT > 0 ? (x.tco.total - minT) / minT : 0;
    const priceScore = Math.max(0, 100 - premium * 200);
    return { s: x.s, tco: x.tco, priceScore };
  });
}
function rankStandard(suppliers, weights, base) {
  const tcos = suppliers.map(s => ({ s, tco: computeTCO(s, base) }));
  const withPrice = pricePoints(tcos);
  const wSum = weights.price + weights.technical + weights.capability + weights.delivery + weights.risk + weights.innovation || 1;
  return withPrice.map(({ s, tco, priceScore }) => {
    const sc = s.scores;
    // Technical: compliance is a mandatory gate (25%), certifications 15%, quality 30%, docs 30%.
    // Compliance treated as pass-heavy but not free — non-compliance halves the section.
    const gate = (sc.technical.specs_met ? 1 : 0.5) * (sc.technical.iso ? 1 : 0.8);
    const technical = gate * (sc.technical.quality_score * 6 + sc.technical.docs_score * 4);
    // Capability: 70% averaged sub-scores × 10, 30% experience curve saturating at 25 years.
    const capAvg = (sc.capability.refs_score + sc.capability.capacity_score + sc.capability.financial_score) / 3 * 10;
    const capExp = Math.min(100, sc.capability.years * 4);
    const capability = capAvg * 0.7 + capExp * 0.3;
    // Delivery: 50% history %, 25% lead-time score, 25% tracking score.
    const delivery = sc.delivery.on_time_history * 0.5 + sc.delivery.lead_score * 2.5 + sc.delivery.tracking_score * 2.5;
    const risk = riskInverse(sc);
    // Innovation surface (template §8): warranty scope + after-sales + defect-rate signal.
    // Defect rate is a % — invert & cap; 0% = 100, 2% = 0.
    const defectScore = Math.max(0, 100 - sc.quality.defect_rate * 50);
    const innovation = (sc.quality.warranty_scope * 4 + sc.quality.aftersales * 4 + defectScore * 0.2);
    const total = (weights.price * priceScore + weights.technical * technical + weights.capability * capability + weights.delivery * delivery + weights.risk * risk + weights.innovation * innovation) / wSum;
    const esgScore = (sc.esg.environmental + sc.esg.social + sc.esg.governance) / 3 * 10;
    return { s, tco, score: total / 100, mcda: { price: priceScore, technical, capability, delivery, risk, innovation }, esgScore };
  }).sort((a, b) => b.score - a.score);
}

/* ================= ANOMALY DETECTION (Template §16.1) ================= */
function detectAnomalies(suppliers, base) {
  const flags = {};
  const tcos = suppliers.map(s => ({ s, tco: computeTCO(s, base) }));
  const totals = tcos.map(x => x.tco.total);
  const mean = totals.reduce((a, b) => a + b, 0) / totals.length;
  const sd = Math.sqrt(totals.reduce((s, v) => s + (v - mean) ** 2, 0) / totals.length);
  tcos.forEach(({ s, tco }) => {
    flags[s.id] = flags[s.id] || [];
    if (Math.abs(tco.total - mean) > 2 * sd && sd > 0) flags[s.id].push({ kind: "OUTLIER", msg: `TCO is >2σ from group mean (${fmtMoney(mean, base)})` });
    if (tco.total < mean * 0.7) flags[s.id].push({ kind: "SUSPICIOUS-LOW", msg: `TCO <70% of average — verify inclusions & specifications` });
    if (tco.total > mean * 1.3) flags[s.id].push({ kind: "HIGH-PRICE", msg: `TCO >130% of average — priority negotiation target` });
    if ((s.fields.payment_terms.advance || 0) >= 0.5) flags[s.id].push({ kind: "CASH-RISK", msg: `${Math.round(s.fields.payment_terms.advance*100)}% advance payment ties up cash` });
    if (s.fields.warranty_months.value < 18) flags[s.id].push({ kind: "WARRANTY", msg: `Warranty only ${s.fields.warranty_months.value} months — below industry norm` });
    ["unit_price","freight","installation","payment_terms"].forEach(k => {
      const f = s.fields[k]; if (f && f.conf < 0.7 && !f.verified) flags[s.id].push({ kind: "LOW-CONF", msg: `${k.replace(/_/g," ")} extracted at ${(f.conf*100).toFixed(0)}% confidence — please verify` });
    });
  });
  return flags;
}
/* ================= ROOT ================= */
function CorvendisPrototype() {
  const [mode, setMode] = useState("quick");
  const [stage, setStage] = useState("intake"); // intake | running | review | decision
  const [suppliers, setSuppliers] = useState(null);
  const [quickWeights, setQuickWeights] = useState({ price: 40, lead: 25, warranty: 20, payment: 15 });
  const [preset, setPreset] = useState("goods");
  const [stdWeights, setStdWeights] = useState(PRESETS.goods.w);
  const [evidence, setEvidence] = useState(null);
  const [uploadState, setUploadState] = useState({ busy: false, error: null });
  const [baseCcy, setBaseCcy] = useState("EUR");
  const [fullState, setFullState] = useState({ scenarios: {}, dueDiligence: {} });
  const [recommendation, setRecommendation] = useState(null);

  const loadSample = () => { setSuppliers(JSON.parse(JSON.stringify(SUPPLIERS_SEED))); setStage("running"); };
  const changePreset = k => { setPreset(k); setStdWeights({ ...PRESETS[k].w }); };

  return (
    <div className="cv-root">
      <style>{CSS}</style>
      <Header stage={stage} baseCcy={baseCcy} setBaseCcy={setBaseCcy} mode={mode} onEditMode={() => { setStage("intake"); setSuppliers(null); setRecommendation(null); }} />
      <div className="flex" style={{ minHeight: "calc(100vh - 76px)" }}>
        <PipelineRail stage={stage} mode={mode} />
        <main className="flex-1 px-6 py-6 lg:px-10 cv-scroll" style={{ overflowY: "auto", maxHeight: "calc(100vh - 76px)" }}>
          {stage === "intake" && <Intake mode={mode} setMode={setMode} onSample={loadSample} uploadState={uploadState} setUploadState={setUploadState} onExtracted={(sup) => { setSuppliers(sup); setStage("running"); }} />}
          {stage === "running" && <RunRitual mode={mode} onDone={() => setStage("review")} />}
          {stage === "review" && suppliers && <Review mode={mode} suppliers={suppliers} setSuppliers={setSuppliers} onEvidence={setEvidence} onConfirm={() => setStage("decision")} baseCcy={baseCcy} />}
          {stage === "decision" && suppliers && <Decision mode={mode} suppliers={suppliers} setSuppliers={setSuppliers}
              quickWeights={quickWeights} setQuickWeights={setQuickWeights}
              stdWeights={stdWeights} setStdWeights={setStdWeights}
              preset={preset} changePreset={changePreset}
              fullState={fullState} setFullState={setFullState}
              recommendation={recommendation} setRecommendation={setRecommendation}
              onEvidence={setEvidence} onBack={() => setStage("review")} baseCcy={baseCcy} setBaseCcy={setBaseCcy} />}
        </main>
        {evidence && <EvidenceDrawer evidence={evidence} suppliers={suppliers} onClose={() => setEvidence(null)} />}
      </div>
    </div>
  );
}

/* ================= HEADER ================= */
function Header({ stage, baseCcy, setBaseCcy, mode, onEditMode }) {
  const m = MODES[mode];
  return (
    <header className="flex items-center justify-between px-6 lg:px-10 py-4 flex-wrap gap-3" style={{ borderBottom: "1px solid #1E4237", background: "#0C231CCC", backdropFilter: "blur(8px)", position: "sticky", top: 0, zIndex: 40 }}>
      <div className="flex items-center gap-3">
        <div className="cv-serif" style={{ fontSize: 22, fontWeight: 600, color: "#F0EFE6", letterSpacing: ".02em" }}>CORVENDIS</div>
        <div className="chip verified cv-mono" style={{ marginTop: 2 }}>ANALYST · SMART RFQ</div>
      </div>
      <div className="hidden md:flex items-center gap-3 text-xs" style={{ color: "var(--sage)" }}>
        <span className="cv-mono" style={{ color: "var(--moss)" }}>RFQ-2026-114</span>
        <span style={{ opacity: .4 }}>·</span>
        <span>40 split AC units + installation, Berlin</span>
      </div>
      <div className="flex items-center gap-3">
        <button className="text-[11px] cv-mono flex items-center gap-1.5 px-2 py-1 rounded" style={{ color: "var(--copper-hot)", background: "#B25E2A22", border: "1px solid #B25E2A44" }} onClick={onEditMode} title="Switch analysis mode">
          <span style={{ letterSpacing: ".08em" }}>{m.name.toUpperCase()} MODE</span>
          <span style={{ opacity: .7, fontSize: 10 }}>⇄</span>
        </button>
        <label className="flex items-center gap-1.5 text-[11px] cv-mono" style={{ color: "var(--moss)" }} title={`Cross-rates captured ${FX_DATE}`}>
          <span className="hidden sm:inline" style={{ letterSpacing: ".08em" }}>DECIDE IN</span>
          <select value={baseCcy} onChange={e => setBaseCcy(e.target.value)}
            className="cv-mono" style={{ background: "#0B221C", border: "1px solid #2E5347", borderRadius: 8, color: "var(--copper-hot)", padding: "4px 8px", fontSize: 12, cursor: "pointer" }}>
            {BASE_CHOICES.map(c => <option key={c} value={c}>{c}</option>)}
          </select>
        </label>
      </div>
    </header>
  );
}

/* ================= PIPELINE RAIL — dynamic per mode ================= */
function PipelineRail({ stage, mode }) {
  const stages = MODES[mode].stages;
  const stageMap = { intake: 0, running: Math.min(2, stages.length - 2), review: Math.min(2, stages.length - 2), decision: stages.length - 2 };
  const idx = stageMap[stage] ?? 0;
  return (
    <aside className="hidden lg:flex flex-col items-center gap-0 pt-10 px-5" style={{ borderRight: "1px solid #1E4237", width: 150 }}>
      {stages.map((st, i) => (
        <div key={st} className="flex flex-col items-center">
          <div className="flex items-center gap-3" style={{ width: 110 }}>
            <div className={`stage-dot ${i < idx ? "done" : i === idx ? "active" : ""}`} />
            <div className="text-xs" style={{ color: i === idx ? "var(--copper-hot)" : i < idx ? "var(--moss)" : "#3E5F53", fontWeight: i === idx ? 700 : 500, letterSpacing: ".04em" }}>{st}</div>
          </div>
          {i < stages.length - 1 && <div style={{ width: 1, height: 36, background: "linear-gradient(#2E5347,#1E4237)", margin: "6px 0 6px -100px" }} />}
        </div>
      ))}
      <div className="mt-auto mb-8 text-center">
        <div className="text-[10px] cv-mono" style={{ color: "#3E5F53", lineHeight: 1.7 }}>MODEL EXTRACTS<br />CODE CALCULATES<br />YOU DECIDE</div>
      </div>
    </aside>
  );
}

/* ================= INTAKE ================= */
function Intake({ mode, setMode, onSample, uploadState, setUploadState, onExtracted }) {
  const fileRef = useRef(null);

  async function handleFiles(files) {
    if (!files?.length) return;
    setUploadState({ busy: true, error: null });
    try {
      const docs = await Promise.all([...files].slice(0, 3).map(f => new Promise((res, rej) => {
        const r = new FileReader();
        r.onload = () => res({ name: f.name, media: f.type || "application/pdf", data: r.result.split(",")[1] });
        r.onerror = () => rej(new Error("Could not read " + f.name));
        r.readAsDataURL(f);
      })));
      const content = [];
      docs.forEach(d => content.push({ type: "document", source: { type: "base64", media_type: d.media, data: d.data } }));
      content.push({ type: "text", text: `Extract each supplier quote into this exact JSON array. For EVERY field include value, conf (0-1), page (int), snippet (short exact quote). Use null value + conf 0 if absent. Respond ONLY with raw JSON.
[{"id":"s1","name":"","country":"","flag":"","currency":"EUR|USD|PLN|GBP|CNY",
"fields":{"unit_price":{"value":0,"conf":0,"page":1,"snippet":""},"quantity":{...},"freight":{...},"installation":{...},"training":{...},"maintenance_annual":{...},"lead_time_days":{...},"warranty_months":{...},"payment_terms":{"value":"","advance":0,"conf":0,"page":1,"snippet":""},"incoterm":{...}},
"scores":{"technical":{"specs_met":true,"iso":true,"quality_score":7,"docs_score":7},"capability":{"years":10,"refs_score":7,"capacity_score":7,"financial_score":7},"delivery":{"on_time_history":90,"lead_score":7,"tracking_score":7},"quality":{"warranty_scope":7,"aftersales":7,"defect_rate":1.0},"risk":{"financial":"Low","operational":"Low","compliance":"Low","geo":"Low"},"compliance":{"biz_reg":true,"tax":true,"insurance":true,"dpa":true},"esg":{"environmental":6,"social":6,"governance":6}},
"doc":["full text page 1","full text page 2"]}]

For scores: extract only what's actually stated in the document. Where not stated, use conservative defaults so the user can fill in.` });
      const resp = await fetch("https://api.anthropic.com/v1/messages", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 1000, messages: [{ role: "user", content }] }),
      });
      const data = await resp.json();
      const txt = (data.content || []).filter(b => b.type === "text").map(b => b.text).join("\n").replace(/```json|```/g, "").trim();
      const parsed = JSON.parse(txt);
      if (!Array.isArray(parsed) || !parsed.length) throw new Error("No quotes recognised.");
      setUploadState({ busy: false, error: null });
      onExtracted(parsed);
    } catch (e) {
      setUploadState({ busy: false, error: e.message || "Extraction failed — try the sample set." });
    }
  }

  return (
    <div className="max-w-5xl mx-auto mt-6 lg:mt-10 cardin">
      <div className="cv-serif" style={{ fontSize: "clamp(28px,4vw,42px)", fontWeight: 500, lineHeight: 1.14, color: "#F0EFE6" }}>
        Turn quotes into a decision <span style={{ color: "var(--copper-hot)" }}>you can defend.</span>
      </div>
      <p className="mt-3 text-sm" style={{ color: "var(--sage)", maxWidth: 640, lineHeight: 1.7 }}>
        Choose your depth. The model extracts every field with a source trace; deterministic code computes the numbers; you correct anything it got wrong. No figure appears without a receipt.
      </p>

      {/* MODE SELECTOR */}
      <div className="mt-8">
        <div className="cv-mono text-[10px] mb-3" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>1 · CHOOSE DEPTH</div>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
          {Object.values(MODES).map(m => (
            <div key={m.id} className={`cvcard modecard p-4 ${mode === m.id && !m.locked ? "selected" : ""} ${m.locked ? "btn-locked" : ""}`}
                 onClick={() => !m.locked && setMode(m.id)}>
              <div className="flex items-center justify-between">
                <div className="cv-serif" style={{ fontSize: 20, color: mode === m.id && !m.locked ? "var(--copper-hot)" : "#EDF3EE" }}>{m.name}</div>
                {m.locked && <span className="chip lo cv-mono">SOON</span>}
                {mode === m.id && !m.locked && <span className="chip verified cv-mono">SELECTED</span>}
              </div>
              <div className="cv-mono text-[10.5px] mt-0.5" style={{ color: "var(--copper)", letterSpacing: ".1em" }}>{m.tag.toUpperCase()} · {m.time.toUpperCase()}</div>
              <div className="text-[12.5px] mt-2" style={{ color: "var(--sage)", lineHeight: 1.55 }}>{m.desc}</div>
              <div className="mt-3 pt-2" style={{ borderTop: "1px dashed #1C4034" }}>
                {m.fits.map((f, i) => (<div key={i} className="text-[11px] flex items-start gap-1.5" style={{ color: "#94AFA3" }}>
                  <span style={{ color: "var(--copper)" }}>·</span><span>{f}</span>
                </div>))}
              </div>
            </div>
          ))}
        </div>
        <div className="text-[10.5px] mt-2" style={{ color: "#4E7264" }}>Personal use, any depth — from a single invoice to a full tender or proposal evaluation. The unit stays one professional; the analysis scales with the work.</div>
      </div>

      {/* INTAKE */}
      <div className="mt-8">
        <div className="cv-mono text-[10px] mb-3" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>2 · DOCUMENT INTAKE</div>
        <div onDragOver={e => e.preventDefault()} onDrop={e => { e.preventDefault(); handleFiles(e.dataTransfer.files); }}
             className="rounded-2xl p-7 lg:p-8 text-center relative overflow-hidden"
             style={{ border: "1.5px dashed #35604F", background: "#0E2A2255" }}>
          <div className="absolute inset-x-0 h-16 scanline pointer-events-none" style={{ background: "linear-gradient(180deg,transparent,#7BA89311,transparent)" }} />
          <div className="flex flex-wrap items-center justify-center gap-3">
            <button className="btn-copper px-6 py-3 text-sm" onClick={onSample}>▸ Run sample RFQ set</button>
            <button className="btn-ghost px-6 py-3 text-sm" disabled={uploadState.busy} onClick={() => fileRef.current?.click()}>
              {uploadState.busy ? "Extracting…" : "Upload real quotes (beta)"}
            </button>
            <input ref={fileRef} type="file" multiple accept=".pdf,image/*" className="hidden" onChange={e => handleFiles(e.target.files)} />
          </div>
          <div className="mt-4 text-[11px]" style={{ color: "#4E7264" }}>Sample: 3 real-shaped quotes · Germany · China · Poland · 3 currencies · deliberately messy terms</div>
          {uploadState.error && <div className="mt-3 text-xs" style={{ color: "var(--low)" }}>{uploadState.error}</div>}
        </div>
      </div>
    </div>
  );
}

/* ================= RUN RITUAL ================= */
const RUN_LOG_QUICK = [
  ["OBSERVE", "3 documents ingested · 6 pages · 2 layouts · 1 scanned table"],
  ["OBSERVE", "Currencies detected: EUR · USD · PLN — cross-rates captured (" + FX_DATE + ")"],
  ["UNDERSTAND", "Extracting commercial fields · 10 per supplier · provenance attached"],
  ["UNDERSTAND", "Confidence scoring · low-conf items flagged for your review"],
  ["RECOMMEND", "Deterministic TCO engine · 7 cost components · unit-tested"],
  ["LEARN", "Twin: matched against Q1 2026 chiller retrofit · benchmark loaded"],
];
const RUN_LOG_STANDARD = [
  ["OBSERVE", "3 documents ingested · 6 pages · 2 layouts · 1 scanned table"],
  ["OBSERVE", "Currencies detected: EUR · USD · PLN — cross-rates captured (" + FX_DATE + ")"],
  ["UNDERSTAND", "Commercial extraction: 10 fields × 3 suppliers · provenance attached"],
  ["UNDERSTAND", "Technical compliance parsed · ISO / spec references identified"],
  ["EVALUATE", "Capability profile scaffolded from quote metadata"],
  ["EVALUATE", "Risk indicators: geo · financial · operational · compliance"],
  ["EVALUATE", "Compliance checklist populated from supplier documentation"],
  ["EVALUATE", "ESG signals extracted where documented; unstated fields flagged"],
  ["SCORE", "Anomaly detection: outliers · <70% price · missing certifications"],
  ["SCORE", "MCDA scaffold ready · category preset waiting for your confirmation"],
  ["RECOMMEND", "Deterministic weighted totals · six criteria · every input traceable"],
  ["LEARN", "Twin: matched against Q1 2026 chiller retrofit · benchmark loaded"],
];
const RUN_LOG_FULL = [
  ["OBSERVE", "3 documents ingested · 6 pages · 2 layouts · 1 scanned table"],
  ["OBSERVE", "Currencies detected: EUR · USD · PLN — cross-rates captured (" + FX_DATE + ")"],
  ["UNDERSTAND", "Commercial extraction: 10 fields × 3 suppliers · provenance attached"],
  ["UNDERSTAND", "Technical compliance parsed · ISO / spec references identified"],
  ["EVALUATE", "Six-dimension supplier profile scaffolded"],
  ["EVALUATE", "Risk register populated · financial · operational · geopolitical"],
  ["EVALUATE", "Compliance signals extracted · document-on-file checklist prepared"],
  ["SCORE", "Anomaly detection: outliers · sub-70% pricing · missing certifications"],
  ["SCORE", "MCDA scaffold ready · category preset awaits confirmation"],
  ["MODEL", "Sensitivity engine primed — 5 weight perturbations queued"],
  ["MODEL", "Scenario space initialised · concession / delay / warranty deltas ready"],
  ["MODEL", "Alternative award structures computed · single / split / primary+backup / frame"],
  ["RECOMMEND", "Deterministic weighted totals · every input traceable · every scenario reproducible"],
  ["LEARN", "Twin: matched against Q1 2026 chiller retrofit · benchmark loaded"],
];
function RunRitual({ mode, onDone }) {
  const log = mode === "full" ? RUN_LOG_FULL : mode === "standard" ? RUN_LOG_STANDARD : RUN_LOG_QUICK;
  const [lines, setLines] = useState(0);
  useEffect(() => {
    if (lines < log.length) { const t = setTimeout(() => setLines(l => l + 1), mode === "quick" ? 340 : 230); return () => clearTimeout(t); }
    const t = setTimeout(onDone, 500); return () => clearTimeout(t);
  }, [lines, onDone, log.length, mode]);
  return (
    <div className="max-w-2xl mx-auto mt-16 lg:mt-24 cardin">
      <div className="flex items-center gap-3 mb-6">
        <div className="w-2.5 h-2.5 rounded-full breathe" style={{ background: "var(--copper-hot)" }} />
        <div className="cv-serif" style={{ fontSize: 26, color: "#F0EFE6" }}>{mode === "full" ? "Running deep analysis…" : mode === "standard" ? "Running full evaluation…" : "Reading your quotes…"}</div>
      </div>
      <div className="rounded-xl p-5 cv-mono text-[12.5px]" style={{ background: "#081712", border: "1px solid #1E4237", minHeight: 300 }}>
        {log.slice(0, lines).map(([tag, msg], i) => (
          <div key={i} className="console-line flex gap-3 py-1">
            <span style={{ color: "var(--copper)", minWidth: 96 }}>[{tag}]</span>
            <span style={{ color: "var(--mist)" }}>{msg}</span>
          </div>
        ))}
        {lines < log.length && <span className="breathe" style={{ color: "var(--moss)" }}>▊</span>}
      </div>
      <button className="btn-ghost px-4 py-2 mt-4 text-xs" onClick={onDone}>Skip →</button>
    </div>
  );
}

/* ================= REVIEW ================= */
const FIELD_META = [
  ["unit_price", "Unit price", "money"], ["quantity", "Quantity", "num"], ["freight", "Freight", "money"],
  ["installation", "Installation", "money"], ["training", "Training", "money"], ["maintenance_annual", "Maintenance / yr", "money"],
  ["lead_time_days", "Lead time (days)", "num"], ["warranty_months", "Warranty (months)", "num"], ["payment_terms", "Payment terms", "text"], ["incoterm", "Incoterm", "text"],
];

function Review({ mode, suppliers, setSuppliers, onEvidence, onConfirm, baseCcy }) {
  const anomalies = useMemo(() => detectAnomalies(suppliers, baseCcy), [suppliers, baseCcy]);
  const flagged = suppliers.reduce((n, s) => n + FIELD_META.filter(([k]) => (s.fields[k]?.conf ?? 1) < 0.85 && !s.fields[k]?.verified).length, 0);
  const editField = (sid, key, raw) => {
    setSuppliers(prev => prev.map(s => {
      if (s.id !== sid) return s;
      const f = { ...s.fields }; const meta = FIELD_META.find(m => m[0] === key);
      const v = meta[2] === "text" ? raw : (parseFloat(String(raw).replace(/[^\d.,-]/g, "").replace(",", ".")) || 0);
      f[key] = { ...f[key], value: v, conf: 1, verified: true };
      return { ...s, fields: f };
    }));
  };
  const editScore = (sid, section, key, raw) => {
    setSuppliers(prev => prev.map(s => {
      if (s.id !== sid) return s;
      const sc = { ...s.scores, [section]: { ...s.scores[section] } };
      sc[section][key] = typeof s.scores[section][key] === "boolean" ? (raw === "true" || raw === true) : (typeof s.scores[section][key] === "string" ? raw : (parseFloat(raw) || 0));
      return { ...s, scores: sc };
    }));
  };

  return (
    <div className="cardin">
      <div className="flex flex-wrap items-end justify-between gap-4 mb-4">
        <div>
          <div className="cv-mono text-[11px] mb-1" style={{ color: "var(--copper)", letterSpacing: ".14em" }}>REVIEW · THE ACCURACY GATE · {MODES[mode].name.toUpperCase()} MODE</div>
          <div className="cv-serif" style={{ fontSize: 28, color: "#F0EFE6" }}>Check what the model read.</div>
          <div className="text-sm mt-1" style={{ color: "var(--sage)" }}>
            {flagged > 0 ? <>Start with the <b style={{ color: "var(--warn)" }}>{flagged} flagged fields</b> — every value is editable, every source is one click away.</> : "All commercial fields verified or high-confidence."}
            {mode === "standard" && <> {" "}Then confirm the evaluation scores below each supplier.</>}
          </div>
        </div>
        <button className="btn-copper px-6 py-3 text-sm" onClick={onConfirm}>Confirm & compute decision →</button>
      </div>

      {/* Anomaly banner */}
      {Object.values(anomalies).flat().length > 0 && (
        <div className="rounded-xl px-4 py-3 mb-5" style={{ background: "#E07B5A11", border: "1px dashed #E07B5A55" }}>
          <div className="cv-mono text-[10px] mb-1" style={{ color: "#F2A183", letterSpacing: ".14em" }}>ANOMALY DETECTION · {Object.values(anomalies).flat().length} FLAG{Object.values(anomalies).flat().length === 1 ? "" : "S"}</div>
          <div className="text-[12px]" style={{ color: "#F5D6C4" }}>
            Automatic checks against the group — outliers, sub-70% pricing, missing certifications, cash-risk terms, low extraction confidence. Click any flag on the supplier cards for context.
          </div>
        </div>
      )}

      <div className="grid grid-cols-1 xl:grid-cols-3 gap-4">
        {suppliers.map((s, si) => (
          <div key={s.id} className="cvcard p-4 cardin" style={{ animationDelay: `${si * 100}ms` }}>
            <div className="flex items-center justify-between mb-3">
              <div>
                <div className="font-semibold" style={{ color: "#EDF3EE", fontSize: 15 }}>{s.name}</div>
                <div className="text-[11px] cv-mono" style={{ color: "var(--moss)" }}>{s.country} · quotes in {s.currency}{s.currency !== baseCcy && ` → ${baseCcy} @ ${crossRate(s.currency, baseCcy).toFixed(4)}`}</div>
              </div>
              <div className="cv-serif" style={{ fontSize: 22, color: "#2E5347" }}>{String(si + 1).padStart(2, "0")}</div>
            </div>

            {/* Anomaly flags */}
            {anomalies[s.id]?.length > 0 && (
              <div className="mb-3 flex flex-wrap gap-1.5">
                {anomalies[s.id].map((a, i) => (
                  <span key={i} className="chip anom cv-mono" title={a.msg}>⚑ {a.kind}</span>
                ))}
              </div>
            )}

            {/* Commercial fields */}
            <div className="cv-mono text-[10px] mb-1.5" style={{ color: "var(--copper)", letterSpacing: ".12em" }}>COMMERCIAL</div>
            <div className="space-y-1.5">
              {FIELD_META.map(([key, label, kind]) => {
                const f = s.fields[key]; if (!f) return null;
                const cls = f.verified ? "verified" : (f.conf >= 0.85 ? "hi" : f.conf >= 0.7 ? "md" : "lo");
                return (
                  <div key={key} className="flex items-center gap-2 py-0.5" style={{ borderBottom: "1px dashed #1C4034" }}>
                    <div className="text-[11px]" style={{ color: "var(--sage)", width: 116, flexShrink: 0 }}>{label}</div>
                    <EditCell value={f.value} kind={kind} ccy={s.currency} onSave={v => editField(s.id, key, v)} />
                    <button className={`chip ${cls} cv-mono`} title="View source" onClick={() => onEvidence({ supplierId: s.id, fieldKey: key })}>
                      {f.verified ? "✓ YOU" : f.conf >= 0.85 ? "HIGH" : f.conf >= 0.7 ? "MED" : "LOW"}
                    </button>
                  </div>
                );
              })}
            </div>

            {/* Standard-mode evaluation scores */}
            {mode === "standard" && (
              <div className="mt-4 pt-3" style={{ borderTop: "1px dashed #1C4034" }}>
                <div className="cv-mono text-[10px] mb-2" style={{ color: "var(--copper)", letterSpacing: ".12em" }}>EVALUATION SCORES · CONFIRM OR EDIT</div>
                <ScoreEditor scores={s.scores} onEdit={(section, key, val) => editScore(s.id, section, key, val)} />
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

function EditCell({ value, kind, ccy, onSave }) {
  const [editing, setEditing] = useState(false);
  const [v, setV] = useState(value);
  useEffect(() => setV(value), [value]);
  if (editing) return (
    <input autoFocus className="editfield cv-mono" value={v}
      onChange={e => setV(e.target.value)}
      onBlur={() => { setEditing(false); if (String(v) !== String(value)) onSave(v); }}
      onKeyDown={e => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setV(value); setEditing(false); } }} />
  );
  return (
    <button className="flex-1 text-left cv-mono text-[12.5px] px-2 py-0.5 rounded hover:bg-[#10302699]" style={{ color: "#E8EFEA" }} onClick={() => setEditing(true)} title="Click to correct">
      {kind === "money" ? `${Number(value).toLocaleString("de-DE")} ${ccy}` : String(value)}
    </button>
  );
}

const SCORE_SECTIONS = [
  { key: "technical",   label: "Technical",   items: [["specs_met", "Specs met", "bool"], ["iso", "ISO 9001", "bool"], ["quality_score", "Quality /10", "num"], ["docs_score", "Documentation /10", "num"]] },
  { key: "capability",  label: "Capability",  items: [["years", "Years in business", "num"], ["refs_score", "References /10", "num"], ["capacity_score", "Capacity /10", "num"], ["financial_score", "Financial /10", "num"]] },
  { key: "delivery",    label: "Delivery",    items: [["on_time_history", "On-time history %", "num"], ["lead_score", "Lead-time /10", "num"], ["tracking_score", "Tracking /10", "num"]] },
  { key: "quality",     label: "Quality & warranty", items: [["warranty_scope", "Warranty scope /10", "num"], ["aftersales", "After-sales /10", "num"], ["defect_rate", "Defect rate %", "num"]] },
  { key: "risk",        label: "Risk",        items: [["financial", "Financial", "risk"], ["operational", "Operational", "risk"], ["compliance", "Compliance", "risk"], ["geo", "Geopolitical", "risk"]] },
  { key: "compliance",  label: "Compliance",  items: [["biz_reg", "Business reg.", "bool"], ["tax", "Tax", "bool"], ["insurance", "Insurance", "bool"], ["dpa", "Data protection", "bool"]] },
  { key: "esg",         label: "ESG",         items: [["environmental", "Environmental /10", "num"], ["social", "Social /10", "num"], ["governance", "Governance /10", "num"]] },
];

function ScoreEditor({ scores, onEdit }) {
  const [openSection, setOpenSection] = useState("technical");
  return (
    <div>
      <div className="flex flex-wrap gap-1 mb-2">
        {SCORE_SECTIONS.map(sec => (
          <button key={sec.key} className={`text-[10.5px] cv-mono px-2.5 py-1 rounded ${openSection === sec.key ? "" : "btn-ghost"}`}
            style={openSection === sec.key ? { background: "#D97A3D22", color: "var(--copper-hot)", border: "1px solid #D97A3D66" } : {}}
            onClick={() => setOpenSection(sec.key)}>{sec.label}</button>
        ))}
      </div>
      <div className="space-y-1.5">
        {SCORE_SECTIONS.find(s => s.key === openSection).items.map(([k, label, kind]) => {
          const val = scores[openSection][k];
          return (
            <div key={k} className="flex items-center gap-2 text-[11.5px]">
              <div style={{ color: "var(--sage)", width: 130 }}>{label}</div>
              {kind === "bool" && (
                <button className="cv-mono text-[11px] px-2 py-0.5 rounded" style={{ background: val ? "#68B98422" : "#E07B5A22", color: val ? "#8FD8A8" : "#F2A183", border: `1px solid ${val ? "#68B98444" : "#E07B5A44"}` }} onClick={() => onEdit(openSection, k, !val)}>{val ? "YES" : "NO"}</button>
              )}
              {kind === "risk" && (
                <select className="cv-mono text-[11px]" style={{ background: "#0B221C", border: "1px solid #26493E", borderRadius: 6, color: val === "Low" ? "#8FD8A8" : val === "Med" ? "#EFCB74" : "#F2A183", padding: "2px 8px" }} value={val} onChange={e => onEdit(openSection, k, e.target.value)}>
                  <option value="Low">Low</option><option value="Med">Med</option><option value="High">High</option>
                </select>
              )}
              {kind === "num" && (<input type="number" className="editfield cv-mono" style={{ width: 80, padding: "3px 8px" }} value={val} onChange={e => onEdit(openSection, k, e.target.value)} />)}
            </div>
          );
        })}
      </div>
    </div>
  );
}
/* ================= DECISION — mode-branched ================= */
function Decision(props) {
  if (props.mode === "full") return <DecisionFull {...props} />;
  if (props.mode === "standard") return <DecisionStandard {...props} />;
  return <DecisionQuick {...props} />;
}

/* ------------------- QUICK ------------------- */
function DecisionQuick({ suppliers, quickWeights, setQuickWeights, onEvidence, onBack, baseCcy, recommendation, setRecommendation }) {
  const ranking = useMemo(() => rankQuick(suppliers, quickWeights, baseCcy), [suppliers, quickWeights, baseCcy]);
  const winner = ranking[0];
  const bestUnitBase = Math.min(...suppliers.map(s => convert(s.fields.unit_price.value, s.currency, baseCcy)));
  const bestUnitSupplier = suppliers.find(s => convert(s.fields.unit_price.value, s.currency, baseCcy) === bestUnitBase);
  const benchmarkBase = convert(TWIN.lastAnalysis.bestUnitEUR, "EUR", baseCcy);
  const drift = ((bestUnitBase - benchmarkBase) / benchmarkBase) * 100;
  const maxTotal = Math.max(...ranking.map(r => r.tco.total));
  const foreign = [...new Set(suppliers.map(s => s.currency))].filter(c => c !== baseCcy);

  return (
    <div className="cardin pb-10">
      <DecisionHeader base={baseCcy} foreign={foreign} onBack={onBack} onDownload={() => downloadWord({ mode: "quick", ranking, weights: quickWeights, base: baseCcy, benchmarkBase, bestUnitBase, drift, recommendation })} />
      <TwinBanner ranking={ranking} base={baseCcy} bestUnitBase={bestUnitBase} bestUnitSupplier={bestUnitSupplier} benchmarkBase={benchmarkBase} drift={drift} onEvidence={onEvidence} onApplyWeights={() => setQuickWeights({ ...TWIN.usualWeights })} />

      <div className="grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-5">
        <div className="cvcard p-5 h-fit lg:sticky lg:top-24">
          <div className="cv-mono text-[10px] mb-4" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>WEIGHT MIXER — MOVE ME</div>
          {[["price", "Total cost (TCO)"], ["lead", "Lead time"], ["warranty", "Warranty"], ["payment", "Payment terms"]].map(([k, label]) => (
            <div key={k} className="mb-4">
              <div className="flex justify-between text-[12px] mb-1.5">
                <span style={{ color: "var(--sage)" }}>{label}</span>
                <span className="cv-mono" style={{ color: "var(--copper-hot)" }}>{quickWeights[k]}</span>
              </div>
              <input type="range" min="0" max="60" value={quickWeights[k]} style={{ "--fill": `${(quickWeights[k] / 60) * 100}%` }} onChange={e => setQuickWeights(w => ({ ...w, [k]: +e.target.value }))} />
            </div>
          ))}
          <div className="text-[10.5px] mt-2" style={{ color: "#4E7264", lineHeight: 1.6 }}>Ranking recomputes deterministically as you drag. The Twin remembers your final mix.</div>
        </div>

        <div className="space-y-4">
          {ranking.map((r, i) => <QuickCard key={r.s.id} r={r} rank={i} maxTotal={maxTotal} onEvidence={onEvidence} base={baseCcy} />)}
          <div className="text-[11px] pt-1" style={{ color: "#4E7264" }}>Financing = advance % × goods+freight × 8% cost of cash × lead-time. Skonto discounts applied where quoted. Every assumption visible; every figure clickable.</div>
        </div>
      </div>

      <DecisionTable ranking={ranking} base={baseCcy} mode="quick" />
      <NegotiationCoach ranking={ranking} base={baseCcy} benchmarkBase={benchmarkBase} onEvidence={onEvidence} />
      <RecommendationSummary mode="quick" ranking={ranking} base={baseCcy} benchmarkBase={benchmarkBase} bestUnitBase={bestUnitBase} drift={drift}
        weights={quickWeights} preset={null} fullState={null} suppliers={suppliers}
        recommendation={recommendation} setRecommendation={setRecommendation} />
    </div>
  );
}

/* ------------------- STANDARD ------------------- */
function DecisionStandard({ suppliers, stdWeights, setStdWeights, preset, changePreset, onEvidence, onBack, baseCcy, recommendation, setRecommendation }) {
  const ranking = useMemo(() => rankStandard(suppliers, stdWeights, baseCcy), [suppliers, stdWeights, baseCcy]);
  const winner = ranking[0];
  const bestUnitBase = Math.min(...suppliers.map(s => convert(s.fields.unit_price.value, s.currency, baseCcy)));
  const bestUnitSupplier = suppliers.find(s => convert(s.fields.unit_price.value, s.currency, baseCcy) === bestUnitBase);
  const benchmarkBase = convert(TWIN.lastAnalysis.bestUnitEUR, "EUR", baseCcy);
  const drift = ((bestUnitBase - benchmarkBase) / benchmarkBase) * 100;
  const foreign = [...new Set(suppliers.map(s => s.currency))].filter(c => c !== baseCcy);

  return (
    <div className="cardin pb-10">
      <DecisionHeader base={baseCcy} foreign={foreign} onBack={onBack} onDownload={() => downloadWord({ mode: "standard", ranking, weights: stdWeights, preset, base: baseCcy, benchmarkBase, bestUnitBase, drift, recommendation })} />
      <TwinBanner ranking={ranking} base={baseCcy} bestUnitBase={bestUnitBase} bestUnitSupplier={bestUnitSupplier} benchmarkBase={benchmarkBase} drift={drift} onEvidence={onEvidence} onApplyWeights={() => { changePreset(TWIN.usualPreset); }} usualLabel="Apply my usual category preset" />

      <div className="grid grid-cols-1 lg:grid-cols-[340px_1fr] gap-5">
        {/* MCDA Weights */}
        <div className="cvcard p-5 h-fit lg:sticky lg:top-24">
          <div className="flex items-center justify-between mb-3">
            <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>WEIGHTED CRITERIA · HIGHER = BETTER</div>
            <div className="cv-mono text-[10px]" style={{ color: "var(--copper)" }}>SUM {stdWeights.price + stdWeights.technical + stdWeights.capability + stdWeights.delivery + stdWeights.risk + stdWeights.innovation}%</div>
          </div>
          <div className="mb-3">
            <div className="text-[10.5px] mb-1.5" style={{ color: "var(--sage)" }}>Category preset</div>
            <div className="grid grid-cols-2 gap-1.5">
              {Object.entries(PRESETS).map(([k, p]) => (
                <button key={k} className={`text-[10.5px] cv-mono px-2 py-1.5 rounded ${preset === k ? "" : "btn-ghost"}`}
                  style={preset === k ? { background: "#D97A3D22", color: "var(--copper-hot)", border: "1px solid #D97A3D66" } : {}}
                  onClick={() => changePreset(k)}>{p.name}</button>
              ))}
            </div>
          </div>
          {[
            ["price", "Price / cost"], ["technical", "Technical quality"], ["capability", "Supplier capability"],
            ["delivery", "Delivery"], ["risk", "Safety (low risk = high score)"], ["innovation", "Innovation & value"],
          ].map(([k, label]) => (
            <div key={k} className="mb-3">
              <div className="flex justify-between text-[12px] mb-1">
                <span style={{ color: "var(--sage)" }}>{label}</span>
                <span className="cv-mono" style={{ color: "var(--copper-hot)" }}>{stdWeights[k]}%</span>
              </div>
              <input type="range" min="0" max="60" value={stdWeights[k]} style={{ "--fill": `${(stdWeights[k] / 60) * 100}%` }} onChange={e => setStdWeights(w => ({ ...w, [k]: +e.target.value }))} />
            </div>
          ))}
          <div className="text-[10.5px] mt-2" style={{ color: "#4E7264", lineHeight: 1.55 }}>Aligned to procurement MCDA practice. Presets are starting points — tune to your mandate. Sum needn't be 100; scores normalise.</div>
        </div>

        {/* Standard supplier cards */}
        <div className="space-y-4">
          {ranking.map((r, i) => <StandardCard key={r.s.id} r={r} rank={i} onEvidence={onEvidence} base={baseCcy} weights={stdWeights} />)}
        </div>
      </div>

      <DecisionTable ranking={ranking} base={baseCcy} mode="standard" />
      <NegotiationCoach ranking={ranking} base={baseCcy} benchmarkBase={benchmarkBase} onEvidence={onEvidence} />
      <ExecSummary ranking={ranking} base={baseCcy} />
      <RecommendationSummary mode="standard" ranking={ranking} base={baseCcy} benchmarkBase={benchmarkBase} bestUnitBase={bestUnitBase} drift={drift}
        weights={stdWeights} preset={preset} fullState={null} suppliers={suppliers}
        recommendation={recommendation} setRecommendation={setRecommendation} />
    </div>
  );
}

/* ---------- Shared decision header ---------- */
function DecisionHeader({ base, foreign, onBack, onDownload }) {
  return (
    <div className="flex flex-wrap items-end justify-between gap-4 mb-5">
      <div>
        <div className="cv-mono text-[11px] mb-1" style={{ color: "var(--copper)", letterSpacing: ".14em" }}>DECISION · ALL FIGURES IN {base} · SCORES 0–100 · HIGHER = BETTER · FX {FX_DATE}</div>
        <div className="cv-serif" style={{ fontSize: 30, color: "#F0EFE6" }}>Ranked. Explained. Traceable.</div>
        {foreign.length > 0 && (
          <div className="text-[11px] cv-mono mt-1.5 flex flex-wrap gap-2">
            {foreign.map(c => <span key={c} className="chip md">{c} → {base} @ {crossRate(c, base).toFixed(4)}</span>)}
          </div>
        )}
      </div>
      <div className="flex gap-2">
        <button className="btn-ghost px-4 py-2.5 text-xs" onClick={onBack}>← Back to review</button>
        <button className="btn-copper px-5 py-2.5 text-xs" onClick={onDownload}>↓ Download Word report</button>
      </div>
    </div>
  );
}

function TwinBanner({ ranking, base, bestUnitBase, bestUnitSupplier, benchmarkBase, drift, onEvidence, onApplyWeights, usualLabel = "Apply my usual weights" }) {
  return (
    <div className="rounded-xl px-5 py-4 mb-6 flex flex-wrap items-center gap-x-6 gap-y-2" style={{ background: "linear-gradient(90deg,#B25E2A22,#B25E2A08)", border: "1px solid #B25E2A55" }}>
      <div className="cv-mono text-[10px]" style={{ color: "var(--copper-hot)", letterSpacing: ".16em" }}>PROCUREMENT TWIN · ANALYSIS #2</div>
      <div className="text-[13px]" style={{ color: "#F2E6DA" }}>
        Best unit price today is <b className="cv-mono traced" onClick={() => onEvidence({ supplierId: bestUnitSupplier.id, fieldKey: "unit_price" })}>{fmtMoney(bestUnitBase, base)}</b> —
        {" "}<b style={{ color: drift > 0 ? "var(--warn)" : "var(--ok)" }}>{Math.abs(drift).toFixed(1)}% {drift > 0 ? "above" : "below"}</b> your {TWIN.lastAnalysis.label} ({fmtMoney(benchmarkBase, base)}/unit).
        Last time you chose the {TWIN.lastAnalysis.chosen} because {TWIN.lastAnalysis.reason}.
      </div>
      <button className="btn-ghost px-3 py-1.5 text-[11px] ml-auto" onClick={onApplyWeights}>{usualLabel}</button>
    </div>
  );
}

/* ---------- QUICK CARD ---------- */
const SEGMENTS = [
  ["goods", "Goods", "#7BA893"], ["freight", "Freight", "#5B8F7E"], ["install", "Installation", "#E8935C"],
  ["training", "Training", "#C9A66B"], ["maint3", "Maintenance ×3yr", "#4E7264"], ["financing", "Financing", "#E3B23C"],
];
function QuickCard({ r, rank, maxTotal, onEvidence, base }) {
  const { s, tco, score } = r;
  const ev = key => onEvidence({ supplierId: s.id, fieldKey: key });
  return (
    <div className={`cvcard p-5 cardin ${rank === 0 ? "winner" : ""}`} style={{ animationDelay: `${rank * 80}ms` }}>
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div className="flex items-center gap-3">
          <div className="cv-serif" title={rank === 0 ? "Rank 1 · highest weighted score = best overall" : `Rank ${rank + 1}`} style={{ fontSize: 30, color: rank === 0 ? "var(--copper-hot)" : "#2E5347", width: 44, cursor: "help" }}>{String(rank + 1).padStart(2, "0")}</div>
          <div>
            <div className="font-semibold flex items-center gap-2" style={{ color: "#EDF3EE", fontSize: 16 }}>{s.name}{rank === 0 && <span className="chip verified cv-mono">RECOMMENDED</span>}</div>
            <div className="text-[11.5px] cv-mono" style={{ color: "var(--moss)" }}>{s.country} · quoted {s.currency}{s.currency !== base && <span style={{color:"var(--warn)"}}> → {base} @ {crossRate(s.currency, base).toFixed(4)}</span>} · <span className="traced" onClick={() => ev("incoterm")}>{s.fields.incoterm.value}</span> · <span className="traced" onClick={() => ev("payment_terms")}>{s.fields.payment_terms.value}</span></div>
          </div>
        </div>
        <div className="text-right">
          <div className="cv-mono" title="3-year Total Cost of Ownership — lower cost is better" style={{ fontSize: 24, fontWeight: 600, color: rank === 0 ? "var(--copper-hot)" : "#DCE8E1", cursor: "help" }}>{fmtMoney(tco.total, base)}</div>
          <div className="text-[10.5px] cv-mono" title="Weighted score: higher = better, max 100" style={{ color: "var(--moss)", cursor: "help" }}>3-YEAR TCO · SCORE {Math.round(score * 100)} / 100</div>
        </div>
      </div>
      <div className="mt-4">
        <div className="flex h-6 rounded-md overflow-hidden" style={{ width: `${(tco.total / maxTotal) * 100}%`, minWidth: "40%", transition: "width .5s cubic-bezier(.2,.9,.3,1)" }}>
          {SEGMENTS.map(([k, label, color]) => tco[k] > 0 && (<div key={k} title={`${label}: ${fmtMoney(tco[k], base)}`} style={{ width: `${(tco[k] / tco.total) * 100}%`, background: color, cursor: "help" }} />))}
        </div>
        {tco.discount < 0 && <div className="text-[11px] cv-mono mt-1" style={{ color: "var(--ok)" }}>− {fmtMoney(-tco.discount, base)} early-payment skonto applied <span className="traced" onClick={() => ev("payment_terms")}>source</span></div>}
      </div>
      <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
        <Stat label="Unit price" onClick={() => ev("unit_price")} value={fmtMoney(convert(s.fields.unit_price.value, s.currency, base), base)} />
        <Stat label="Lead time" onClick={() => ev("lead_time_days")} value={`${s.fields.lead_time_days.value} days`} />
        <Stat label="Warranty" onClick={() => ev("warranty_months")} value={`${s.fields.warranty_months.value} months`} />
        <Stat label="Maintenance ×3yr" onClick={() => ev("maintenance_annual")} value={fmtMoney(tco.maint3, base)} />
      </div>
      <div className="mt-4 text-[13px] rounded-lg px-4 py-3" style={{ background: "#0B221C", border: "1px solid #1C4034", color: "var(--sage)", lineHeight: 1.65 }}>
        <span className="cv-mono text-[10px] mr-2" style={{ color: "var(--copper)" }}>WHY</span>{whyTextQuick(r, rank, base)}
      </div>
    </div>
  );
}

/* ---------- STANDARD CARD ---------- */
function StandardCard({ r, rank, onEvidence, base }) {
  const { s, tco, score, mcda, esgScore } = r;
  const ev = key => onEvidence({ supplierId: s.id, fieldKey: key });
  const criteria = [
    ["Price", mcda.price, "Cost / TCO score — higher = cheaper relative to the group"],
    ["Technical", mcda.technical, "Technical & quality score — higher = better"],
    ["Capability", mcda.capability, "Supplier capability score — higher = stronger references, capacity, financials"],
    ["Delivery", mcda.delivery, "Delivery reliability score — higher = better on-time history & shorter lead time"],
    ["Safety", mcda.risk, "Risk profile — higher = SAFER (lower actual risk)"],
    ["Innovation", mcda.innovation, "Innovation & value score — higher = stronger warranty, after-sales, low defect rate"],
  ];
  return (
    <div className={`cvcard p-5 cardin ${rank === 0 ? "winner" : ""}`} style={{ animationDelay: `${rank * 80}ms` }}>
      <div className="flex flex-wrap items-start justify-between gap-3 mb-4">
        <div className="flex items-center gap-3">
          <div className="cv-serif" title={rank === 0 ? "Rank 1 · highest weighted score = best overall" : `Rank ${rank + 1}`} style={{ fontSize: 30, color: rank === 0 ? "var(--copper-hot)" : "#2E5347", width: 44, cursor: "help" }}>{String(rank + 1).padStart(2, "0")}</div>
          <div>
            <div className="font-semibold flex items-center gap-2" style={{ color: "#EDF3EE", fontSize: 16 }}>{s.name}{rank === 0 && <span className="chip verified cv-mono">RECOMMENDED</span>}</div>
            <div className="text-[11.5px] cv-mono" style={{ color: "var(--moss)" }}>{s.country} · quoted {s.currency}{s.currency !== base && ` → ${base}`} · TCO {fmtMoney(tco.total, base)}</div>
          </div>
        </div>
        <div className="text-right">
          <div className="cv-mono" title="Weighted total across all criteria · higher = better · max 100" style={{ fontSize: 30, fontWeight: 600, color: rank === 0 ? "var(--copper-hot)" : "#DCE8E1", cursor: "help" }}>{Math.round(score * 100)}</div>
          <div className="text-[10.5px] cv-mono" style={{ color: "var(--moss)" }}>WEIGHTED SCORE / 100</div>
        </div>
      </div>

      {/* Persistent scoring legend */}
      <div className="text-[10.5px] mb-2 flex items-center gap-2" style={{ color: "#4E7264" }}>
        <span className="cv-mono" style={{ letterSpacing: ".08em" }}>SCORES 0–100 · HIGHER = BETTER</span>
        <span style={{ opacity: .5 }}>·</span>
        <span>hover any label for what it means</span>
      </div>

      {/* Six-criterion bars */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-1.5">
        {criteria.map(([label, v, hint]) => (
          <div key={label} title={hint}>
            <div className="flex justify-between text-[11px] mb-0.5">
              <span style={{ color: "var(--sage)", cursor: "help", borderBottom: "1px dotted #3E5F53" }}>{label}</span>
              <span className="cv-mono" style={{ color: v >= 70 ? "var(--ok)" : v >= 50 ? "var(--warn)" : "var(--low)" }}>{Math.round(v)}</span>
            </div>
            <div className="scoregauge"><div className="scorefill" style={{ width: `${Math.max(0, Math.min(100, v))}%` }} /></div>
          </div>
        ))}
      </div>

      <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
        <Stat label="Unit price" onClick={() => ev("unit_price")} value={fmtMoney(convert(s.fields.unit_price.value, s.currency, base), base)} />
        <Stat label="Lead time" onClick={() => ev("lead_time_days")} value={`${s.fields.lead_time_days.value} days`} />
        <Stat label="Warranty" onClick={() => ev("warranty_months")} value={`${s.fields.warranty_months.value} months`} />
        <Stat label="ESG score" value={`${Math.round(esgScore)} / 100`} onClick={() => ev("payment_terms")} />
      </div>

      <div className="mt-4 text-[13px] rounded-lg px-4 py-3" style={{ background: "#0B221C", border: "1px solid #1C4034", color: "var(--sage)", lineHeight: 1.65 }}>
        <span className="cv-mono text-[10px] mr-2" style={{ color: "var(--copper)" }}>WHY</span>{whyTextStandard(r, rank, base)}
      </div>
    </div>
  );
}

function Stat({ label, value, onClick }) {
  return (
    <div>
      <div className="text-[10px] cv-mono" style={{ color: "#4E7264", letterSpacing: ".1em" }}>{label.toUpperCase()}</div>
      <div className="cv-mono text-[14px] traced inline-block mt-0.5" style={{ color: "#E8EFEA" }} onClick={onClick}>{value}</div>
    </div>
  );
}

function whyTextQuick(r, rank, base) {
  const { s, tco, parts } = r; const bits = [];
  if (parts.nPrice > 0.95) bits.push(`lowest 3-year TCO at ${fmtMoney(tco.total, base)}`);
  else bits.push(`3-year TCO of ${fmtMoney(tco.total, base)}${tco.financing > 0 ? ` incl. ${fmtMoney(tco.financing, base)} financing cost` : ""}`);
  if (parts.nLead === 1) bits.push(`fastest delivery (${s.fields.lead_time_days.value} days)`);
  else if (parts.nLead < 0.3) bits.push(`slowest delivery (${s.fields.lead_time_days.value} days) — a real project risk`);
  if (parts.nWar === 1) bits.push(`strongest warranty (${s.fields.warranty_months.value} months)`);
  else if (parts.nWar <= 0.4) bits.push(`weakest warranty (${s.fields.warranty_months.value} months, return-to-factory)`);
  if (s.fields.payment_terms.advance > 0) bits.push(`${Math.round(s.fields.payment_terms.advance*100)}% advance ties up cash`);
  if (s.fields.payment_terms.earlyDiscount) bits.push(`2% skonto rewards fast payment`);
  return (rank === 0 ? "Ranks first on your current weights: " : rank === 1 ? "Close second: " : "Trails: ") + bits.join("; ") + ".";
}
function whyTextStandard(r, rank, base) {
  const { s, mcda } = r;
  const strong = Object.entries(mcda).filter(([k, v]) => v >= 75).map(([k]) => k);
  const weak = Object.entries(mcda).filter(([k, v]) => v < 55).map(([k]) => k);
  const bits = [];
  if (strong.length) bits.push(`strong on ${strong.join(", ")}`);
  if (weak.length) bits.push(`weaker on ${weak.join(", ")}`);
  if (s.fields.payment_terms.advance > 0) bits.push(`${Math.round(s.fields.payment_terms.advance*100)}% advance is a cash-flow risk`);
  return (rank === 0 ? "Leads on the weighted multi-criteria total: " : `Ranks #${rank+1}: `) + bits.join("; ") + ".";
}

/* ================= DECISION TABLE ================= */
function decisionRowsQuick(ranking, base) {
  const m = v => fmtMoney(v, base);
  return [
    ["Rank", r => r._rank === 0 ? "1 — RECOMMENDED" : String(r._rank + 1)],
    ["Weighted score", r => String(Math.round(r.score * 100)) + " / 100"],
    ["3-year TCO", r => m(r.tco.total), true],
    ["Unit price", r => m(convert(r.s.fields.unit_price.value, r.s.currency, base))],
    ["Quantity", r => String(r.s.fields.quantity.value)],
    ["Goods subtotal", r => m(r.tco.goods)],
    ["Freight", r => r.tco.freight > 0 ? m(r.tco.freight) : "Included"],
    ["Installation", r => r.tco.install > 0 ? m(r.tco.install) + (r.s.fields.installation.note ? " (est.)" : "") : "Included"],
    ["Maintenance ×3 yr", r => m(r.tco.maint3)],
    ["Financing cost", r => r.tco.financing > 0 ? m(r.tco.financing) : "—"],
    ["Early-pay discount", r => r.tco.discount < 0 ? "−" + m(-r.tco.discount) : "—"],
    ["Lead time", r => r.s.fields.lead_time_days.value + " days"],
    ["Warranty", r => r.s.fields.warranty_months.value + " months"],
    ["Payment terms", r => String(r.s.fields.payment_terms.value)],
    ["Incoterm", r => String(r.s.fields.incoterm.value)],
    ["Quoted in", r => r.s.currency + (r.s.currency !== base ? " → " + base + " @ " + crossRate(r.s.currency, base).toFixed(4) : "")],
  ];
}
function decisionRowsStandard(ranking, base) {
  const m = v => fmtMoney(v, base);
  return [
    ["Rank", r => r._rank === 0 ? "1 — RECOMMENDED" : String(r._rank + 1)],
    ["Weighted MCDA score", r => Math.round(r.score * 100) + " / 100", true],
    ["Price / Cost score", r => Math.round(r.mcda.price) + " / 100"],
    ["Technical score", r => Math.round(r.mcda.technical) + " / 100"],
    ["Capability score", r => Math.round(r.mcda.capability) + " / 100"],
    ["Delivery score", r => Math.round(r.mcda.delivery) + " / 100"],
    ["Risk (inverse) score", r => Math.round(r.mcda.risk) + " / 100"],
    ["Innovation score", r => Math.round(r.mcda.innovation) + " / 100"],
    ["ESG score", r => Math.round(r.esgScore) + " / 100"],
    ["3-year TCO", r => m(r.tco.total), true],
    ["Unit price", r => m(convert(r.s.fields.unit_price.value, r.s.currency, base))],
    ["Lead time", r => r.s.fields.lead_time_days.value + " days"],
    ["Warranty", r => r.s.fields.warranty_months.value + " months"],
    ["Payment terms", r => String(r.s.fields.payment_terms.value)],
    ["Incoterm", r => String(r.s.fields.incoterm.value)],
    ["Overall risk profile", r => `Fin ${r.s.scores.risk.financial} · Ops ${r.s.scores.risk.operational} · Comp ${r.s.scores.risk.compliance} · Geo ${r.s.scores.risk.geo}`],
    ["Compliance", r => Object.entries(r.s.scores.compliance).filter(([_,v])=>v).length + " / 4 checks pass"],
    ["Quoted in", r => r.s.currency + (r.s.currency !== base ? " → " + base + " @ " + crossRate(r.s.currency, base).toFixed(4) : "")],
  ];
}

function DecisionTable({ ranking, base, mode }) {
  const ranked = ranking.map((r, i) => ({ ...r, _rank: i }));
  const rows = mode === "standard" ? decisionRowsStandard(ranking, base) : decisionRowsQuick(ranking, base);
  return (
    <div className="cvcard p-5 mt-6 cv-scroll" style={{ overflowX: "auto" }}>
      <div className="mb-3">
        <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>FINAL VENDOR COMPARISON · {mode === "standard" ? "MCDA" : "QUICK"}</div>
        <div className="cv-serif" style={{ fontSize: 20, color: "#F0EFE6" }}>Decision table — all figures in {base}</div>
      </div>
      <table style={{ borderCollapse: "collapse", width: "100%", minWidth: 640 }}>
        <thead>
          <tr>
            <th style={{ textAlign: "left", padding: "8px 10px", borderBottom: "1px solid #2E5347", color: "var(--sage)", fontSize: 11, fontWeight: 600, letterSpacing: ".06em" }}>CRITERION</th>
            {ranked.map((r, i) => (
              <th key={r.s.id} style={{ textAlign: "left", padding: "8px 10px", borderBottom: i === 0 ? "2px solid var(--copper)" : "1px solid #2E5347", background: i === 0 ? "#D97A3D14" : "transparent" }}>
                <div style={{ color: i === 0 ? "var(--copper-hot)" : "#EDF3EE", fontSize: 13, fontWeight: 700 }}>{r.s.name}</div>
                <div className="cv-mono" style={{ color: "var(--moss)", fontSize: 10 }}>{r.s.country}</div>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map(([label, fn, strong], ri) => (
            <tr key={label} style={{ background: ri % 2 ? "#0C261F66" : "transparent" }}>
              <td style={{ padding: "7px 10px", color: "var(--sage)", fontSize: 12, borderBottom: "1px dashed #1C4034", whiteSpace: "nowrap" }}>{label}</td>
              {ranked.map((r, i) => (
                <td key={r.s.id} className="cv-mono" style={{ padding: "7px 10px", fontSize: 12.5, color: strong ? (i === 0 ? "var(--copper-hot)" : "#EDF3EE") : "#CFE0D7", fontWeight: strong ? 700 : 400, borderBottom: "1px dashed #1C4034", background: i === 0 ? "#D97A3D0D" : "transparent" }}>{fn(r)}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <div className="text-[10.5px] mt-3" style={{ color: "#4E7264" }}>The Word export contains this exact table — fully editable, with the executive summary and audit block attached.</div>
    </div>
  );
}

/* ================= EXEC SUMMARY (Standard only) — Template §12 ================= */
function ExecSummary({ ranking, base }) {
  const top3 = ranking.slice(0, 3);
  return (
    <div className="cvcard p-5 mt-6">
      <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>EXECUTIVE SUMMARY · TOP 3 SUPPLIERS</div>
      <div className="cv-serif mb-4" style={{ fontSize: 20, color: "#F0EFE6" }}>Strengths & weaknesses — for your write-up.</div>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
        {top3.map((r, i) => {
          const sw = summarizeStrengthsWeaknesses(r);
          return (
            <div key={r.s.id} className="rounded-lg p-3.5" style={{ background: "#0B221C", border: `1px solid ${i === 0 ? "#D97A3D66" : "#1C4034"}` }}>
              <div className="flex items-center justify-between">
                <div style={{ color: i === 0 ? "var(--copper-hot)" : "#EDF3EE", fontWeight: 700, fontSize: 14 }}>{i+1}. {r.s.name}</div>
                <div className="cv-mono text-[11px]" style={{ color: "var(--copper-hot)" }}>{Math.round(r.score*100)}/100</div>
              </div>
              <div className="mt-2">
                <div className="cv-mono text-[10px]" style={{ color: "var(--ok)", letterSpacing: ".1em" }}>STRENGTHS</div>
                <ul className="text-[12px] mt-1 pl-4" style={{ color: "var(--sage)", listStyle: "disc" }}>{sw.strengths.map((x,j) => <li key={j}>{x}</li>)}</ul>
              </div>
              <div className="mt-2">
                <div className="cv-mono text-[10px]" style={{ color: "var(--low)", letterSpacing: ".1em" }}>WEAKNESSES</div>
                <ul className="text-[12px] mt-1 pl-4" style={{ color: "var(--sage)", listStyle: "disc" }}>{sw.weaknesses.map((x,j) => <li key={j}>{x}</li>)}</ul>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}
function summarizeStrengthsWeaknesses(r) {
  const strengths = [], weaknesses = [];
  const { mcda, esgScore, s } = r;
  if (mcda.price >= 75) strengths.push("Strong price / TCO position");
  if (mcda.technical >= 75) strengths.push("Strong technical & quality profile");
  if (mcda.capability >= 75) strengths.push("Established capability & references");
  if (mcda.delivery >= 75) strengths.push("Reliable delivery track record");
  if (mcda.risk >= 75) strengths.push("Low overall risk exposure");
  if (esgScore >= 75) strengths.push("Solid ESG credentials");
  if (s.fields.payment_terms.earlyDiscount) strengths.push("Early-payment discount offered");

  if (mcda.price < 55) weaknesses.push("Highest cost in the set");
  if (mcda.technical < 55) weaknesses.push("Technical gaps to address");
  if (mcda.capability < 55) weaknesses.push("Capability concerns — references thin");
  if (mcda.delivery < 55) weaknesses.push("Delivery risk (lead time / history)");
  if (mcda.risk < 55) weaknesses.push("Elevated risk profile");
  if (esgScore < 55) weaknesses.push("ESG posture below peers");
  if ((s.fields.payment_terms.advance || 0) >= 0.5) weaknesses.push(`${Math.round(s.fields.payment_terms.advance*100)}% advance payment required`);
  if (s.fields.warranty_months.value < 18) weaknesses.push(`Short warranty (${s.fields.warranty_months.value} months)`);

  if (!strengths.length) strengths.push("Balanced profile without stand-out advantages");
  if (!weaknesses.length) weaknesses.push("No material weaknesses on current data");
  return { strengths, weaknesses };
}

/* ================= FULL MODE ================= */
/* Standard's scoring foundation + four depth features:
   sensitivity, scenarios, alternative awards, personal due diligence. */

function DecisionFull({ suppliers, setSuppliers, stdWeights, setStdWeights, preset, changePreset,
  fullState, setFullState, recommendation, setRecommendation, onEvidence, onBack, baseCcy }) {
  const ranking = useMemo(() => rankStandard(suppliers, stdWeights, baseCcy), [suppliers, stdWeights, baseCcy]);
  const bestUnitBase = Math.min(...suppliers.map(s => convert(s.fields.unit_price.value, s.currency, baseCcy)));
  const bestUnitSupplier = suppliers.find(s => convert(s.fields.unit_price.value, s.currency, baseCcy) === bestUnitBase);
  const benchmarkBase = convert(TWIN.lastAnalysis.bestUnitEUR, "EUR", baseCcy);
  const drift = ((bestUnitBase - benchmarkBase) / benchmarkBase) * 100;
  const foreign = [...new Set(suppliers.map(s => s.currency))].filter(c => c !== baseCcy);

  return (
    <div className="cardin pb-10">
      <DecisionHeader base={baseCcy} foreign={foreign} onBack={onBack}
        onDownload={() => downloadWord({ mode: "full", ranking, weights: stdWeights, preset, base: baseCcy,
          benchmarkBase, bestUnitBase, drift, suppliers, fullState, recommendation })} />
      <TwinBanner ranking={ranking} base={baseCcy} bestUnitBase={bestUnitBase} bestUnitSupplier={bestUnitSupplier}
        benchmarkBase={benchmarkBase} drift={drift} onEvidence={onEvidence}
        onApplyWeights={() => changePreset(TWIN.usualPreset)} usualLabel="Apply my usual category preset" />

      <div className="grid grid-cols-1 lg:grid-cols-[340px_1fr] gap-5">
        <div className="cvcard p-5 h-fit lg:sticky lg:top-24">
          <div className="flex items-center justify-between mb-3">
            <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>WEIGHTED CRITERIA · HIGHER = BETTER</div>
            <div className="cv-mono text-[10px]" style={{ color: "var(--copper)" }}>SUM {stdWeights.price + stdWeights.technical + stdWeights.capability + stdWeights.delivery + stdWeights.risk + stdWeights.innovation}%</div>
          </div>
          <div className="mb-3">
            <div className="text-[10.5px] mb-1.5" style={{ color: "var(--sage)" }}>Category preset</div>
            <div className="grid grid-cols-2 gap-1.5">
              {Object.entries(PRESETS).map(([k, pr]) => (
                <button key={k} className={`text-[10.5px] cv-mono px-2 py-1.5 rounded ${preset === k ? "" : "btn-ghost"}`}
                  style={preset === k ? { background: "#D97A3D22", color: "var(--copper-hot)", border: "1px solid #D97A3D66" } : {}}
                  onClick={() => changePreset(k)}>{pr.name}</button>
              ))}
            </div>
          </div>
          {[["price", "Price / cost"], ["technical", "Technical quality"], ["capability", "Supplier capability"],
            ["delivery", "Delivery"], ["risk", "Safety (low risk = high score)"], ["innovation", "Innovation & value"]].map(([k, label]) => (
            <div key={k} className="mb-3">
              <div className="flex justify-between text-[12px] mb-1">
                <span style={{ color: "var(--sage)" }}>{label}</span>
                <span className="cv-mono" style={{ color: "var(--copper-hot)" }}>{stdWeights[k]}%</span>
              </div>
              <input type="range" min="0" max="60" value={stdWeights[k]} style={{ "--fill": `${(stdWeights[k] / 60) * 100}%` }} onChange={e => setStdWeights(w => ({ ...w, [k]: +e.target.value }))} />
            </div>
          ))}
          <div className="text-[10.5px] mt-2" style={{ color: "#4E7264", lineHeight: 1.55 }}>Change the weights to see the ranking shift in real time. The sensitivity panel below shows how fragile that ranking is.</div>
        </div>

        <div className="space-y-4">
          {ranking.map((r, i) => <StandardCard key={r.s.id} r={r} rank={i} onEvidence={onEvidence} base={baseCcy} />)}
        </div>
      </div>

      <DecisionTable ranking={ranking} base={baseCcy} mode="standard" />
      <NegotiationCoach ranking={ranking} base={baseCcy} benchmarkBase={benchmarkBase} onEvidence={onEvidence} />

      {/* Full-mode depth sections */}
      <SensitivityPanel suppliers={suppliers} baseWeights={stdWeights} base={baseCcy} currentRanking={ranking} />
      <ScenarioPanel suppliers={suppliers} setSuppliers={setSuppliers} weights={stdWeights} base={baseCcy}
        fullState={fullState} setFullState={setFullState} />
      <AwardStructurePanel ranking={ranking} base={baseCcy} weights={stdWeights} />
      <DueDiligencePanel suppliers={suppliers} setSuppliers={setSuppliers} fullState={fullState} setFullState={setFullState} />
      <ExecSummary ranking={ranking} base={baseCcy} />
      <RecommendationSummary mode="full" ranking={ranking} base={baseCcy} benchmarkBase={benchmarkBase} bestUnitBase={bestUnitBase} drift={drift}
        weights={stdWeights} preset={preset} fullState={fullState} suppliers={suppliers}
        recommendation={recommendation} setRecommendation={setRecommendation} />
    </div>
  );
}

/* ---------- SENSITIVITY (Template §11.1) ---------- */
/* Shows whether the current #1 ranking survives realistic weight shifts.
   Robust ranking = defensible decision. Fragile ranking = a warning. */
function SensitivityPanel({ suppliers, baseWeights, base, currentRanking }) {
  const winner = currentRanking[0];
  const runner = currentRanking[1];
  // Test 5 what-if scenarios by perturbing weights
  const tests = useMemo(() => {
    const perturb = (delta) => Object.fromEntries(Object.entries(baseWeights).map(([k, v]) => [k, Math.max(0, v + (delta[k] || 0))]));
    const scenarios = [
      { label: "Price weight +15", w: perturb({ price: 15 }) },
      { label: "Price weight −15", w: perturb({ price: -15 }) },
      { label: "Risk weight ×2", w: { ...baseWeights, risk: baseWeights.risk * 2 } },
      { label: "Technical weight +10", w: perturb({ technical: 10 }) },
      { label: "Capability weight +10", w: perturb({ capability: 10 }) },
    ];
    return scenarios.map(sc => {
      const r = rankStandard(suppliers, sc.w, base);
      const holds = r[0].s.id === winner.s.id;
      const margin = holds ? Math.round((r[0].score - r[1].score) * 100) : Math.round((r[0].score - (r.find(x => x.s.id === winner.s.id)?.score || 0)) * 100);
      return { label: sc.label, winner: r[0].s.name, holds, margin };
    });
  }, [suppliers, baseWeights, base, winner]);
  const holdsCount = tests.filter(t => t.holds).length;
  const currentMargin = Math.round((winner.score - (runner?.score || 0)) * 100);
  const robust = holdsCount >= 4 && currentMargin >= 5;

  return (
    <div className="cvcard p-5 mt-6">
      <div className="flex flex-wrap items-end justify-between gap-3 mb-3">
        <div>
          <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>SENSITIVITY · IS YOUR DECISION FRAGILE?</div>
          <div className="cv-serif" style={{ fontSize: 20, color: "#F0EFE6" }}>How would the ranking change if the weights did?</div>
        </div>
        <div className="text-right">
          <div className="cv-mono" style={{ fontSize: 22, fontWeight: 600, color: robust ? "var(--ok)" : "var(--warn)" }}>
            {robust ? "ROBUST" : holdsCount >= 3 ? "STABLE" : "FRAGILE"}
          </div>
          <div className="text-[10.5px] cv-mono" style={{ color: "var(--moss)" }}>{winner.s.name} holds in {holdsCount}/{tests.length} scenarios · current margin {currentMargin} pts</div>
        </div>
      </div>
      <div className="text-[12px] mb-3" style={{ color: "var(--sage)" }}>
        If small changes to your weights flip the winner, your decision is defensible only under your specific mix. Robust rankings survive reasonable perturbation — the "so what if I'd weighted it slightly differently" question, answered mathematically.
      </div>
      <div className="space-y-1.5">
        {tests.map((t, i) => (
          <div key={i} className="flex items-center gap-3 py-1.5 px-3 rounded" style={{ background: "#0B221C", border: "1px solid #1C4034" }}>
            <div className="cv-mono text-[11.5px]" style={{ color: "var(--sage)", width: 200 }}>{t.label}</div>
            <div className="flex-1 text-[12px]">
              {t.holds ? (
                <span style={{ color: "var(--ok)" }}>✓ {winner.s.name} still #1</span>
              ) : (
                <span style={{ color: "var(--warn)" }}>⚑ Winner flips to <b>{t.winner}</b></span>
              )}
            </div>
            <div className="cv-mono text-[11px]" style={{ color: t.holds ? "var(--moss)" : "var(--copper-hot)" }}>{t.holds ? "+" : ""}{t.margin} pts</div>
          </div>
        ))}
      </div>
      <div className="text-[10.5px] mt-3" style={{ color: "#4E7264" }}>
        {!robust && "A fragile ranking is not wrong — it means the decision is close and depends heavily on your weighting. Document your reasoning; the exec summary and Word memo capture it."}
        {robust && "A robust ranking is your strongest defence: the winner is not an artefact of one weighting."}
      </div>
    </div>
  );
}

/* ---------- SCENARIO MODELING ---------- */
/* "What if the winner concedes X%? What if FX moves Y%? What if lead time drops Z%?"
   Editable per-supplier deltas that re-run the deterministic engine. */
function ScenarioPanel({ suppliers, weights, base, fullState, setFullState }) {
  const scenarios = fullState.scenarios || {};
  const winner = rankStandard(suppliers, weights, base)[0];
  const currentDelta = scenarios[winner.s.id] || { pricePct: 0, leadPct: 0, warrantyDelta: 0 };

  // Apply the delta to a shallow copy of suppliers
  const modifiedSuppliers = useMemo(() => suppliers.map(s => {
    const d = scenarios[s.id] || { pricePct: 0, leadPct: 0, warrantyDelta: 0 };
    if (!d.pricePct && !d.leadPct && !d.warrantyDelta) return s;
    return {
      ...s,
      fields: {
        ...s.fields,
        unit_price: { ...s.fields.unit_price, value: s.fields.unit_price.value * (1 + d.pricePct / 100) },
        lead_time_days: { ...s.fields.lead_time_days, value: Math.max(1, Math.round(s.fields.lead_time_days.value * (1 + d.leadPct / 100))) },
        warranty_months: { ...s.fields.warranty_months, value: Math.max(0, s.fields.warranty_months.value + d.warrantyDelta) },
      }
    };
  }), [suppliers, scenarios]);

  const modifiedRanking = useMemo(() => rankStandard(modifiedSuppliers, weights, base), [modifiedSuppliers, weights, base]);
  const originalRanking = useMemo(() => rankStandard(suppliers, weights, base), [suppliers, weights, base]);

  const setDelta = (sid, key, val) => setFullState(fs => ({
    ...fs, scenarios: { ...fs.scenarios, [sid]: { ...(fs.scenarios?.[sid] || { pricePct: 0, leadPct: 0, warrantyDelta: 0 }), [key]: val } }
  }));
  const reset = () => setFullState(fs => ({ ...fs, scenarios: {} }));
  const dirty = Object.values(scenarios).some(d => d && (d.pricePct || d.leadPct || d.warrantyDelta));

  return (
    <div className="cvcard p-5 mt-6">
      <div className="flex flex-wrap items-end justify-between gap-3 mb-2">
        <div>
          <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>SCENARIO MODELLING · WHAT-IF</div>
          <div className="cv-serif" style={{ fontSize: 20, color: "#F0EFE6" }}>Model concessions, delays, and FX shifts.</div>
        </div>
        {dirty && <button className="btn-ghost px-3 py-1.5 text-[11px]" onClick={reset}>Reset all deltas</button>}
      </div>
      <div className="text-[12px] mb-4" style={{ color: "var(--sage)" }}>
        Perturb each supplier's terms and watch the ranking recompute. Use it to test what you're walking into: <i>"if I get {winner.s.name} down 5%, does anything else change?"</i>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-4">
        {suppliers.map(s => {
          const d = scenarios[s.id] || { pricePct: 0, leadPct: 0, warrantyDelta: 0 };
          return (
            <div key={s.id} className="rounded-lg p-3" style={{ background: "#0B221C", border: "1px solid #1C4034" }}>
              <div className="text-[13px] font-semibold mb-2" style={{ color: "#EDF3EE" }}>{s.name}</div>
              <ScenarioSlider label="Unit price" val={d.pricePct} unit="%" min={-25} max={25} onChange={v => setDelta(s.id, "pricePct", v)} />
              <ScenarioSlider label="Lead time" val={d.leadPct} unit="%" min={-50} max={50} onChange={v => setDelta(s.id, "leadPct", v)} />
              <ScenarioSlider label="Warranty" val={d.warrantyDelta} unit="mo" min={-12} max={24} onChange={v => setDelta(s.id, "warrantyDelta", v)} />
            </div>
          );
        })}
      </div>

      {dirty && (
        <div className="rounded-lg p-3" style={{ background: "#B25E2A11", border: "1px dashed #B25E2A55" }}>
          <div className="cv-mono text-[10px] mb-2" style={{ color: "var(--copper-hot)", letterSpacing: ".14em" }}>MODELLED OUTCOME · WITH YOUR CHANGES</div>
          <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
            {modifiedRanking.map((r, i) => {
              const orig = originalRanking.find(x => x.s.id === r.s.id);
              const rankShift = originalRanking.findIndex(x => x.s.id === r.s.id) - i;
              const scoreShift = Math.round((r.score - (orig?.score || 0)) * 100);
              return (
                <div key={r.s.id} className="text-[12px]" style={{ color: "var(--sage)" }}>
                  <span className="cv-mono" style={{ color: i === 0 ? "var(--copper-hot)" : "#EDF3EE", fontWeight: 700 }}>#{i + 1} {r.s.name}</span>
                  <span className="cv-mono ml-2" style={{ color: "var(--moss)" }}>{Math.round(r.score * 100)} pts</span>
                  {rankShift !== 0 && <span className="cv-mono ml-2" style={{ color: rankShift > 0 ? "var(--ok)" : "var(--warn)" }}>{rankShift > 0 ? "↑" : "↓"}{Math.abs(rankShift)}</span>}
                  {scoreShift !== 0 && <span className="cv-mono ml-2 text-[11px]" style={{ color: scoreShift > 0 ? "var(--ok)" : "var(--warn)" }}>({scoreShift > 0 ? "+" : ""}{scoreShift})</span>}
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}
function ScenarioSlider({ label, val, unit, min, max, onChange }) {
  return (
    <div className="mb-2">
      <div className="flex justify-between text-[11px] mb-0.5">
        <span style={{ color: "var(--sage)" }}>{label}</span>
        <span className="cv-mono" style={{ color: val === 0 ? "var(--moss)" : val > 0 ? "var(--ok)" : "var(--warn)" }}>{val > 0 ? "+" : ""}{val}{unit}</span>
      </div>
      <input type="range" min={min} max={max} value={val} step={unit === "mo" ? 1 : 1} style={{ "--fill": `${((val - min) / (max - min)) * 100}%` }} onChange={e => onChange(+e.target.value)} />
    </div>
  );
}

/* ---------- ALTERNATIVE AWARD STRUCTURES (Template §11.2) ---------- */
function AwardStructurePanel({ ranking, base, weights }) {
  const top2 = ranking.slice(0, 2);
  const top3 = ranking.slice(0, 3);
  const primary = ranking[0], backup = ranking[1];
  // Split award: 60% top / 40% second — averaged TCO with risk-diversification bonus
  const splitTCO = 0.6 * ranking[0].tco.total + 0.4 * ranking[1].tco.total;
  const singleTCO = ranking[0].tco.total;
  const frameTCO = ranking.slice(0, 3).reduce((s, r) => s + r.tco.total, 0) / 3;
  const structures = [
    { name: "Single-source award", desc: `${primary.s.name} takes 100%.`, tco: singleTCO, risk: "Med", pros: "Simplest to manage; strongest volume leverage; one relationship to invest in.", cons: `Full exposure to ${primary.s.name}'s delivery, capacity, or geopolitical risk. No fallback.` },
    { name: "Split award (60/40)", desc: `${top2[0].s.name} 60% · ${top2[1].s.name} 40%.`, tco: splitTCO, risk: "Low", pros: "Diversifies delivery risk. Creates ongoing competitive tension. Live benchmark for the next tender.", cons: `~${Math.round(((splitTCO / singleTCO) - 1) * 100)}% higher weighted TCO. More admin overhead; smaller volume discounts.` },
    { name: "Primary + backup", desc: `${primary.s.name} takes 100% now, ${backup.s.name} pre-qualified for surge or fallback.`, tco: singleTCO, risk: "Low", pros: "Full volume leverage today; instant fallback if the primary falters. Backup keeps them honest.", cons: `Backup will not hold pricing indefinitely. Requires periodic re-touching.` },
    { name: "Frame agreement (top 3)", desc: `${top3.map(r => r.s.name).join(", ")} on call-off. Volume distributed per period.`, tco: frameTCO, risk: "Low", pros: "Maximum resilience and category coverage. Excellent for volatile demand or multi-site rollouts.", cons: `Weakest per-vendor leverage; most admin. Suits recurring buys, not one-off tenders.` },
  ];
  return (
    <div className="cvcard p-5 mt-6">
      <div className="mb-3">
        <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>ALTERNATIVE AWARD STRUCTURES</div>
        <div className="cv-serif" style={{ fontSize: 20, color: "#F0EFE6" }}>The winner isn't always one supplier.</div>
      </div>
      <div className="text-[12px] mb-4" style={{ color: "var(--sage)" }}>Different structures trade cost against risk and leverage. Pick the shape that fits your mandate — not just the top-ranked name.</div>
      <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
        {structures.map(st => (
          <div key={st.name} className="rounded-lg p-3.5" style={{ background: "#0B221C", border: "1px solid #1C4034" }}>
            <div className="flex items-center justify-between mb-1">
              <div className="text-[13px] font-semibold" style={{ color: "#EDF3EE" }}>{st.name}</div>
              <span className={`chip cv-mono ${st.risk === "Low" ? "hi" : st.risk === "Med" ? "md" : "lo"}`}>RISK {st.risk.toUpperCase()}</span>
            </div>
            <div className="text-[11.5px]" style={{ color: "var(--sage)" }}>{st.desc}</div>
            <div className="cv-mono text-[12px] mt-1.5" style={{ color: "var(--copper-hot)" }}>Modeled TCO: {fmtMoney(st.tco, base)}</div>
            <div className="mt-2 text-[11.5px]" style={{ color: "var(--sage)", lineHeight: 1.55 }}>
              <div><span className="cv-mono text-[10px]" style={{ color: "var(--ok)" }}>PROS </span>{st.pros}</div>
              <div className="mt-1"><span className="cv-mono text-[10px]" style={{ color: "var(--low)" }}>CONS </span>{st.cons}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ---------- DUE DILIGENCE (personal quick-check, not institutional sign-off) ---------- */
function DueDiligencePanel({ suppliers, setSuppliers, fullState, setFullState }) {
  const dd = fullState.dueDiligence || {};
  const setDD = (sid, key, val) => setFullState(fs => ({
    ...fs, dueDiligence: { ...fs.dueDiligence, [sid]: { ...(fs.dueDiligence?.[sid] || {}), [key]: val } }
  }));
  return (
    <div className="cvcard p-5 mt-6">
      <div className="mb-3">
        <div className="cv-mono text-[10px]" style={{ color: "var(--moss)", letterSpacing: ".16em" }}>YOUR DUE DILIGENCE · PERSONAL QUICK-CHECK</div>
        <div className="cv-serif" style={{ fontSize: 20, color: "#F0EFE6" }}>What you've verified, in your own words.</div>
      </div>
      <div className="text-[12px] mb-4" style={{ color: "var(--sage)" }}>
        Not a compliance sign-off — your own working checklist. What documents you've seen, which references you've spoken to, what's still open, and any red flags to name.
      </div>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
        {suppliers.map(s => {
          const st = dd[s.id] || {};
          return (
            <div key={s.id} className="rounded-lg p-3.5" style={{ background: "#0B221C", border: "1px solid #1C4034" }}>
              <div className="text-[13px] font-semibold mb-2" style={{ color: "#EDF3EE" }}>{s.name}</div>
              <div className="cv-mono text-[10px] mb-1.5" style={{ color: "var(--copper)", letterSpacing: ".1em" }}>DOCUMENTS ON FILE</div>
              {[["biz_reg", "Business registration"], ["tax", "Tax certificate"], ["insurance", "Insurance"], ["dpa", "Data protection"]].map(([k, label]) => (
                <div key={k} className="flex items-center justify-between text-[11.5px] mb-0.5">
                  <span style={{ color: "var(--sage)" }}>{label}</span>
                  <TriState val={st[k] ?? (s.scores.compliance[k] ? "yes" : "no")} onChange={v => setDD(s.id, k, v)} />
                </div>
              ))}
              <div className="cv-mono text-[10px] mt-3 mb-1" style={{ color: "var(--copper)", letterSpacing: ".1em" }}>REFERENCE CHECKS</div>
              <input type="text" className="editfield cv-mono text-[11.5px]" placeholder="e.g. 2 of 3 contacted, positive" value={st.refs || ""} onChange={e => setDD(s.id, "refs", e.target.value)} />
              <div className="cv-mono text-[10px] mt-3 mb-1" style={{ color: "var(--copper)", letterSpacing: ".1em" }}>RED FLAGS / OPEN</div>
              <textarea className="editfield cv-mono text-[11.5px]" rows={2} placeholder="Anything unresolved, anything unusual" value={st.flags || ""} onChange={e => setDD(s.id, "flags", e.target.value)} />
              <div className="cv-mono text-[10px] mt-3 mb-1" style={{ color: "var(--copper)", letterSpacing: ".1em" }}>PERSONAL NOTE</div>
              <textarea className="editfield cv-mono text-[11.5px]" rows={2} placeholder="Your read on this supplier" value={st.note || ""} onChange={e => setDD(s.id, "note", e.target.value)} />
            </div>
          );
        })}
      </div>
      <div className="text-[10.5px] mt-3" style={{ color: "#4E7264" }}>This appears verbatim in the downloaded memo. Your own words, defensibly recorded.</div>
    </div>
  );
}
function TriState({ val, onChange }) {
  const states = [{ v: "yes", l: "✓", cls: "hi" }, { v: "partial", l: "~", cls: "md" }, { v: "no", l: "✗", cls: "lo" }];
  return (
    <div className="flex gap-1">
      {states.map(s => (
        <button key={s.v} className={`chip cv-mono ${val === s.v ? s.cls : ""}`} style={val !== s.v ? { background: "transparent", color: "#3E5F53", border: "1px solid #26493E" } : {}} onClick={() => onChange(s.v)}>{s.l}</button>
      ))}
    </div>
  );
}


function computeLevers(ranking, base, benchmarkBase) {
  const w = ranking[0], runner = ranking[1];
  const f = w.s.fields;
  const cheapest = [...ranking].sort((a, b) => a.tco.total - b.tco.total)[0];
  const maxWarranty = Math.max(...ranking.map(r => r.s.fields.warranty_months.value));
  const skontoVendor = ranking.find(r => r.s.fields.payment_terms.earlyDiscount && r.s.id !== w.s.id);
  const unitBase = convert(f.unit_price.value, w.s.currency, base);
  const levers = [];
  if (cheapest.s.id !== w.s.id) {
    const gap = w.tco.total - cheapest.tco.total;
    levers.push({ cat: "PRICE", title: "Competitive anchor", detail: `${cheapest.s.name} lands ${fmtMoney(gap, base)} below on 3-yr TCO. You are asking them to defend the premium.`, ask: `Share the gap (not the quote); half closes ${fmtMoney(gap / 2, base)}.`, value: gap / 2, field: "unit_price" });
  } else {
    levers.push({ cat: "TERMS", title: "You hold the price card — spend it on terms", detail: `${w.s.name} is already the cheapest compliant offer.`, ask: "Trade a fast signature for warranty extension, stronger SLAs, or softer payment structure.", value: null, field: "unit_price" });
  }
  const driftPct = ((unitBase - benchmarkBase) / benchmarkBase) * 100;
  if (driftPct > 1) levers.push({ cat: "PRICE", title: "Your own benchmark says the price drifted", detail: `Unit price is ${driftPct.toFixed(1)}% above what you accepted for a similar category in Q1 2026 (${fmtMoney(benchmarkBase, base)}/unit).`, ask: `Cite your historical price point. Recovering the drift is worth ${fmtMoney((unitBase - benchmarkBase) * f.quantity.value, base)} on this order.`, value: (unitBase - benchmarkBase) * f.quantity.value, field: "unit_price" });
  if (w.tco.maint3 / w.tco.total > 0.06) levers.push({ cat: "SERVICE", title: `Maintenance is a quiet ${Math.round((w.tco.maint3 / w.tco.total) * 100)}% of the deal`, detail: `Three years of service adds ${fmtMoney(w.tco.maint3, base)}. Service margins are typically softest.`, ask: `Ask for year 1 free with the order, or a 3-yr prepaid rate at −15%. Worth ~${fmtMoney(w.tco.maint3 * 0.15, base)}.`, value: w.tco.maint3 * 0.15, field: "maintenance_annual" });
  if (!f.payment_terms.earlyDiscount && skontoVendor) { const v = 0.02 * w.tco.goods; levers.push({ cat: "TERMS", title: "Another vendor pays you to pay fast — this one doesn't", detail: `${skontoVendor.s.name} offers a 2% skonto. ${w.s.name} quotes ${f.payment_terms.value} with none.`, ask: `Ask to match 2% for payment within 5 days — worth ${fmtMoney(v, base)}.`, value: v, field: "payment_terms" }); }
  if ((f.payment_terms.advance || 0) > 0.2) { const fin20 = 0.2 * (w.tco.goods + w.tco.freight) * COST_OF_CASH * (f.lead_time_days.value / 365); const saving = w.tco.financing - fin20; levers.push({ cat: "TERMS", title: "The advance payment is your money working for them", detail: `${Math.round(f.payment_terms.advance * 100)}% advance over a ${f.lead_time_days.value}-day lead ties up cash — ${fmtMoney(w.tco.financing, base)} of financing cost.`, ask: `Propose 20% advance / 80% against delivery, or an LC. Saves ~${fmtMoney(saving, base)}.`, value: saving, field: "payment_terms" }); }
  if (f.warranty_months.value < maxWarranty) levers.push({ cat: "RISK", title: "Warranty lags the best in this RFQ", detail: `${f.warranty_months.value} months vs ${maxWarranty} elsewhere in the same tender.`, ask: `Ask to match ${maxWarranty} months.`, value: null, field: "warranty_months" });
  else levers.push({ cat: "RISK", title: "Warranty is strong — harden it into an SLA", detail: `${f.warranty_months.value} months is the best in the set.`, ask: "Ask for a written on-site response commitment (72h or better) and named spare-parts availability.", value: null, field: "warranty_months" });
  levers.push({ cat: "RISK", title: "Lead time needs teeth", detail: `${f.lead_time_days.value} days quoted. A date without a consequence is a hope.`, ask: "Ask for liquidated damages of 0.5% of order value per week of delay (capped at 5%), and a price-validity extension to 90 days.", value: null, field: "lead_time_days" });
  const headroom = levers.reduce((s, l) => s + (l.value || 0), 0);
  return { levers, headroom, runner };
}

/* ================= RECOMMENDATION SUMMARY ================= */
/* The written memo language the professional signs their name to.
   Grounded generation: computed facts → model → prose. The model
   never invents numbers; it sequences and phrases what the engine
   already knows. Editable before export. */

function buildRecommendationFacts({ mode, ranking, base, benchmarkBase, bestUnitBase, drift, weights, preset, fullState, suppliers }) {
  const w = ranking[0], r2 = ranking[1], r3 = ranking[2];
  const { levers, headroom, runner } = computeLevers(ranking, base, benchmarkBase);

  const facts = {
    context: {
      rfq: "RFQ-2026-114 · 40 commercial split AC units + installation, Berlin",
      analysis_mode: mode,
      decision_currency: base,
      benchmark_reference: `${TWIN.lastAnalysis.label}, best unit price ${fmtMoney(benchmarkBase, base)}/unit`,
    },
    ranking: ranking.map((r, i) => ({
      rank: i + 1,
      name: r.s.name,
      country: r.s.country,
      currency_quoted: r.s.currency,
      tco_3yr: fmtMoney(r.tco.total, base),
      unit_price: fmtMoney(convert(r.s.fields.unit_price.value, r.s.currency, base), base),
      lead_time_days: r.s.fields.lead_time_days.value,
      warranty_months: r.s.fields.warranty_months.value,
      payment_terms: r.s.fields.payment_terms.value,
      incoterm: r.s.fields.incoterm.value,
      weighted_score: Math.round(r.score * 100),
      mcda_breakdown: r.mcda ? Object.fromEntries(Object.entries(r.mcda).map(([k, v]) => [k, Math.round(v)])) : null,
    })),
    benchmark_drift: {
      best_unit_today: fmtMoney(bestUnitBase, base),
      vs_benchmark_pct: `${drift > 0 ? "+" : "−"}${Math.abs(drift).toFixed(1)}%`,
      direction: drift > 0 ? "above your historical benchmark" : "below your historical benchmark",
    },
    weights_used: mode === "quick" ? weights : { ...weights, preset: PRESETS[preset]?.name || "custom" },
    negotiation: {
      total_estimated_headroom: headroom > 0 ? fmtMoney(headroom, base) : null,
      top_levers: levers.filter(l => l.value > 0).slice(0, 3).map(l => ({ category: l.cat, ask: l.ask, worth: fmtMoney(l.value, base) })),
      batna: runner ? { supplier: runner.s.name, tco: fmtMoney(runner.tco.total, base) } : null,
    },
  };

  if (mode === "full") {
    const perturb = (delta) => Object.fromEntries(Object.entries(weights).map(([k, v]) => [k, Math.max(0, v + (delta[k] || 0))]));
    const tests = [
      { label: "Price weight +15", w: perturb({ price: 15 }) },
      { label: "Price weight −15", w: perturb({ price: -15 }) },
      { label: "Risk weight ×2", w: { ...weights, risk: weights.risk * 2 } },
      { label: "Technical weight +10", w: perturb({ technical: 10 }) },
      { label: "Capability weight +10", w: perturb({ capability: 10 }) },
    ];
    const holds = tests.filter(t => rankStandard(suppliers, t.w, base)[0].s.id === w.s.id).length;
    facts.sensitivity = { winner_holds_in: `${holds} of ${tests.length}`, verdict: holds >= 4 ? "ROBUST" : holds >= 3 ? "STABLE" : "FRAGILE" };

    const dd = fullState?.dueDiligence || {};
    const flagged = Object.entries(dd).filter(([_, x]) => x?.flags).map(([sid, x]) => {
      const s = suppliers.find(z => z.id === sid);
      return { supplier: s?.name, note: x.flags };
    });
    if (flagged.length) facts.red_flags = flagged;
  }

  return facts;
}

function fallbackRecommendation(facts) {
  const w = facts.ranking[0], r2 = facts.ranking[1];
  const marginPts = w.weighted_score - (r2?.weighted_score || 0);
  const parts = [];
  parts.push(`**Recommendation:** Award ${w.name} (${w.country}) at a 3-year TCO of ${w.tco_3yr}.`);
  parts.push("");
  parts.push(`${w.name} ranks first on the applied weighting with a score of ${w.weighted_score}/100, a ${marginPts}-point margin over ${r2?.name || "the runner-up"}. Their offer combines a unit price of ${w.unit_price} with a ${w.lead_time_days}-day lead time, ${w.warranty_months}-month warranty, and terms of ${w.payment_terms} (${w.incoterm}).`);
  parts.push("");
  parts.push(`Against the ${facts.context.benchmark_reference}, the best unit price in this RFQ is ${facts.benchmark_drift.best_unit_today} — ${facts.benchmark_drift.vs_benchmark_pct} ${facts.benchmark_drift.direction}.`);
  if (facts.sensitivity) {
    parts.push("");
    parts.push(`Sensitivity: the recommended ranking is **${facts.sensitivity.verdict}**, holding in ${facts.sensitivity.winner_holds_in} weight-perturbation scenarios.`);
  }
  if (facts.negotiation.total_estimated_headroom) {
    parts.push("");
    parts.push(`**Negotiation:** an estimated ${facts.negotiation.total_estimated_headroom} of headroom remains before signature. Priorities: ${facts.negotiation.top_levers.slice(0, 2).map(l => l.ask).join(" ")}`);
  }
  if (facts.negotiation.batna) {
    parts.push("");
    parts.push(`**BATNA:** ${facts.negotiation.batna.supplier} at ${facts.negotiation.batna.tco} remains a credible alternative. Retain until signature.`);
  }
  if (facts.red_flags?.length) {
    parts.push("");
    parts.push(`**Open items to close before award:** ${facts.red_flags.map(f => `${f.supplier} — ${f.note}`).join("; ")}.`);
  }
  return parts.join("\n");
}

function RecommendationSummary({ mode, ranking, base, benchmarkBase, bestUnitBase, drift, weights, preset, fullState, suppliers, recommendation, setRecommendation }) {
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const generated = recommendation?.text || "";
  const facts = useMemo(() => buildRecommendationFacts({ mode, ranking, base, benchmarkBase, bestUnitBase, drift, weights, preset, fullState, suppliers }), [mode, ranking, base, benchmarkBase, bestUnitBase, drift, weights, preset, fullState, suppliers]);

  async function generate() {
    setBusy(true); setError(null);
    try {
      const wordLimit = mode === "full" ? 320 : mode === "standard" ? 220 : 140;
      const prompt = `You are drafting the recommendation summary a procurement professional will paste into their approval memo. Write in their voice — first-person is acceptable but not required — with the confidence of a senior practitioner. British English.

Use ONLY the facts in the JSON below. Do NOT invent numbers, dates, supplier attributes, or claims not present. Never mention "the model", "the AI", "the analysis engine", or "Corvendis" — write as the professional.

Structure (adapt if a section has no facts):
1. Recommendation in one sentence — supplier, country, headline commercial (TCO, unit price).
2. Rationale — why this supplier ranks first: the two or three commercial and qualitative reasons that matter most.
3. Comparison — what the runner-up offers and why it did not win.
4. Benchmark & context — how the winning unit price compares to the historical benchmark provided.
${mode === "full" ? "5. Sensitivity — one sentence on whether the recommendation is robust to weight changes.\n" : ""}6. Negotiation posture — the top one or two remaining asks and estimated value.
7. BATNA — the fallback supplier and TCO.
${facts.red_flags?.length ? "8. Open items to close before award.\n" : ""}
Length: ${wordLimit} words maximum. Prose paragraphs, no bullet points, no headers. Every number, name, and percentage must appear exactly as given in the facts. Use markdown **bold** only for the recommendation itself and the BATNA tag.

FACTS:
${JSON.stringify(facts, null, 2)}`;

      const resp = await fetch("https://api.anthropic.com/v1/messages", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 1000, messages: [{ role: "user", content: prompt }] }),
      });
      const data = await resp.json();
      const text = (data.content || []).filter(b => b.type === "text").map(b => b.text).join("\n").trim();
      if (!text) throw new Error("Empty response");
      setRecommendation({ text, generatedAt: new Date().toISOString(), source: "generated" });
    } catch (e) {
      setRecommendation({ text: fallbackRecommendation(facts), generatedAt: new Date().toISOString(), source: "fallback" });
      setError("Live drafting unavailable — using deterministic fallback. You can edit it.");
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="cvcard p-5 mt-6" style={{ borderColor: "#D97A3D44" }}>
      <div className="flex flex-wrap items-end justify-between gap-3 mb-3">
        <div>
          <div className="cv-mono text-[10px]" style={{ color: "var(--copper-hot)", letterSpacing: ".16em" }}>RECOMMENDATION SUMMARY · FOR YOUR APPROVAL MEMO</div>
          <div className="cv-serif" style={{ fontSize: 22, color: "#F0EFE6" }}>Your write-up, in one paste.</div>
        </div>
        <div className="flex gap-2">
          {generated && <button className="btn-ghost px-3 py-2 text-[11px]" onClick={() => navigator.clipboard?.writeText(generated)}>Copy</button>}
          <button className="btn-copper px-4 py-2 text-[12px]" disabled={busy} onClick={generate}>
            {busy ? "Drafting…" : generated ? "Regenerate" : "Draft recommendation"}
          </button>
        </div>
      </div>
      <div className="text-[12px] mb-3" style={{ color: "var(--sage)", lineHeight: 1.6 }}>
        A grounded, editable summary you can paste directly into your approval memo. Every figure, supplier, and percentage is passed as a locked fact from the analysis — the language is drafted, the numbers are not.
      </div>
      {!generated && !busy && (
        <div className="rounded-lg p-4 text-[12.5px] text-center" style={{ background: "#0B221C", border: "1px dashed #26493E", color: "var(--moss)" }}>
          Click <b style={{ color: "var(--copper-hot)" }}>Draft recommendation</b> to generate a written summary based on this analysis. You can edit before you copy.
        </div>
      )}
      {generated && (
        <>
          <textarea
            className="cv-scroll"
            style={{ width: "100%", minHeight: 220, padding: "14px 16px", background: "#0B221C", border: "1px solid #26493E", borderRadius: 10, color: "#EDF3EE", fontSize: 13.5, lineHeight: 1.7, fontFamily: "'Public Sans', sans-serif", resize: "vertical" }}
            value={generated}
            onChange={e => setRecommendation({ ...recommendation, text: e.target.value, source: "edited" })}
          />
          <div className="flex items-center justify-between mt-2 flex-wrap gap-2">
            <div className="text-[10.5px]" style={{ color: "#4E7264" }}>
              {recommendation?.source === "edited" ? "Edited by you" : recommendation?.source === "fallback" ? "Deterministic draft — edit as you like" : "Drafted just now — edit as you like"}
              {" · "}
              {generated.trim().split(/\s+/).length} words
              {" · included in the Word memo when you download"}
            </div>
            {recommendation?.source === "edited" && <span className="chip verified cv-mono">✓ YOUR WORDING</span>}
          </div>
        </>
      )}
      {error && <div className="text-[11px] mt-2" style={{ color: "var(--warn)" }}>{error}</div>}
    </div>
  );
}

function NegotiationCoach({ ranking, base, benchmarkBase, onEvidence }) {
  const { levers, headroom, runner } = useMemo(() => computeLevers(ranking, base, benchmarkBase), [ranking, base, benchmarkBase]);
  const w = ranking[0];
  return (
    <div className="cvcard p-5 mt-6" style={{ borderColor: "#B25E2A66" }}>
      <div className="flex flex-wrap items-end justify-between gap-3 mb-1">
        <div>
          <div className="cv-mono text-[10px]" style={{ color: "var(--copper-hot)", letterSpacing: ".16em" }}>NEGOTIATION COACH · BEFORE YOU SIGN</div>
          <div className="cv-serif" style={{ fontSize: 20, color: "#F0EFE6" }}>Best offer ≠ final offer.</div>
        </div>
        {headroom > 0 && (<div className="text-right"><div className="cv-mono" style={{ fontSize: 20, fontWeight: 600, color: "var(--copper-hot)" }}>~{fmtMoney(headroom, base)}</div><div className="text-[10px] cv-mono" style={{ color: "var(--moss)" }}>ESTIMATED HEADROOM</div></div>)}
      </div>
      <div className="text-[12px] mb-4" style={{ color: "var(--sage)" }}>Levers below are computed from the quotes in this RFQ — each names the evidence, the ask, and what it is worth with {w.s.name}.</div>
      <div className="space-y-2.5">
        {levers.map((l, i) => (
          <div key={i} className="rounded-lg px-4 py-3 cardin" style={{ background: "#0B221C", border: "1px solid #1C4034", animationDelay: `${i * 60}ms` }}>
            <div className="flex flex-wrap items-center gap-2 mb-1">
              <span className={`chip cv-mono ${l.cat === "PRICE" ? "verified" : l.cat === "TERMS" ? "md" : "hi"}`}>{l.cat}</span>
              <span className="text-[13px] font-semibold" style={{ color: "#EDF3EE" }}>{l.title}</span>
              {l.value > 0 && <span className="cv-mono text-[12px] ml-auto traced" style={{ color: "var(--copper-hot)" }} onClick={() => onEvidence({ supplierId: w.s.id, fieldKey: l.field })}>worth ~{fmtMoney(l.value, base)}</span>}
            </div>
            <div className="text-[12px]" style={{ color: "var(--sage)", lineHeight: 1.6 }}>{l.detail}</div>
            <div className="text-[12px] mt-1.5" style={{ color: "#F2E6DA", lineHeight: 1.6 }}><span className="cv-mono text-[10px] mr-1.5" style={{ color: "var(--copper)" }}>ASK</span>{l.ask}</div>
          </div>
        ))}
      </div>
      {runner && (<div className="mt-3 rounded-lg px-4 py-3" style={{ background: "#B25E2A14", border: "1px dashed #B25E2A55" }}><span className="cv-mono text-[10px] mr-2" style={{ color: "var(--copper-hot)" }}>YOUR BATNA</span><span className="text-[12.5px]" style={{ color: "#F2E6DA" }}>{runner.s.name} at {fmtMoney(runner.tco.total, base)} is a credible alternative. Keep them warm until signature.</span></div>)}
    </div>
  );
}

/* ================= WORD EXPORT ================= */
function downloadWord({ mode, ranking, weights, preset, base, benchmarkBase, bestUnitBase, drift, suppliers, fullState, recommendation }) {
  const ranked = ranking.map((r, i) => ({ ...r, _rank: i }));
  const rows = mode === "standard" ? decisionRowsStandard(ranking, base) : decisionRowsQuick(ranking, base);
  const { levers, headroom, runner } = computeLevers(ranking, base, benchmarkBase);
  const esc = s => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  const winner = ranked[0];
  const foreign = [...new Set(ranked.map(r => r.s.currency))].filter(c => c !== base);
  const today = new Date().toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" });

  const th = (r, i) => `<th style="border:1px solid #C9D4CE;padding:8px 10px;text-align:left;background:${i === 0 ? "#F6E3D3" : "#EDF2EF"};font-family:Calibri,sans-serif;font-size:12pt;color:${i === 0 ? "#B25E2A" : "#1F5C4D"}">${esc(r.s.name)}<br/><span style="font-size:9pt;color:#5D6660;font-weight:normal">${esc(r.s.country)}${i === 0 ? " — RECOMMENDED" : ""}</span></th>`;
  const bodyRows = rows.map(([label, fn, strong], ri) =>
    `<tr style="background:${ri % 2 ? "#F7FAF8" : "#FFFFFF"}">
      <td style="border:1px solid #C9D4CE;padding:6px 10px;font-family:Calibri,sans-serif;font-size:10.5pt;color:#3A4540">${esc(label)}</td>
      ${ranked.map((r, i) => `<td style="border:1px solid #C9D4CE;padding:6px 10px;font-family:Consolas,monospace;font-size:10.5pt;${strong ? "font-weight:bold;" : ""}${i === 0 ? "background:#FBF0E7;color:#8A4A21;" : "color:#22302B;"}">${esc(fn(r))}</td>`).join("")}
    </tr>`).join("");

  // Standard & Full: exec summary section
  let execSection = "";
  if (mode === "standard" || mode === "full") {
    const top3 = ranking.slice(0, 3);
    execSection = `<p style="font-size:14pt;font-weight:bold;color:#1F5C4D;margin:16pt 0 4pt 0">Executive Summary — Top 3 Suppliers</p>` +
      top3.map((r, i) => {
        const sw = summarizeStrengthsWeaknesses(r);
        return `<p style="font-size:11pt;margin:6pt 0 2pt 0"><b>${i+1}. ${esc(r.s.name)} — ${Math.round(r.score*100)}/100</b></p>
        <p style="font-size:10.5pt;margin:0 0 2pt 12pt"><b style="color:#68B984">Strengths:</b> ${sw.strengths.map(esc).join(" · ")}</p>
        <p style="font-size:10.5pt;margin:0 0 8pt 12pt"><b style="color:#B25E2A">Weaknesses:</b> ${sw.weaknesses.map(esc).join(" · ")}</p>`;
      }).join("");
  }

  // Full-only sections
  let fullSections = "";
  if (mode === "full") {
    // Sensitivity
    const perturb = (delta) => Object.fromEntries(Object.entries(weights).map(([k, v]) => [k, Math.max(0, v + (delta[k] || 0))]));
    const sensScenarios = [
      { label: "Price weight +15", w: perturb({ price: 15 }) },
      { label: "Price weight −15", w: perturb({ price: -15 }) },
      { label: "Risk weight ×2", w: { ...weights, risk: weights.risk * 2 } },
      { label: "Technical weight +10", w: perturb({ technical: 10 }) },
      { label: "Capability weight +10", w: perturb({ capability: 10 }) },
    ];
    const sensResults = sensScenarios.map(sc => { const r = rankStandard(suppliers, sc.w, base); return { label: sc.label, winner: r[0].s.name, holds: r[0].s.id === winner.s.id }; });
    const holdsCount = sensResults.filter(x => x.holds).length;
    const verdict = holdsCount >= 4 ? "ROBUST" : holdsCount >= 3 ? "STABLE" : "FRAGILE";
    fullSections += `<p style="font-size:14pt;font-weight:bold;color:#1F5C4D;margin:16pt 0 4pt 0">Sensitivity Analysis — ${verdict}</p>
    <p style="font-size:10.5pt;margin:0 0 4pt 0">The recommended winner (${esc(winner.s.name)}) holds in <b>${holdsCount} of ${sensResults.length}</b> weight-perturbation scenarios:</p>
    <ul style="font-size:10.5pt;margin:0 0 8pt 16pt">
      ${sensResults.map(s => `<li>${esc(s.label)} — ${s.holds ? `<span style="color:#68B984">✓ ${esc(winner.s.name)} still #1</span>` : `<span style="color:#B25E2A">⚑ Flips to ${esc(s.winner)}</span>`}</li>`).join("")}
    </ul>`;

    // Scenarios if any applied
    const scenarios = fullState?.scenarios || {};
    const dirty = Object.values(scenarios).some(d => d && (d.pricePct || d.leadPct || d.warrantyDelta));
    if (dirty) {
      const modified = suppliers.map(s => {
        const d = scenarios[s.id] || { pricePct: 0, leadPct: 0, warrantyDelta: 0 };
        return { ...s, fields: { ...s.fields,
          unit_price: { ...s.fields.unit_price, value: s.fields.unit_price.value * (1 + d.pricePct / 100) },
          lead_time_days: { ...s.fields.lead_time_days, value: Math.max(1, Math.round(s.fields.lead_time_days.value * (1 + d.leadPct / 100))) },
          warranty_months: { ...s.fields.warranty_months, value: Math.max(0, s.fields.warranty_months.value + d.warrantyDelta) },
        }};
      });
      const modRank = rankStandard(modified, weights, base);
      const deltaLines = Object.entries(scenarios).filter(([_, d]) => d && (d.pricePct || d.leadPct || d.warrantyDelta)).map(([sid, d]) => {
        const s = suppliers.find(x => x.id === sid);
        const parts = [];
        if (d.pricePct) parts.push(`unit price ${d.pricePct > 0 ? "+" : ""}${d.pricePct}%`);
        if (d.leadPct) parts.push(`lead time ${d.leadPct > 0 ? "+" : ""}${d.leadPct}%`);
        if (d.warrantyDelta) parts.push(`warranty ${d.warrantyDelta > 0 ? "+" : ""}${d.warrantyDelta} months`);
        return `<li>${esc(s.name)}: ${parts.join("; ")}</li>`;
      }).join("");
      fullSections += `<p style="font-size:14pt;font-weight:bold;color:#1F5C4D;margin:16pt 0 4pt 0">Scenario Modelling — Modelled Outcome</p>
      <p style="font-size:10.5pt;margin:0 0 2pt 0"><b>Applied deltas:</b></p>
      <ul style="font-size:10.5pt;margin:0 0 6pt 16pt">${deltaLines}</ul>
      <p style="font-size:10.5pt;margin:0 0 2pt 0"><b>Ranking under modelled conditions:</b> ${modRank.map((r, i) => `${i+1}. ${esc(r.s.name)} (${Math.round(r.score * 100)})`).join(" · ")}</p>`;
    }

    // Award structures
    const splitTCO = 0.6 * ranking[0].tco.total + 0.4 * ranking[1].tco.total;
    const singleTCO = ranking[0].tco.total;
    const frameTCO = ranking.slice(0, 3).reduce((s, r) => s + r.tco.total, 0) / 3;
    fullSections += `<p style="font-size:14pt;font-weight:bold;color:#1F5C4D;margin:16pt 0 4pt 0">Alternative Award Structures</p>
    <table style="border-collapse:collapse;width:100%;font-size:10pt;margin-bottom:8pt">
      <tr style="background:#EDF2EF"><th style="border:1px solid #C9D4CE;padding:6px;text-align:left">Structure</th><th style="border:1px solid #C9D4CE;padding:6px;text-align:left">Modeled TCO</th><th style="border:1px solid #C9D4CE;padding:6px;text-align:left">Risk</th></tr>
      <tr><td style="border:1px solid #C9D4CE;padding:6px"><b>Single-source</b> — ${esc(ranking[0].s.name)} takes 100%</td><td style="border:1px solid #C9D4CE;padding:6px">${esc(fmtMoney(singleTCO, base))}</td><td style="border:1px solid #C9D4CE;padding:6px">Med</td></tr>
      <tr style="background:#F7FAF8"><td style="border:1px solid #C9D4CE;padding:6px"><b>Split 60/40</b> — ${esc(ranking[0].s.name)} 60% · ${esc(ranking[1].s.name)} 40%</td><td style="border:1px solid #C9D4CE;padding:6px">${esc(fmtMoney(splitTCO, base))}</td><td style="border:1px solid #C9D4CE;padding:6px">Low</td></tr>
      <tr><td style="border:1px solid #C9D4CE;padding:6px"><b>Primary + backup</b> — ${esc(ranking[0].s.name)} live, ${esc(ranking[1].s.name)} pre-qualified</td><td style="border:1px solid #C9D4CE;padding:6px">${esc(fmtMoney(singleTCO, base))}</td><td style="border:1px solid #C9D4CE;padding:6px">Low</td></tr>
      <tr style="background:#F7FAF8"><td style="border:1px solid #C9D4CE;padding:6px"><b>Frame agreement (top 3)</b></td><td style="border:1px solid #C9D4CE;padding:6px">${esc(fmtMoney(frameTCO, base))}</td><td style="border:1px solid #C9D4CE;padding:6px">Low</td></tr>
    </table>`;

    // Due diligence
    const dd = fullState?.dueDiligence || {};
    const hasDD = Object.keys(dd).length > 0 && Object.values(dd).some(x => x && (x.refs || x.flags || x.note));
    if (hasDD) {
      fullSections += `<p style="font-size:14pt;font-weight:bold;color:#1F5C4D;margin:16pt 0 4pt 0">Due Diligence — Personal Quick-Check</p>`;
      suppliers.forEach(s => {
        const st = dd[s.id]; if (!st) return;
        const docs = [["biz_reg", "Business registration"], ["tax", "Tax certificate"], ["insurance", "Insurance"], ["dpa", "Data protection"]]
          .map(([k, l]) => {
            const v = st[k] ?? (s.scores.compliance[k] ? "yes" : "no");
            const mark = v === "yes" ? "✓" : v === "partial" ? "~" : "✗";
            return `${esc(l)} ${mark}`;
          }).join(" · ");
        fullSections += `<p style="font-size:11pt;margin:6pt 0 2pt 0"><b>${esc(s.name)}</b></p>
        <p style="font-size:10pt;margin:0 0 2pt 12pt">Documents on file: ${docs}</p>
        ${st.refs ? `<p style="font-size:10pt;margin:0 0 2pt 12pt">Reference checks: ${esc(st.refs)}</p>` : ""}
        ${st.flags ? `<p style="font-size:10pt;margin:0 0 2pt 12pt;color:#B25E2A">Open / red flags: ${esc(st.flags)}</p>` : ""}
        ${st.note ? `<p style="font-size:10pt;margin:0 0 8pt 12pt;font-style:italic">Personal note: ${esc(st.note)}</p>` : ""}`;
      });
    }
  }

  const modeLabel = mode === "full" ? "Full Decision Memo (deep individual analysis)"
    : mode === "standard" ? "Full Evaluation Report (Standard mode · MCDA)" : "Quick Decision Brief";
  const weightsLine = (mode === "standard" || mode === "full")
    ? `Preset: ${esc(PRESETS[preset]?.name || "custom")}. MCDA (multi-criteria) weights — Price ${weights.price}%, Technical ${weights.technical}%, Capability ${weights.capability}%, Delivery ${weights.delivery}%, Safety/Risk ${weights.risk}%, Innovation ${weights.innovation}%.`
    : `Weights — TCO ${weights.price}, lead ${weights.lead}, warranty ${weights.warranty}, payment ${weights.payment}.`;

  // Convert the recommendation's minimal markdown (**bold** and paragraph breaks) into safe HTML.
  const recToHtml = (raw) => {
    const escaped = esc(raw);
    const withBold = escaped.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
    return withBold.split(/\n\n+/).map(p => p.replace(/\n/g, "<br/>").trim()).filter(Boolean).map(p => `<p style="font-size:11pt;margin:0 0 8pt 0;line-height:1.55">${p}</p>`).join("");
  };
  const recommendationBlock = recommendation?.text
    ? `<div style="border-left:4px solid #D97A3D;padding:10pt 14pt;margin:0 0 14pt 0;background:#FBF0E7">
         <p style="font-size:10pt;font-weight:bold;color:#B25E2A;margin:0 0 6pt 0;letter-spacing:1px">RECOMMENDATION SUMMARY</p>
         ${recToHtml(recommendation.text)}
       </div>`
    : "";

  const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word">
<head><meta charset="utf-8"><title>Corvendis Decision Report</title></head>
<body style="font-family:Calibri,sans-serif;color:#22302B">
  <p style="font-size:20pt;font-weight:bold;color:#1F5C4D;margin:0">CORVENDIS — ${esc(modeLabel)}</p>
  <p style="font-size:11pt;color:#5D6660;margin:4pt 0 14pt 0">RFQ-2026-114 · 40 commercial split AC units + installation, Berlin · ${today}</p>
  ${recommendationBlock}
  <p style="font-size:11pt;margin:0 0 4pt 0"><b>Recommended vendor:</b> ${esc(winner.s.name)} (${esc(winner.s.country)}) — 3-year TCO ${esc(fmtMoney(winner.tco.total, base))}, weighted score ${Math.round(winner.score * 100)}/100.</p>
  <table style="border-collapse:collapse;width:100%;margin-top:8pt">
    <tr><th style="border:1px solid #C9D4CE;padding:8px 10px;text-align:left;background:#EDF2EF;font-family:Calibri,sans-serif;font-size:11pt;color:#1F5C4D">Criterion</th>${ranked.map(th).join("")}</tr>
    ${bodyRows}
  </table>
  ${execSection}
  ${fullSections}
  <p style="font-size:14pt;font-weight:bold;color:#B25E2A;margin:16pt 0 4pt 0">Areas for further negotiation${headroom > 0 ? ` — estimated headroom ~${esc(fmtMoney(headroom, base))}` : ""}</p>
  <ol style="font-size:10.5pt;color:#22302B;margin:0 0 6pt 16pt;padding:0">
    ${levers.map(l => `<li style="margin-bottom:6pt"><b>[${l.cat}] ${esc(l.title)}${l.value > 0 ? ` — worth ~${esc(fmtMoney(l.value, base))}` : ""}.</b> ${esc(l.detail)}<br/><i>Ask: ${esc(l.ask)}</i></li>`).join("")}
  </ol>
  ${runner ? `<p style="font-size:10.5pt;color:#8A4A21;margin:0 0 12pt 0"><b>BATNA:</b> ${esc(runner.s.name)} at ${esc(fmtMoney(runner.tco.total, base))} remains a credible alternative — keep them warm until signature.</p>` : ""}
  <p style="font-size:9.5pt;color:#5D6660;margin-top:12pt"><b>Audit block.</b> Decision currency: ${base}. FX ${FX_DATE}${foreign.length ? " — rates applied: " + foreign.map(c => `${c}→${base} @ ${crossRate(c, base).toFixed(4)}`).join(", ") : ""}. ${weightsLine} Financing = advance % × (goods+freight) × 8% p.a. × lead time. Benchmark (Procurement Twin): ${esc(fmtMoney(benchmarkBase, base))}/unit — best today ${esc(fmtMoney(bestUnitBase, base))} (${drift > 0 ? "+" : "−"}${Math.abs(drift).toFixed(1)}%). Every figure traceable to source in Corvendis.</p>
</body></html>`;

  const blob = new Blob(["\ufeff" + html], { type: "application/msword" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url; a.download = `Corvendis_${mode === "full" ? "Full_Decision_Memo" : mode === "standard" ? "Full_Evaluation" : "Quick_Decision"}_RFQ-2026-114.doc`;
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

/* ================= EVIDENCE DRAWER ================= */
function EvidenceDrawer({ evidence, suppliers, onClose }) {
  const s = suppliers?.find(x => x.id === evidence.supplierId);
  const f = s?.fields?.[evidence.fieldKey];
  useEffect(() => { const onKey = e => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]);
  if (!s || !f) return null;
  const pageIdx = Math.max(0, (f.page || 1) - 1);
  return (
    <aside className="drawerin cv-scroll" style={{ width: "min(440px, 92vw)", borderLeft: "1px solid #1E4237", background: "#0A1E18", padding: 20, overflowY: "auto", maxHeight: "calc(100vh - 76px)", position: "sticky", top: 76 }}>
      <div className="flex items-center justify-between mb-1">
        <div className="cv-mono text-[10px]" style={{ color: "var(--copper-hot)", letterSpacing: ".18em" }}>EVIDENCE · SOURCE TRACE</div>
        <button className="btn-ghost px-2.5 py-1 text-xs" onClick={onClose}>Close ✕</button>
      </div>
      <div className="text-sm font-semibold mb-0.5" style={{ color: "#EDF3EE" }}>{s.name}</div>
      <div className="text-[11px] cv-mono mb-4" style={{ color: "var(--moss)" }}>field: {evidence.fieldKey} · page {f.page || "?"} · confidence {f.verified ? "human-verified" : (f.conf ?? 0).toFixed(2)}</div>
      {(s.doc || []).map((pg, i) => (
        <div key={i} className="paper rounded-lg p-4 mb-3 text-[12px]" style={{ lineHeight: 1.75, whiteSpace: "pre-wrap", opacity: i === pageIdx ? 1 : 0.45 }}>
          <div className="cv-mono text-[9px] mb-2" style={{ color: "#8A8672", letterSpacing: ".14em" }}>— PAGE {i + 1} —</div>
          {i === pageIdx && f.snippet ? highlight(pg, f.snippet) : pg}
        </div>
      ))}
      {f.note && <div className="text-[11px] mt-1" style={{ color: "var(--warn)" }}>⚑ Analyst note: {f.note}</div>}
      <div className="text-[10.5px] mt-3" style={{ color: "#4E7264", lineHeight: 1.6 }}>This trace ships with every export — the receipt behind the recommendation.</div>
    </aside>
  );
}
function highlight(text, snippet) {
  const idx = text.indexOf(snippet); if (idx < 0) return text;
  return (<>{text.slice(0, idx)}<mark className="paper-hl" style={{ color: "inherit", background: undefined }}>{snippet}</mark>{text.slice(idx + snippet.length)}</>);
}


// Expose to global scope for the HTML wrapper
window.CorvendisPrototype = CorvendisPrototype;
