File size: 46,753 Bytes
d5a43af
a56b06d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="TradeScope is an offline international trade analytics laboratory for export concentration, market stress and diversification."><title>TradeScope | Export Intelligence Lab</title><style>
:root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#080e18;color:#eaf1f6;font-synthesis:none;font-feature-settings:"tnum";--canvas:#0b111c;--card:#101a27;--border:#273341;--quiet:#8191a6;--green:#37c9a9;--sky:#85aafa;--gold:#dfbd83}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;min-width:320px;background:#0b1019;color:#eaf1f6}button,input,select{font:inherit}button{cursor:pointer}button:focus-visible,select:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid #71b1ff;outline-offset:3px}a{color:inherit;text-decoration:none}.shell{display:grid;grid-template-columns:260px minmax(0,1fr);min-height:100vh;max-width:1880px;margin:auto}.sidebar{background:#0b121d;border-right:1px solid #23303f;display:flex;flex-direction:column;padding:30px 15px 25px;min-height:100vh;position:sticky;top:0;height:100vh}.brand{display:flex;align-items:center;gap:12px;padding:4px 10px 46px}.brand-icon{width:41px;height:41px;border:1px solid #64caae;border-radius:12px;background:#173e37;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:800;color:#e5fff6;box-shadow:0 0 26px #1a6a5840}.brand-icon span{color:#48e0bc}.brand strong{display:block;font-size:20px;letter-spacing:-.6px}.period{color:var(--green)}.brand small{font-size:8px;letter-spacing:1.4px;color:#9da9b7}.side-label{font-size:10px;font-weight:750;letter-spacing:2px;color:#69798a;margin:0 15px 17px}.menu{display:grid;gap:7px}.nav-item{width:100%;background:none;border:1px solid transparent;color:#9cabbd;text-align:left;display:flex;align-items:center;gap:11px;border-radius:9px;padding:13px 11px;font-size:12px;font-weight:600}.nav-icon{width:24px;font-size:21px;line-height:20px;color:#899cb3;text-align:center}.nav-item.active{background:#162f2d;border:1px solid #276754;color:#e2fff4}.nav-item.active .nav-icon{color:#55dfb8}.nav-item i{font-size:6px;margin-left:auto;color:var(--green)}.nav-item:hover{background:#182635;color:white}.sidebar-bottom{margin-top:auto;padding:20px 11px 0}.status{font-size:11px;color:#b3c3ca;padding:13px 10px;border:1px solid #2b3b45;border-radius:8px;background:#15232d}.status i{display:inline-block;border-radius:50%;width:7px;height:7px;margin-right:8px;background:var(--green);box-shadow:0 0 10px var(--green)}.side-small{font-size:9px;color:#617284;letter-spacing:1px;line-height:2;margin-top:19px}.workspace{min-width:0}.topbar{height:73px;border-bottom:1px solid #253342;background:#0e1622;display:flex;align-items:center;justify-content:space-between;padding:0 39px}.breadcrumb{font-size:10px;color:#8294a6;letter-spacing:1.4px;font-weight:650}.breadcrumb span{color:#4f6372;margin:0 12px}.topright{display:flex;align-items:center;gap:21px}.top-tag{font-size:10px;color:var(--green);border:1px solid #31534a;padding:7px 10px;border-radius:5px;letter-spacing:.7px}.top-avatar{width:31px;height:31px;border-radius:8px;display:grid;place-items:center;background:#2c4159;font-size:10px;font-weight:800}.main{padding:38px 42px 30px;max-width:1510px;margin:auto}.intro{display:flex;justify-content:space-between;align-items:flex-end;gap:20px;margin-bottom:30px}.eyebrow{font-weight:750;letter-spacing:2.1px;color:#67cdb6;font-size:10px;display:flex;align-items:center;gap:10px}.eyebrow-line{height:1px;width:22px;background:var(--green)}.eyebrow-number{color:#60798d}.intro h1{font-size:35px;line-height:1.12;letter-spacing:-1.25px;margin:13px 0}.intro p{font-size:12px;color:#96a5b6;line-height:1.7;max-width:690px;margin:0}.dataset-control{min-width:242px;border:1px solid #2a3a48;background:#15202d;border-radius:9px;padding:12px}.dataset-control label{display:block;font-size:9px;color:#7c91a3;letter-spacing:1.4px;font-weight:700;margin:0 0 5px}.dataset-control select{font-size:12px;color:#e2eef2;background:#15202d;border:0;width:100%;font-weight:750;padding:4px 0}.dataset-control small{font-size:10px;color:#8296a4;display:block;padding-top:5px}.content{display:grid;gap:19px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px}.metric,.panel{border:1px solid #263544;background:linear-gradient(155deg,#152131,#101925);border-radius:11px;box-shadow:0 5px 24px #00000010}.metric{min-height:145px;padding:20px 19px}.metric-top{display:flex;justify-content:space-between;align-items:center;gap:9px;color:#a4b0c0;font-size:11px}.metric-icon{font-size:19px;color:#55ae9c}.metric>strong{font-size:28px;line-height:1.5;letter-spacing:-.7px;display:block;margin-top:12px;color:#f4fafb}.metric p{font-size:10px;color:#7f92a5;margin:3px 0}.two-col{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:18px}.panel{min-width:0;padding:22px}.panel-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:20px}.panel h3{margin:0;font-size:14px;letter-spacing:.1px}.panel .panel-head p{font-size:11px;color:#8294a7;margin:7px 0 0;line-height:1.5}.donut-layout{display:grid;grid-template-columns:minmax(145px,210px) 1fr;align-items:center;gap:20px}.donut-layout svg{width:100%}.chart-big{font-size:37px;font-weight:750;fill:#f0f7f9}.chart-small{font-size:11px;fill:#8395a7}.legend{display:grid;gap:14px}.legend>div{display:grid;grid-template-columns:8px 1fr auto;align-items:center;gap:9px;font-size:11px;color:#a7b7c5}.legend i{height:8px;width:8px;border-radius:2px}.legend strong{font-weight:750;color:#e0eaf0}.bar-stack{display:grid;gap:15px}.bar-item{min-width:0}.bar-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:7px;font-size:11px}.bar-meta strong{font-weight:600;color:#c3d3df;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bar-meta span{color:#b3c4d2;font-weight:700;white-space:nowrap}.bar-track{height:8px;width:100%;background:#293747;border-radius:8px;overflow:hidden}.bar-fill{height:100%;border-radius:8px;background:#37c9a9}.color-1{background:#82a1f8}.color-2{background:#d2b180}.color-3{background:#b48af2}.color-4{background:#78bdc8}.chart-header{display:flex;justify-content:space-between;align-items:center;gap:7px;margin-bottom:19px;font-size:11px}.quiet{color:#8a9caf!important}.detail-grid,.focus-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.detail-grid>div,.focus-grid>div{background:#111e2b;border:1px solid #293949;padding:16px;border-radius:7px;min-width:0}.detail-grid span,.focus-grid span{display:block;font-size:11px;color:#8fa2b3;margin-bottom:8px}.detail-grid strong,.focus-grid strong{display:block;font-size:21px;color:#e9f6f4}.help,.disclaimer{font-size:11px;line-height:1.7;color:#90a2b5;border-left:2px solid #406b67;padding-left:12px;margin-top:17px}.tablewrap{width:100%;overflow:auto}table{border-collapse:collapse;width:100%;font-size:11px;text-align:left;white-space:nowrap}th{font-size:10px;letter-spacing:.5px;text-transform:uppercase;color:#7b90a3;font-weight:750}td,th{padding:12px 9px;border-bottom:1px solid #273747}td{color:#bbcad5}td:first-child{font-weight:700;color:#e2ebef}.risk{font-weight:800;border-radius:4px;padding:4px 7px}.risk.low{background:#183d37;color:#75e6bf}.risk.medium{background:#3d3224;color:#e6bd7f}.risk.high{background:#422b31;color:#ff9eaa}.matrix-wrap{overflow:auto}.heatmap{display:grid;grid-template-columns:minmax(160px,1.45fr) repeat(var(--cols),minmax(115px,1fr));gap:6px;min-width:760px;align-items:stretch}.heat-head,.heat-market{font-size:10px;text-transform:uppercase;color:#8da3b6;letter-spacing:.7px;padding:10px 5px}.heat-product{display:flex;align-items:center;padding:10px 7px;font-weight:650;font-size:11px;color:#b8c9d6}.heat-cell{border:1px solid #325346;background:rgba(53,204,165,calc(.11 + .57*var(--heat)));color:#e6fff7;min-height:50px;border-radius:5px;font-size:12px;font-weight:700}.heat-cell:hover,.heat-cell.selected{border-color:#6ee1bd;box-shadow:inset 0 0 0 1px #71d9b6}.heat-key{display:flex;align-items:center;justify-content:flex-end;gap:10px;color:#8a9cab;font-size:10px;margin-top:15px}.heat-scale{height:8px;width:110px;border-radius:8px;background:linear-gradient(to right,#243a3d,#38cba2)}.portfolio-list{display:grid;gap:8px}.prod-row{display:grid;grid-template-columns:37px minmax(130px,1.3fr) minmax(70px,1fr) minmax(75px,.4fr);gap:14px;align-items:center;width:100%;border:1px solid #2b3a48;background:#14202c;border-radius:8px;padding:16px 13px;text-align:left;color:#dce8eb}.prod-row.on{background:#1c3a39;border-color:#5dba9f}.prod-number{color:#74b39f;font-size:13px}.prod-name{font-size:12px;font-weight:750}.prod-name small{font-size:10px;color:#88a3b1;display:block;margin-top:4px;font-weight:450}.prod-track{height:7px;background:#263b49;border-radius:9px;overflow:hidden}.prod-track i{display:block;height:100%;background:#44caac;border-radius:9px}.prod-row strong{font-size:12px;text-align:right}.field{display:grid;gap:9px;margin:0 0 19px;color:#a9b9c6;font-size:11px}.field>span{display:flex;justify-content:space-between;gap:10px}.field>span strong{color:#5ee2bf;font-size:12px}.field select{border:1px solid #35495a;background:#101f2b;border-radius:7px;padding:11px;color:#e7f3f5;width:100%}.rangefield input{width:100%;accent-color:#47cda9}.range-extents{display:flex;justify-content:space-between;color:#8295a7}.impact-big{background:#12302d;border:1px solid #28665c;padding:22px;border-radius:9px;display:grid;gap:8px;margin-bottom:20px}.impact-big span{color:#aaccc4;font-size:11px}.impact-big strong{color:#effdf9;font-size:31px}.impact-down,.neg{color:#f0a396!important}.good{color:#72e2b9!important}.duo{display:flex;gap:11px}.duo>div{flex:1;border:1px solid #304456;border-radius:7px;padding:12px}.duo span{color:#879aab;display:block;font-size:10px;margin-bottom:7px}.duo strong{font-size:17px}.comparebar{height:14px;position:relative;margin:20px 0}.comparebar i{height:13px;position:absolute;left:0;top:0;border-radius:3px}.spaced{margin-top:22px}.button{border:1px solid #45675e;border-radius:7px;padding:10px 15px;background:#19332d;color:#b4f7df;font-size:11px;font-weight:700}.button:hover{background:#285145}.button.primary{background:#39c6a6;color:#07251f}.actions{display:flex;gap:9px;flex-wrap:wrap;margin-top:15px}.upload-zone{border:1px dashed #526a7b;border-radius:10px;padding:29px;text-align:center;background:#11202a;display:flex;flex-direction:column;align-items:center;gap:9px}.upload-symbol{font-size:33px;color:#51d5b3}.upload-zone strong{font-size:15px}.upload-zone p{max-width:550px;color:#9aadb9;font-size:11px;margin:0;line-height:1.7}.file-label{display:inline-flex;margin:10px 0;cursor:pointer}.export-list{display:grid;gap:9px}.export-list button{padding:12px;border:1px solid #304254;background:#122434;border-radius:7px;color:#d7e9ee;text-align:left;display:grid;grid-template-columns:27px 1fr 17px;align-items:center;gap:10px;font-size:11px;font-weight:650}.export-list button:hover{border-color:#62bfa8}.export-icon{font-size:16px;color:#62d1b5}.table-note{font-size:10px;color:#8b9eae}.method-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.method-grid strong{font-size:12px;color:#c9ede3}.method-grid p{font-size:11px;line-height:1.7;color:#98aabc}.disclaimer{border-color:#b68f58}.notice{border:1px solid #276956;background:#133b31;color:#a5f9da;padding:13px;border-radius:8px;font-size:12px;display:flex;align-items:center;justify-content:space-between}.notice.error{background:#3b2229;border-color:#9a535e;color:#ffd6d8}.notice button{color:inherit;border:0;background:transparent;font-size:19px}.footer{display:flex;justify-content:space-between;gap:15px;margin:28px 0 10px;font-size:9px;letter-spacing:1px;color:#607487}.mobile-nav{display:none}@media(max-width:1200px){.shell{grid-template-columns:220px minmax(0,1fr)}.main{padding:29px}.topbar{padding:0 27px}.sidebar{padding-left:9px;padding-right:9px}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.donut-layout{grid-template-columns:minmax(110px,175px) 1fr;gap:12px}}@media(max-width:850px){.shell{display:block}.sidebar{display:none}.topbar{height:59px;padding:0 18px}.breadcrumb{font-size:9px}.top-tag{font-size:9px}.topright{gap:9px}.main{padding:23px 16px 100px}.intro{display:block}.intro h1{font-size:28px}.dataset-control{margin-top:22px;max-width:420px}.two-col{grid-template-columns:1fr}.mobile-nav{display:grid;grid-template-columns:repeat(6,1fr);position:fixed;bottom:0;left:0;right:0;padding:10px 4px 12px;background:#0c1722;border-top:1px solid #314153;z-index:10}.mobile-nav button{border:0;background:none;color:#8599a9;display:grid;gap:2px;justify-items:center;font-size:18px}.mobile-nav small{font-size:9px}.mobile-nav button.active{color:#4cdbb4}.footer{display:block;line-height:2}}@media(max-width:510px){.metrics{gap:9px}.metric{padding:16px 13px;min-height:128px}.metric-top{font-size:10px}.metric>strong{font-size:21px}.metric p{font-size:9px}.panel{padding:16px}.donut-layout{grid-template-columns:150px minmax(0,1fr);gap:8px}.legend{gap:10px}.legend>div{gap:5px;font-size:10px}.detail-grid,.focus-grid{gap:9px}.detail-grid>div,.focus-grid>div{padding:12px}.detail-grid strong,.focus-grid strong{font-size:17px}.prod-row{grid-template-columns:20px minmax(115px,1fr) minmax(62px,.5fr);gap:7px;padding:13px 10px}.prod-track{display:none}.prod-row strong{font-size:11px}.method-grid{grid-template-columns:1fr}.eyebrow{font-size:8px}.intro p{font-size:11px}}
@media(max-width:850px){.footer span{display:block;overflow-wrap:anywhere}}

</style></head><body><div id="app"><div style="padding:60px;font-family:sans-serif">Loading TradeScope…</div></div><script>
'use strict';
const E = (() => {
  const sum=a=>a.reduce((s,x)=>s+x,0);
  const round=(n,d=4)=>Number(n.toFixed(d));
  const safe=(x)=>String(x??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
  const slug=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'');
  function parseCSV(text){
    const lines=[];let row=[],field='',inQ=false;
    text=String(text).replace(/^\uFEFF/,'');
    for(let i=0;i<text.length;i++){
      const ch=text[i];
      if(inQ){if(ch==='"'){if(text[i+1]==='"'){field+='"';i++;}else inQ=false;}else field+=ch;}
      else if(ch==='"'){if(field.trim()!=='')throw Error('Unexpected quote in CSV');inQ=true;}
      else if(ch===','){row.push(field);field='';}
      else if(ch==='\n'||ch==='\r'){if(ch==='\r'&&text[i+1]==='\n')i++;row.push(field);if(row.some(v=>v.trim()))lines.push(row);row=[];field='';}
      else field+=ch;
    }
    if(inQ)throw Error('CSV contains an unclosed quote');
    row.push(field);if(row.some(v=>v.trim()))lines.push(row);
    if(!lines.length)throw Error('Empty CSV');
    const cols=lines[0].map(c=>c.trim().toLowerCase());
    if(new Set(cols).size!==cols.length)throw Error('Duplicate CSV column names');
    if(lines.slice(1).some(r=>r.length!==cols.length))throw Error('Inconsistent number of CSV columns');
    return lines.slice(1).map(r=>Object.fromEntries(cols.map((c,j)=>[c,r[j].trim()])));
  }
  function validate(rows){
    if(!Array.isArray(rows)||rows.length<4||rows.length>400)throw Error('CSV must contain 4 to 400 data rows');
    const required=['product','market','exports_usd_m','tariff_pct','risk_score','growth_pct'];
    const keys=Object.keys(rows[0]||{});for(const k of required)if(!keys.includes(k))throw Error('Missing column: '+k);
    const pairs=new Set();const clean=rows.map((r,i)=>{
      const product=String(r.product||'').trim(), market=String(r.market||'').trim();
      if(!product||!market||product.length>70||market.length>70)throw Error('Invalid product/market in row '+(i+2));
      if(/[<>]/.test(product+market))throw Error('HTML characters not allowed in names');
      const key=product.toLowerCase()+'\0'+market.toLowerCase();if(pairs.has(key))throw Error('Repeated product-market pair in row '+(i+2));pairs.add(key);
      const vals=['exports_usd_m','tariff_pct','risk_score','growth_pct'].map(k=>{const raw=String(r[k]??'').trim();if(!raw||!/^[-+]?(?:\d+\.?\d*|\.\d+)$/.test(raw))throw Error('Invalid number for '+k+' in row '+(i+2));return Number(raw);});
      const [v,t,risk,growth]=vals;
      if(!(v>0&&v<=1e6&&t>=0&&t<=100&&risk>=0&&risk<=100&&growth>=-100&&growth<=200))throw Error('Out-of-range value in row '+(i+2));
      return {product,market,value:v,tariff:t,risk,growth};
    });
    if(new Set(clean.map(r=>r.market)).size<2)throw Error('At least two markets required');
    if(new Set(clean.map(r=>r.product)).size<2)throw Error('At least two products required');
    return clean;
  }
  function fromCSV(text){return validate(parseCSV(text));}
  function group(rows,by){const m=new Map();for(const r of rows){let g=m.get(r[by]);if(!g){g={name:r[by],value:0,weightedRisk:0,weightedTariff:0,weightedGrowth:0,count:0};m.set(r[by],g);}g.value+=r.value;g.weightedRisk+=r.value*r.risk;g.weightedTariff+=r.value*r.tariff;g.weightedGrowth+=r.value*r.growth;g.count++;}return [...m.values()].map(g=>({...g,risk:g.weightedRisk/g.value,tariff:g.weightedTariff/g.value,growth:g.weightedGrowth/g.value})).sort((a,b)=>b.value-a.value||a.name.localeCompare(b.name));}
  function analytics(rows){const total=sum(rows.map(r=>r.value)), markets=group(rows,'market'),products=group(rows,'product');const hhi=a=>sum(a.map(r=>(r.value/total)**2));const mhhi=hhi(markets), phhi=hhi(products);return {total, markets,products, marketHHI:mhhi,productHHI:phhi,marketEffective:1/mhhi,productEffective:1/phhi, marketEntropy:-sum(markets.map(g=>{let p=g.value/total;return p*Math.log(p)})),weightedTariff:sum(rows.map(r=>r.value*r.tariff))/total,weightedRisk:sum(rows.map(r=>r.value*r.risk))/total,weightedGrowth:sum(rows.map(r=>r.value*r.growth))/total};}
  function shock(rows,market,disruption,tariffPp,elasticity=1.5){
    if(!rows.some(r=>r.market===market))throw Error('Unknown market');
    const d=Math.max(0,Math.min(90,Number(disruption)))/100, t=Math.max(0,Math.min(40,Number(tariffPp)));if(!Number.isFinite(d+t))throw Error('Non-finite shock');
    const factor=(1-d)*Math.max(0,1-elasticity*t/100);
    const altered=rows.map(r=>r.market===market?{...r,value:r.value*factor,tariff:Math.min(100,r.tariff+t)}:{...r});
    const before=analytics(rows), after=analytics(altered);return {factor,before,after,loss:before.total-after.total,lossPct:(before.total-after.total)/before.total*100,altered};
  }
  function diversify(rows,sourceMarket,desiredPct,maxNewPct=50){
    if(!rows.some(r=>r.market===sourceMarket))throw Error('Unknown source market');
    const pct=Math.max(0,Math.min(40,Number(desiredPct)))/100;if(!Number.isFinite(pct))throw Error('Invalid shift');
    const altered=rows.map(r=>({...r}));let shifted=0,unallocated=0;
    const sources=altered.filter(r=>r.market===sourceMarket);
    for(const s of sources){let amount=s.value*pct;const targets=altered.filter(r=>r.product===s.product &&r.market!==sourceMarket &&r.risk<s.risk &&r.tariff<=s.tariff+15).map(r=>({r,capacity:r.value*(maxNewPct/100),score:Math.max(0.1,110-r.risk)*Math.max(0.1,1+r.growth/100)/(1+r.tariff/100)})).sort((a,b)=>b.score-a.score);
      const requested=amount;
      // Deterministic greedy allocation by growth-adjusted risk/tariff score; not globally optimal.
      for(const t of targets){const give=Math.min(amount,t.capacity);t.r.value+=give;s.value-=give;shifted+=give;amount-=give;if(amount<1e-10)break;}
      unallocated+=requested- (requested-amount);
    }
    const before=analytics(rows), after=analytics(altered);return {before,after,altered,shifted,requested:sum(sources.map(s=>rows.find(r=>r.product===s.product&&r.market===sourceMarket).value*pct)),unallocated:sum(sources.map(s=>rows.find(r=>r.product===s.product&&r.market===sourceMarket).value*pct))-shifted};
  }
  function csvEscape(v){let s=String(v??'');if(typeof v!=='number' && /^[=+@\-\t\r]/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"';}
  function toCSV(rows){return [['product','market','exports_usd_m','tariff_pct','risk_score','growth_pct'],...rows.map(r=>[r.product,r.market,r.value,r.tariff,r.risk,r.growth])].map(r=>r.map(csvEscape).join(',')).join('\r\n');}
  function demos(){
    const sets=[{key:'andes',title:'Andean Export Portfolio',subtitle:'Agri-food exporters · synthetic',products:['Quinoa','Specialty Coffee','Olive Oil','Dried Fruit','Natural Fibres'],markets:['Brazil','Chile','Germany','United States','Japan'],bases:[45,30,26,35,14],factor:[1.15,1.05,.8,1.35,.65],risk:[27,18,22,38,17],tariff:[6,3,9,11,5],growth:[7,4,2,9,6]},
    {key:'energy',title:'Clean Energy Components',subtitle:'Equipment supply chains · synthetic',products:['Solar Modules','Battery Packs','Smart Sensors','Inverters','Grid Controls'],markets:['Mexico','Spain','India','South Korea','Canada'],bases:[48,42,19,28,35],factor:[1.3,.9,1.4,.85,.75],risk:[36,18,48,25,12],tariff:[12,5,16,8,4],growth:[9,5,13,7,6]},
    {key:'services',title:'Digital Services Exports',subtitle:'B2B software markets · synthetic',products:['Cloud Support','Cyber Audits','UX Services','Data Analytics','Remote Training'],markets:['Argentina','Colombia','United States','United Kingdom','Australia'],bases:[26,31,19,36,21],factor:[.85,1,1.6,1.05,.7],risk:[42,34,20,17,14],tariff:[3,4,1,2,1],growth:[6,8,11,5,7]}];
    return sets.map(s=>({key:s.key,title:s.title,subtitle:s.subtitle,rows:s.products.flatMap((p,i)=>s.markets.map((m,j)=>({product:p,market:m,value:round(s.bases[i]*s.factor[j]*(.66+((i*5+j*3)%7)*.078),2),risk:Math.min(100,s.risk[j]+i*2),tariff:Math.min(100,s.tariff[j]+i%3),growth:s.growth[j]+i-2})))}));
  }
  return {sum,round,safe,slug,parseCSV,validate,fromCSV,group,analytics,shock,diversify,csvEscape,toCSV,demos};
})();
if (typeof module !== 'undefined' && module.exports) module.exports=E;

'use strict';
const DEMOS=E.demos();
const state={dataset:DEMOS[0],active:'overview',market:'Brazil',product:'Quinoa',disruption:25,tariffPp:8,shiftPct:20,view:'market',notice:null};
const ids=['overview','matrix','products','stress','planner','data'];
const labels={overview:'Intelligence Overview',matrix:'Market Matrix',products:'Product Portfolio',stress:'Trade Shock Studio',planner:'Diversification Planner',data:'Data & Methodology'};
const icons={overview:'◈',matrix:'▦',products:'▤',stress:'⚡',planner:'◎',data:'◫'};
const $=sel=>document.querySelector(sel);
const fmt=(n,d=1)=>new Intl.NumberFormat('en-US',{maximumFractionDigits:d,minimumFractionDigits:d}).format(n);
const money=n=>'$'+fmt(n,1)+'M';
const pct=n=>fmt(n,1)+'%';
const esc=E.safe;
const button=(label,action,extra='')=>`<button class="button ${extra}" data-action="${action}">${label}</button>`;
const panel=(title,desc,body,extra='')=>`<section class="panel ${extra}"><div class="panel-head"><div><h3>${title}</h3><p>${desc}</p></div></div>${body}</section>`;
const tag=(a,t='neutral')=>`<span class="tag ${t}">${a}</span>`;
const bars=(items,max,key='value',end='')=>`<div class="bar-stack">${items.map((r,i)=>`<div class="bar-item"><div class="bar-meta"><strong>${esc(r.name)}</strong><span>${end==='percent'?pct(r[key]):money(r[key])}</span></div><div class="bar-track"><div class="bar-fill color-${i%5}" style="width:${Math.max(0.7,100*r[key]/(max||1)).toFixed(2)}%"></div></div></div>`).join('')}</div>`;
const duo=(a,b)=>`<div class="duo"><div><span>Baseline</span><strong>${money(a)}</strong></div><div><span>Scenario</span><strong>${money(b)}</strong></div></div>`;
const rowTable=(cols, rows)=>`<div class="tablewrap"><table><thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table></div>`;
function donut(data,total){let acc=0;const palette=['#37c9a9','#7298ff','#deb875','#ef8e7f','#a49afa','#8094aa'];let rings='';data.forEach((x,i)=>{let len=x.value/total*100;let off=-acc; rings+=`<circle cx="100" cy="100" r="72" fill="none" stroke="${palette[i%palette.length]}" stroke-width="21" stroke-dasharray="${len.toFixed(4)} ${100-len}" stroke-dashoffset="${off.toFixed(4)}" transform="rotate(-90 100 100)" pathLength="100"/>`;acc+=len;});return `<div class="donut-layout"><svg viewBox="0 0 200 200" aria-label="Market shares donut chart" role="img"><circle cx="100" cy="100" r="72" stroke="#263141" fill="none" stroke-width="21"/>${rings}<text x="100" y="96" text-anchor="middle" class="chart-big">${data.length}</text><text x="100" y="116" text-anchor="middle" class="chart-small">markets</text></svg><div class="legend">${data.map((x,i)=>`<div><i style="background:${palette[i%palette.length]}"></i><span>${esc(x.name)}</span><strong>${pct(x.value/total*100)}</strong></div>`).join('')}</div></div>`;}
function lineChart(rows){const max=Math.max(...rows.map(r=>r.value));return `<div class="chart-header"><span>Market export revenue</span><span class="quiet">USD millions · synthetic</span></div>${bars(rows,max)}`;}
function navItem(id){return `<button class="nav-item ${state.active===id?'active':''}" data-nav="${id}"><span class="nav-icon">${icons[id]}</span><span>${labels[id]}</span>${state.active===id?'<i>●</i>':''}</button>`;}
function metrics(a){const list=[['Export portfolio',money(a.total),'Sum of observed trade flows'],['Market concentration',fmt(a.marketHHI*10000,0),'HHI on a 0–10,000 scale'],['Effective markets',fmt(a.marketEffective,2),'Inverse market HHI'],['Weighted risk',fmt(a.weightedRisk,1)+'/100','Export-weighted risk indicator']];return `<div class="metrics">${list.map((r,i)=>`<div class="metric"><div class="metric-top"><span>${r[0]}</span><span class="metric-icon">${['◈','▥','◎','◇'][i]}</span></div><strong>${r[1]}</strong><p>${r[2]}</p></div>`).join('')}</div>`;}
function overview(a,rows){return `${metrics(a)}<div class="two-col">${panel('Market composition','Export shares by destination; synthetic demonstration',donut(a.markets,a.total))}${panel('Destination ranking','Markets ordered by total export value',lineChart(a.markets))}</div><div class="two-col">${panel('Exposure diagnostics','Portfolio-level weighted assumptions',`<div class="detail-grid"><div><span>Weighted tariff</span><strong>${pct(a.weightedTariff)}</strong></div><div><span>Weighted growth</span><strong>${pct(a.weightedGrowth)}</strong></div><div><span>Product HHI</span><strong>${fmt(a.productHHI*10000,0)}</strong></div><div><span>Trade observations</span><strong>${rows.length}</strong></div></div><div class="help">HHI measures concentration. High export value in one market increases exposure to market-specific disruptions.</div>`)}${panel('Market risk dashboard','Assumed risk and tariff parameters, not external live feeds',rowTable(['Market','Risk / 100','Tariff','Share'],a.markets.map(r=>`<tr><td>${esc(r.name)}</td><td>${riskLabel(r.risk)}</td><td>${pct(r.tariff)}</td><td>${pct(r.value/a.total*100)}</td></tr>`)))}</div>`;}
const riskLabel=r=>`<span class="risk ${r>=50?'high':r>=30?'medium':'low'}">${fmt(r,0)}</span>`;
function matrix(a,rows){const prods=[...new Set(rows.map(r=>r.product))],markets=[...new Set(rows.map(r=>r.market))],peak=Math.max(...rows.map(r=>r.value));let matrix=`<div class="matrix-wrap"><div class="heatmap" style="--cols:${markets.length}"><div class="heat-head">Product / market</div>${markets.map(m=>`<div class="heat-market">${esc(m)}</div>`).join('')}${prods.map(p=>`<div class="heat-product">${esc(p)}</div>${markets.map(m=>{const r=rows.find(r=>r.product===p&&r.market===m);const q=r?Math.pow(r.value/peak,.7):0;return `<button class="heat-cell ${state.market===m?'selected':''}" data-market="${esc(m)}" style="--heat:${q.toFixed(3)}" title="${esc(p)} to ${esc(m)}: ${money(r?.value||0)}">${r?fmt(r.value,1):'–'}</button>`;}).join('')}`).join('')}</div></div><div class="heat-key">Low volume <div class="heat-scale"></div> High volume</div>`;
const focus=a.markets.find(m=>m.name===state.market)||a.markets[0];return `${panel('Product × destination trade matrix','Click any cell to inspect that destination; figures in USD millions',matrix)}<div class="two-col">${panel('Selected destination',`Focused view: ${esc(focus.name)}`,`<div class="focus-grid"><div><span>Total export value</span><strong>${money(focus.value)}</strong></div><div><span>Market share</span><strong>${pct(focus.value/a.total*100)}</strong></div><div><span>Risk index</span><strong>${fmt(focus.risk,1)}/100</strong></div><div><span>Mean tariff</span><strong>${pct(focus.tariff)}</strong></div></div>`)}${panel('Top exposures',`Export products shipped to ${esc(focus.name)}`,bars(rows.filter(r=>r.market===focus.name).map(r=>({name:r.product,value:r.value})).sort((x,y)=>y.value-x.value),Math.max(...rows.filter(r=>r.market===focus.name).map(r=>r.value))))}</div>`;}
function products(a,rows){let all=a.products;const focus=all.find(p=>p.name===state.product)||all[0];const ms=rows.filter(r=>r.product===focus.name).sort((a,b)=>b.value-a.value);return `${panel('Product concentration','Export portfolio by product',`<div class="portfolio-list">${all.map((p,i)=>`<button class="prod-row ${p.name===focus.name?'on':''}" data-product="${esc(p.name)}"><span class="prod-number">0${i+1}</span><span class="prod-name">${esc(p.name)}<small>${pct(p.value/a.total*100)} of portfolio</small></span><span class="prod-track"><i style="width:${pct(p.value/all[0].value*100)}"></i></span><strong>${money(p.value)}</strong></button>`).join('')}</div>`)}<div class="two-col">${panel('Selected product',esc(focus.name),`<div class="focus-grid"><div><span>Total exports</span><strong>${money(focus.value)}</strong></div><div><span>Market reach</span><strong>${ms.length}</strong></div><div><span>Weighted growth</span><strong>${pct(focus.growth)}</strong></div><div><span>Weighted risk</span><strong>${fmt(focus.risk,1)}</strong></div></div>`)}${panel('Destination breakdown','Revenue generated in each destination',bars(ms.map(r=>({name:r.market,value:r.value})),ms[0].value))}</div>`;}
function selectMarket(){return `<label class="field"><span>Target destination</span><select id="market-select">${E.group(state.dataset.rows,'market').map(m=>`<option value="${esc(m.name)}" ${m.name===state.market?'selected':''}>${esc(m.name)}</option>`).join('')}</select></label>`;}
function slider(id,name,val,min,max,step=1,suffix='%'){return `<label class="field rangefield"><span>${name}<strong>${val}${suffix}</strong></span><input id="${id}" type="range" min="${min}" max="${max}" step="${step}" value="${val}"><div class="range-extents"><small>${min}${suffix}</small><small>${max}${suffix}</small></div></label>`;}
function stress(a,rows){const s=E.shock(rows,state.market,state.disruption,state.tariffPp);return `<div class="two-col config-col">${panel('Shock assumptions','Adjust synthetic market disruption and incremental tariff',`${selectMarket()}${slider('disruption','Demand disruption',state.disruption,0,80,5)}${slider('tariff','Additional tariff',state.tariffPp,0,30,1,' pp')}<div class="help">Affected market export value is multiplied by (1 − disruption) × max(0, 1 − 1.5 × tariff increase / 100). This is a stylized elasticity assumption, not a demand forecast.</div>`)}${panel('Portfolio impact','Before and after the combined shock',`<div class="impact-big"><span>Estimated export value at risk</span><strong>${money(s.loss)}</strong><span class="impact-down">−${pct(s.lossPct)} of baseline trade value</span></div>${duo(s.before.total,s.after.total)}<div class="comparebar"><i style="width:100%;background:#38465b"></i><i style="width:${100*s.after.total/s.before.total}%;background:#3acaa9"></i></div><div class="help">Only exports in the selected destination are affected. Other markets remain unchanged in this simplified model.</div>`)}</div><div class="two-col">${panel('Market-level outcomes','Export revenue before and after disruption',rowTable(['Destination','Baseline','Stress','Difference'],a.markets.map(m=>{const after=s.after.markets.find(z=>z.name===m.name);return `<tr><td>${esc(m.name)}</td><td>${money(m.value)}</td><td>${money(after?.value||0)}</td><td class="${m.name===state.market?'neg':''}">${m.name===state.market?'−'+money(m.value-(after?.value||0)):'—'}</td></tr>`;})))}${panel('Concentration shift','Recomputed portfolio exposure',`<div class="focus-grid"><div><span>Baseline market HHI</span><strong>${fmt(a.marketHHI*10000,0)}</strong></div><div><span>After-shock market HHI</span><strong>${fmt(s.after.marketHHI*10000,0)}</strong></div><div><span>Baseline risk index</span><strong>${fmt(a.weightedRisk,1)}</strong></div><div><span>After-shock risk index</span><strong>${fmt(s.after.weightedRisk,1)}</strong></div></div><p class="help">Shares and risk scores are recomputed from the adjusted simulated export values.</p>`)}</div>`;}
function planner(a,rows){const p=E.diversify(rows,state.market,state.shiftPct);return `<div class="two-col config-col">${panel('Diversification assumptions','Shift part of this market’s product exports to existing buyers',`${selectMarket()}${slider('shift','Target share to reallocate',state.shiftPct,0,40,5)}<div class="help">For each product, only destinations with a lower assumed risk and no more than 15 percentage points higher tariff qualify. Each destination can absorb at most 50% of its existing product volume. The deterministic greedy allocation favors lower risk and faster growth; it does not claim a global optimum.</div>`)}${panel('Potential reallocation','What the model can redistribute',`<div class="impact-big"><span>Feasible reallocation</span><strong>${money(p.shifted)}</strong><span class="${p.shifted>0?'good':'quiet'}">${pct(100*p.shifted/a.total)} of portfolio</span></div>${duo(p.before.total,p.after.total)}<div class="focus-grid spaced"><div><span>Requested shift</span><strong>${money(p.requested)}</strong></div><div><span>Unallocated</span><strong>${money(p.unallocated)}</strong></div><div><span>Risk score: baseline</span><strong>${fmt(p.before.weightedRisk,1)}</strong></div><div><span>Risk score: proposed</span><strong>${fmt(p.after.weightedRisk,1)}</strong></div></div>`)}</div><div class="two-col">${panel('Proposed destination allocations','Reallocation does not create additional total export value',rowTable(['Destination','Current','Proposed','Change'],a.markets.map(m=>{const n=p.after.markets.find(r=>r.name===m.name);const delta=n.value-m.value;return `<tr><td>${esc(m.name)}</td><td>${money(m.value)}</td><td>${money(n.value)}</td><td class="${delta>0?'good':delta<0?'neg':''}">${delta>0?'+':''}${money(delta)}</td></tr>`;})))}${panel('Decision-quality metrics','Compare concentration and exposure before and after reallocation',`<div class="focus-grid"><div><span>Market HHI (current)</span><strong>${fmt(p.before.marketHHI*10000,0)}</strong></div><div><span>Market HHI (proposed)</span><strong>${fmt(p.after.marketHHI*10000,0)}</strong></div><div><span>Tariff (current)</span><strong>${pct(p.before.weightedTariff)}</strong></div><div><span>Tariff (proposed)</span><strong>${pct(p.after.weightedTariff)}</strong></div></div><div class="help">These are hypothetical shifts. Trade agreements, sales capacity, regulations, transportation and customer acquisition costs are not modeled.</div>`)}</div>`;}
function dataMethods(a,rows){return `${panel('Import trade-flow data','Local CSV only. Your uploaded files never leave this browser.',`<div class="upload-zone"><span class="upload-symbol">⇧</span><strong>Import your own export flows</strong><p>CSV: product, market, exports_usd_m, tariff_pct, risk_score, growth_pct</p><label class="button primary file-label">Select CSV<input id="file-import" type="file" accept=".csv,text/csv" hidden></label><p class="quiet">4–400 rows · 2+ products · 2+ destinations · one row per product-market pair</p></div><div class="actions">${button('Download CSV template','template')}${button('Restore demo dataset','reset')}</div>`)}<div class="two-col">${panel('Data preview','Current active trade-flow observations',rowTable(['Product','Destination','Exports (USD M)','Risk'],rows.slice(0,12).map(r=>`<tr><td>${esc(r.product)}</td><td>${esc(r.market)}</td><td>${money(r.value)}</td><td>${fmt(r.risk,0)}</td></tr>`))+`<p class="table-note">Displaying 12 of ${rows.length} rows</p>`)}${panel('Reports & exports','All reports describe the selected dataset and current scenario assumptions',`<div class="export-list">${[['Download portfolio CSV','dataset'],['Download JSON audit','json'],['Download Markdown report','markdown'],['Download stress scenario CSV','stress-csv'],['Download diversification CSV','diversification-csv']].map((x,i)=>`<button data-action="${x[1]}"><span class="export-icon">${['▦','{}','≡','↯','◎'][i]}</span><span>${x[0]}</span><span>↓</span></button>`).join('')}</div>`)}</div>${panel('Methods, validation and limitations','Transparent formulas and responsible interpretation',`<div class="method-grid"><div><strong>Market HHI</strong><p>Σ(market exports / all exports)². Displayed on 0–10,000 scale. Effective markets = 1 / HHI.</p></div><div><strong>Weighted exposure</strong><p>Weighted average risk, tariff and growth assumptions based on each observed export flow.</p></div><div><strong>Shock simulator</strong><p>Market-specific volume loss and incremental-tariff response use a stylized elasticity factor of 1.5.</p></div><div><strong>Diversification</strong><p>Greedy lower-risk redistribution with per-destination volume caps. Not an exact global optimization.</p></div></div><p class="disclaimer">All bundled examples are synthetic and no actual export statistics, official risk scores or up-to-date tariff schedules are supplied. Import data from verified sources before relying on results. The application is educational and does not provide investment, financial or legal advice.</p>`)}`;}
function content(a,rows){switch(state.active){case 'overview':return overview(a,rows);case 'matrix':return matrix(a,rows);case 'products':return products(a,rows);case 'stress':return stress(a,rows);case 'planner':return planner(a,rows);case 'data':return dataMethods(a,rows);}}
function render(){const rows=state.dataset.rows,a=E.analytics(rows);const markets=a.markets.map(m=>m.name),products=a.products.map(p=>p.name);if(!markets.includes(state.market))state.market=markets[0];if(!products.includes(state.product))state.product=products[0];
  $('#app').innerHTML=`<div class="shell"><aside class="sidebar"><a class="brand" href="#" data-nav="overview"><div class="brand-icon">T<span>·</span></div><div><strong>TradeScope<span class="period">.</span></strong><small>GLOBAL COMMERCE INTELLIGENCE</small></div></a><div class="side-label">WORKSPACE</div><div class="menu">${ids.map(navItem).join('')}</div><div class="sidebar-bottom"><div class="status"><i></i> Offline analytical engine</div><div class="side-small">PROJECT 47 · RESEARCH LAB<br>© 2026 TradeScope · MIT</div></div></aside><div class="workspace"><header class="topbar"><div class="breadcrumb">RESEARCH WORKSPACE <span>/</span> ${labels[state.active].toUpperCase()}</div><div class="topright"><span class="top-tag">● LOCAL-FIRST</span><span class="top-avatar">TS</span></div></header><main class="main"><div class="intro"><div><div class="eyebrow"><span class="eyebrow-line"></span> GLOBAL COMMERCE ANALYTICS <span class="eyebrow-number">// 047</span></div><h1>${labels[state.active]}</h1><p>${state.active==='overview'?'Measure market concentration, export exposure and commercial resilience from transparent, reproducible inputs.':{matrix:'Explore product-market relationships, revenue distribution and destination exposure.',products:'Track the mix of exports and where each product generates value.',stress:'Test stylized market disruptions and tariff changes against your portfolio.',planner:'Model feasible redistribution of existing exports toward less exposed destinations.',data:'Inspect assumptions, import your own CSV files and export an audit trail.'}[state.active]}</p></div><div class="dataset-control"><label>ACTIVE DATASET</label><select id="dataset-select">${DEMOS.map(d=>`<option value="${d.key}" ${state.dataset.key===d.key?'selected':''}>${esc(d.title)}</option>`).join('')}${state.dataset.key==='custom'?`<option value="custom" selected>Imported CSV dataset</option>`:''}</select><small>${esc(state.dataset.subtitle)}</small></div></div>${state.notice?`<div class="notice ${state.notice.type}"><span>${state.notice.type==='error'?'⚠':'✓'} ${esc(state.notice.text)}</span><button data-action="dismiss" aria-label="Dismiss message">×</button></div>`:''}<div class="content">${content(a,rows)}</div><footer class="footer"><span>TRADESCOPE · SYNTHETIC DATA FOR DEMONSTRATION</span><span>Private by design · Browser-only execution</span></footer></main></div></div><div class="mobile-nav">${ids.map(id=>`<button data-nav="${id}" class="${state.active===id?'active':''}" title="${labels[id]}"><span>${icons[id]}</span><small>${['Home','Matrix','Mix','Stress','Plan','Data'][ids.indexOf(id)]}</small></button>`).join('')}</div>`;
}
function report(){const a=E.analytics(state.dataset.rows);const stress=E.shock(state.dataset.rows,state.market,state.disruption,state.tariffPp);const plan=E.diversify(state.dataset.rows,state.market,state.shiftPct);return {application:'TradeScope Project 47',version:'1.0.0',dataset:state.dataset.title,synthetic:state.dataset.key!=='custom',assumptions:{market:state.market,disruption_pct:state.disruption,tariff_increase_pp:state.tariffPp,elasticity:1.5,diversification_request_pct:state.shiftPct,max_destination_volume_increase_pct:50},summary:{total_exports_usd_m:E.round(a.total),market_hhi_10000:E.round(a.marketHHI*10000),effective_markets:E.round(a.marketEffective),weighted_risk:E.round(a.weightedRisk),weighted_tariff_pct:E.round(a.weightedTariff),markets:a.markets,products:a.products},stress:{loss_usd_m:E.round(stress.loss),loss_pct:E.round(stress.lossPct)},diversification:{requested_usd_m:E.round(plan.requested),feasible_usd_m:E.round(plan.shifted),unallocated_usd_m:E.round(plan.unallocated),before_risk:E.round(a.weightedRisk),after_risk:E.round(plan.after.weightedRisk)},methodology:'Deterministic export-share HHI, linear stylized tariff response, and rule-based diversification. Not real-time data, not predictive.',data:state.dataset.rows};}
function download(content,name,type){const blob=new Blob([content],{type});const u=URL.createObjectURL(blob);const a=document.createElement('a');a.href=u;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(u),10000);}
function downloadAction(action){const rows=state.dataset.rows;const stem='tradescope-'+E.slug(state.dataset.key);const csv=arr=>E.toCSV(arr.map(r=>({...r,product:r.product,market:r.market})));
  if(action==='template'){download('product,market,exports_usd_m,tariff_pct,risk_score,growth_pct\r\n"Quinoa","Chile",25,5,20,6\r\n"Quinoa","Germany",15,9,24,4\r\n"Olive Oil","Chile",12,6,20,5\r\n"Olive Oil","Germany",8,8,24,3', 'tradescope-template.csv','text/csv');return;}
  if(action==='dataset'){download(E.toCSV(rows),stem+'-portfolio.csv','text/csv');return;}
  if(action==='json'){download(JSON.stringify(report(),null,2),stem+'-audit.json','application/json');return;}
  if(action==='markdown'){const r=report();const str=`# TradeScope analytical report\n\nDataset: ${r.dataset}\nSynthetic bundled data: ${r.synthetic?'Yes':'No, user imported'}\n\n## Portfolio\n- Total trade: ${money(r.summary.total_exports_usd_m)}\n- Market HHI: ${fmt(r.summary.market_hhi_10000,0)}\n- Effective markets: ${fmt(r.summary.effective_markets,2)}\n- Weighted risk: ${fmt(r.summary.weighted_risk,1)}/100\n\n## Stress scenario\n- Destination: ${r.assumptions.market}\n- Demand disruption: ${r.assumptions.disruption_pct}%\n- Additional tariff: ${r.assumptions.tariff_increase_pp} pp\n- Modeled loss: ${money(r.stress.loss_usd_m)} (${pct(r.stress.loss_pct)})\n\n## Diversification\n- Requested reallocation: ${money(r.diversification.requested_usd_m)}\n- Feasible reallocation: ${money(r.diversification.feasible_usd_m)}\n- Unallocated request: ${money(r.diversification.unallocated_usd_m)}\n\n## Limitations\n- ${r.methodology}\n`;download(str,stem+'-report.md','text/markdown');return;}
  if(action==='stress-csv'){download(csv(E.shock(rows,state.market,state.disruption,state.tariffPp).altered),stem+'-stress.csv','text/csv');return;}
  if(action==='diversification-csv'){download(csv(E.diversify(rows,state.market,state.shiftPct).altered),stem+'-reallocation.csv','text/csv');return;}
}
function notice(text,type='success'){state.notice={type,text};render();}
$('#app').addEventListener('click',e=>{const nav=e.target.closest('[data-nav]');if(nav){e.preventDefault();state.active=nav.dataset.nav;state.notice=null;render();window.scrollTo(0,0);return;}const mark=e.target.closest('[data-market]');if(mark){state.market=mark.dataset.market;render();return;}const prod=e.target.closest('[data-product]');if(prod){state.product=prod.dataset.product;render();return;}const b=e.target.closest('[data-action]');if(!b)return;const action=b.dataset.action;if(action==='dismiss'){state.notice=null;render();return;}if(action==='reset'){state.dataset=DEMOS[0];state.market=state.dataset.rows[0].market;state.product=state.dataset.rows[0].product;notice('Demo dataset restored.');return;}try{downloadAction(action);}catch(err){notice(err.message,'error');}});
$('#app').addEventListener('change',e=>{const id=e.target.id;const val=e.target.value;if(id==='dataset-select'){const next=DEMOS.find(d=>d.key===val);if(next){state.dataset=next;state.market=E.group(next.rows,'market')[0].name;state.product=E.group(next.rows,'product')[0].name;state.notice=null;render();}}else if(id==='market-select'){state.market=val;render();}else if(id==='disruption'){state.disruption=Number(val);render();}else if(id==='tariff'){state.tariffPp=Number(val);render();}else if(id==='shift'){state.shiftPct=Number(val);render();}else if(id==='file-import'){const f=e.target.files?.[0];if(!f)return;if(f.size>350000){notice('CSV file is too large (maximum 350 KB).','error');return;}const reader=new FileReader();reader.onerror=()=>notice('Unable to read CSV.','error');reader.onload=()=>{try{const parsed=E.fromCSV(reader.result);state.dataset={key:'custom',title:'Imported CSV dataset',subtitle:'User-provided local data',rows:parsed};state.market=E.group(parsed,'market')[0].name;state.product=E.group(parsed,'product')[0].name;notice(`Successfully imported ${parsed.length} trade flows.`);}catch(err){notice(err.message,'error');}};reader.readAsText(f,'utf-8');}});
render();

</script></body></html>