import {createGlobe} from './globe.mjs';
import {DEFAULTS,STRATEGIES,evaluate,landscape,recommendation,normalizeState} from './model.mjs';
import {RAMP,rampColor,scaleFraction} from './globe-math.mjs';
import {observationUrl,parseObservations,compareSeries,UNDC_REST} from './undc.mjs';
const $=id=>document.getElementById(id);
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const fmt=(n,d=0)=>Number.isFinite(n)?new Intl.NumberFormat('en-US',{maximumFractionDigits:d}).format(n):'Not available';
const compact=n=>Number.isFinite(n)?new Intl.NumberFormat('en-US',{notation:'compact',maximumFractionDigits:1}).format(n):'Not available';
const money=n=>`R$${n>=1e6?fmt(n/1e6,2)+'m':fmt(n)}`;
const shortNames={current:'Current service',theatre:'Surgical capacity',referral:'Referral programme',combined:'Combined package'};
const colors=Object.fromEntries(STRATEGIES.map(s=>[s.id,s.color]));
const S=(tag,attrs={},text='')=>{const e=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const[k,v]of Object.entries(attrs))e.setAttribute(k,v);if(text)e.textContent=text;return e;};
const draw=(svg,tag,attrs,text)=>{const e=S(tag,attrs,text);svg.append(e);return e;};
const REGION_COLORS={'East Asia & Pacific':'#4284b8','Europe & Central Asia':'#8876a7','Latin America & Caribbean':'#147d70','Middle East, North Africa, Afghanistan & Pakistan':'#bd7842','North America':'#2b4a50','South Asia':'#c9a227','Sub-Saharan Africa':'#a0443c'};
const regionColor=r=>REGION_COLORS[r]||'#5c706f';
const DEFAULT_VIEW={country:'BRA',year:'latest',metric:'sao',globeMetric:'sao',region:'all'};
let globe=null,overlayOn=false,overlaySurface='motorized',globeMetric=DEFAULT_VIEW.globeMetric,globeAutoRotate=!matchMedia('(prefers-reduced-motion: reduce)').matches;
let live={},liveBusy=false;
let data,params={...DEFAULTS},activeTab='world',country=DEFAULT_VIEW.country,year=DEFAULT_VIEW.year,metric=DEFAULT_VIEW.metric,region=DEFAULT_VIEW.region,tableSort={key:'name',dir:1},evidenceFilter='all',livePapers=null,liveSearchMeta=null,toastTimer,searchController;
function toast(message){$('toast').textContent=message;$('toast').classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('toast').classList.remove('show'),3500);}
function urlState(){const p=new URLSearchParams({view:activeTab,country,year:String(year),metric,globeMetric,region,category:evidenceFilter,q:$('paperSearch').value.trim()});if(overlayOn)p.set('overlay',overlaySurface);for(const k of ['budget','capacity','referralGain','arrivals','unitCost'])p.set(k,String(params[k]));return '#'+p;}
function saveState(push=false){try{history[push?'pushState':'replaceState'](null,'',urlState());}catch{}}
function readState(){const p=new URLSearchParams(location.hash.slice(1));activeTab=['world','evidence','landscape'].includes(p.get('view'))?p.get('view'):'world';country=data?.countries.some(c=>c.id===p.get('country'))?p.get('country'):DEFAULT_VIEW.country;const y=p.get('year');year=y==='latest'||y===null?'latest':(Number.isInteger(Number(y))&&Number(y)>=data.yearRange[0]&&Number(y)<=data.yearRange[1]?Number(y):'latest');metric=metricKeys().includes(p.get('metric'))?p.get('metric'):DEFAULT_VIEW.metric;globeMetric=metricKeys().includes(p.get('globeMetric'))?p.get('globeMetric'):DEFAULT_VIEW.globeMetric;region=regions().includes(p.get('region'))?p.get('region'):'all';overlayOn=['motorized','walking'].includes(p.get('overlay'));if(overlayOn)overlaySurface=p.get('overlay');evidenceFilter=['capacity','access','financing'].includes(p.get('category'))?p.get('category'):'all';$('paperSearch').value=p.get('q')||'';document.querySelectorAll('[data-evidence]').forEach(b=>b.classList.toggle('active',b.dataset.evidence===evidenceFilter));let input={};for(const k of ['budget','capacity','referralGain','arrivals','unitCost'])if(p.has(k)&&Number.isFinite(Number(p.get(k))))input[k]=Number(p.get(k));params=normalizeState(input);}
function setTab(tab,push=true){activeTab=tab;document.body.dataset.view=tab;globe?.setActive(tab==='world');document.querySelectorAll('.view').forEach(v=>{v.hidden=v.id!==tab;v.classList.toggle('active',v.id===tab);});document.querySelectorAll('[data-tab]').forEach(b=>{b.classList.toggle('active',b.dataset.tab===tab);if(b.dataset.tab===tab)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current');});$('viewStatus').innerHTML=tab==='landscape'?'ILLUSTRATIVE SCENARIO12-month planning model · not a policy forecast':tab==='world'?`PUBLIC OBSERVATIONS${data.countries.length} economies · ${year==='latest'?'latest available observation per economy':'fixed year '+year} · missing values preserved${Object.keys(live).length?`LIVE · UN DATA COMMONS${Object.keys(live).map(k=>def(k).short).join(', ')} retrieved ${new Date(Math.max(...Object.values(live).map(l=>l.retrievedAt))).toISOString().slice(11,16)} UTC`:''}`:'RETRIEVED SOURCESDiscovery leads · not approved model parameters';$('saveChart').hidden=tab!=='landscape';if(tab==='world')renderWorld();if(tab==='evidence')renderEvidence();if(tab==='landscape')renderModel();const off=$('liveOff');if(off)off.onclick=()=>{live={};setTab(activeTab,false);toast('Snapshot values restored.');};saveState(push);}
function bindControls(){for(const k of ['budget','capacity','referralGain','arrivals','unitCost'])$(k).value=params[k];$('budgetValue').textContent=money(params.budget);$('capacityValue').textContent=fmt(params.capacity);$('referralValue').textContent='+'+fmt(params.referralGain*100)+' pp';}
function renderModel(){bindControls();const results=evaluate(params),winner=recommendation(results);renderTerrain();renderFlow(winner);renderComparison(results,winner);const ties=winner.tiedIds.filter(id=>id!==winner.id);$('decisionCallout').innerHTML=`
${ties.length?'Same outcome and spending as '+ties.map(id=>esc(shortNames[id])).join(', ')+'. ':''}${winner.id==='current'?'No modeled throughput gain from the additional investment.':'Highest modeled throughput; not a policy recommendation.'}
+${fmt(winner.incremental,1)}expected procedures / year
`;
const capBinds=winner.bottleneck.includes('capacity'),refBinds=winner.bottleneck.includes('Referral');$('nextQuestion').textContent=capBinds?'How much staffed capacity is actually usable?':refBinds?'How much can referral throughput really improve?':'Which resource would constrain the next expansion?';$('nextReason').textContent=capBinds?'Measure staffed theatre time and cancellations. A registered facility or bed count cannot answer this question.':refBinds?'Measure referral completion and losses along the pathway. A promising study from another setting is not a local effect estimate.':'Audit operating funds, demand and implementation delays before treating this scenario as a real investment case.';}
function renderTerrain(){const svg=$('terrain');if(!svg.clientWidth)return;const W=Math.max(300,svg.clientWidth),H=svg.clientHeight||430,m={l:48,r:22,t:24,b:58},pw=W-m.l-m.r,ph=H-m.t-m.b,cols=43,rows=31;svg.replaceChildren();svg.setAttribute('viewBox',`0 0 ${W} ${H}`);draw(svg,'title',{id:'terrainTitle'},'Decision landscape: strategy with greatest modeled additional completions');draw(svg,'desc',{id:'terrainDesc'},'Horizontal axis usable monthly surgical capacity, 40 to 180 slots. Vertical axis assumed referral gain, 0 to 40 percentage points. Use the labeled sliders to select an exact point. Colors indicate strategies, not probabilities.');const cells=landscape(params,cols,rows),cw=pw/cols,ch=ph/rows;
for(const c of cells)draw(svg,'rect',{x:m.l+c.col*cw,y:m.t+(rows-1-c.row)*ch,width:cw+.2,height:ch+.2,fill:c.color,opacity:c.winnerId==='current'?'.3':'.84'});
for(let i=1;im.l+(v-40)/140*pw,y=v=>m.t+ph-v/.4*ph;
for(const t of [40,80,120,160,180]){draw(svg,'line',{x1:x(t),x2:x(t),y1:m.t+ph,y2:m.t+ph+5,stroke:'#9fb7af'});draw(svg,'text',{x:x(t),y:m.t+ph+20,fill:'#c6d6cf','font-size':10,'text-anchor':'middle'},String(t));}
for(const t of [0,.1,.2,.3,.4]){draw(svg,'text',{x:m.l-10,y:y(t)+4,fill:'#c6d6cf','font-size':10,'text-anchor':'end'},'+'+Math.round(t*100));}
draw(svg,'text',{x:m.l+pw/2,y:H-11,fill:'#d2e5da','font-size':11,'text-anchor':'middle'},'Usable surgical capacity · slots / month');draw(svg,'text',{transform:`translate(15 ${m.t+ph/2}) rotate(-90)`,fill:'#d2e5da','font-size':10,'text-anchor':'middle'},'Referral improvement · percentage points');
// Labels describe regions, never data-derived causal mechanisms.
for(const s of STRATEGIES){const subset=cells.filter(c=>c.winnerId===s.id);if(subset.length<45)continue;let candidates=subset.filter(c=>c.col>3&&c.col3&&c.row{const r=svg.getBoundingClientRect(),xx=e.clientX-r.left,yy=e.clientY-r.top;if(xxW-m.r||yyH-m.b)return;params.capacity=Math.round(40+(xx-m.l)/pw*140);params.referralGain=Math.round((1-(yy-m.t)/ph)*40)/100;renderModel();saveState();};}
function renderFlow(winner){const m=winner.monthly[0];const stages=[['Eligible arrivals',params.arrivals,'Demand'],['Referral throughput',m.referralThroughput,'Referral flow'],['Staffed slots',m.capacity,'Staffed capacity'],['Funded slots',m.fundedSlots,'Operating funds']];$('flow').innerHTML=`
${fmt(m.completed,1)} expected completions / month under the selected ${esc(shortNames[winner.id].toLowerCase())}. Unresolved at 12 months: ${fmt(winner.queue,1)} cases.
`;}
function renderComparison(results,winner){const max=Math.max(...results.map(r=>r.incremental),1);$('comparison').innerHTML=results.map(r=>`
* Highest-ranked under these assumptions. End-of-horizon unresolved cases: ${fmt(winner.queue)} versus ${fmt(results[0].queue)} under current service.
`;}
/* ---------- World lens: all economies ---------- */
function metricKeys(){return Object.keys(data?.indicators||{}).filter(k=>k!=='population');}
function regions(){return ['all',...new Set((data?.countries||[]).map(c=>c.region))];}
function def(key){return data.indicators[key]||{short:key,unit:'',decimals:1,scale:'linear'};}
/** Observation for an economy and indicator under the current year mode: {value, year} or null. */
function obs(c,key,y=year){const s=live[key]?.series?.[c.id]||c.series?.[key];if(!s||!s.length)return null;if(y==='latest'){const [yy,v]=s[s.length-1];return {value:v,year:yy};}const hit=s.find(p=>p[0]===Number(y));return hit?{value:hit[1],year:hit[0]}:null;}
function value(c,key,y=year){return obs(c,key,y)?.value??null;}
function fmtValue(key,v){const d=def(key);if(!Number.isFinite(v))return 'Not available';if(key==='population')return compact(v);if(d.scale==='log'&&v>=10000)return fmt(v,0);return fmt(v,d.decimals??1);}
function yearLabel(){return year==='latest'?'latest available':String(year);}
function visibleCountries(){return data.countries.filter(c=>region==='all'||c.region===region);}
function metricOptions(selected,includePopulation=false){return data.indicatorGroups.map(g=>``).join('');}
function targetProgress(key,v){const d=def(key);if(d.target===undefined||d.target===null||!Number.isFinite(v))return '';const better=d.better;let frac;if(d.target===0)frac=Math.max(0,Math.min(1,1-v/100));else frac=Math.max(0,Math.min(1,v/d.target));const met=d.target===0?v<=0.5:(better==='higher'?v>=d.target:v<=d.target);return `
${esc(d.targetLabel)}${met?' · met':''}`;}
function renderWorld(){if(!data)return;$('year').value=String(year);$('worldMetric').value=metric;$('regionFilter').value=region;syncGlobe();renderDetail();renderScatter();renderTable();}
function renderDetail(){const c=data.countries.find(c=>c.id===country)||data.countries[0];const pop=obs(c,'population','latest');
$('countryDetail').innerHTML=`
SELECTED SYSTEM · ${esc(yearLabel().toUpperCase())}
${esc(c.name)}
${esc(c.region)} · ${esc(c.income)}${pop?` · ${compact(pop.value)} people (${pop.year})`:''}
${esc(def(k).short)}${o?fmtValue(k,o.value):'—'}${esc(def(k).unit)} · ${o?o.year:'no observation'+(year!=='latest'&&c.series[k]?.length?' in '+year+' · latest '+c.series[k][c.series[k].length-1][0]:'')}${o?targetProgress(k,o.value):''}${crossLine(c,k)}
`;}).join('')}
`).join('')}${undcKeys().length?`
SOURCE TRIANGULATION
Latest UN System Data Commons values are stored beside World Bank/WHO series for ${Object.values(data.indicators).filter(d=>d.undc?.variable||d.undc?.derived).length} indicators (${undcKeys().length} refreshable live).
`:''}
Country selection changes context only. It does not calibrate the illustrative scenario.
`;
const lb=$('liveCountry');if(lb)lb.onclick=()=>liveCheckCountry(c);}
function undcKeys(){return Object.keys(data.indicators).filter(k=>data.indicators[k].undc?.variable);}
function crossLine(c,k){const u=def(k).undc;if(!u)return '';if(u.status==='no-counterpart')return `No UN Data Commons counterpart`;if(!u.variable&&!u.derived)return '';const l=live[k]?.series?.[c.id];const snap=c.series[k]||[];const cc=c.crosscheck?.[k];const tag=u.derived?'UN Data Commons (derived from WHO head-counts)':'UN Data Commons';
if(l){const r=compareSeries(snap,l);return `UN Data Commons live: ${fmtValue(k,l[l.length-1][1])} (${l[l.length-1][0]}) · ${l.length} obs · ${r.status==='agree'?'agrees with snapshot at '+r.year:r.status==='differ'?'differs from snapshot at '+r.year+' ('+fmtValue(k,r.snapshot)+')':'no common year'}`;}
if(!cc)return `${tag}: no series for this economy`;const [[yy,vv],n]=cc;const sl=snap[snap.length-1];let verdict='';
if(sl){if(yy>sl[0])verdict=' · newer than snapshot ('+sl[0]+')';else if(yy===sl[0])verdict=Math.abs(vv-sl[1])/Math.max(Math.abs(sl[1]),1e-9)<=.05?' · agrees':' · differs ('+fmtValue(k,sl[1])+' in snapshot)';else verdict=' · snapshot newer ('+sl[0]+')';}
return `${tag}: ${fmtValue(k,vv)} (${yy}) · ${n} obs${verdict}`;}
async function liveCheckCountry(c){const st=$('liveCountryStatus');liveBusy=true;$('liveCountry').disabled=true;st.textContent='Querying the UN System Data Commons V2 observation endpoint…';
try{const keys=undcKeys();const results=await Promise.all(keys.map(async k=>{const u=def(k).undc;const r=await fetch(observationUrl(u.variable,{iso3:[c.id],date:''}),{signal:AbortSignal.timeout(20000)});if(!r.ok)throw Error('HTTP '+r.status);const j=await r.json();return [k,parseObservations(j,u.variable,u.factor)[c.id]||null];}));
const now=Date.now();for(const [k,ser] of results){if(!ser)continue;live[k]=live[k]||{series:{},retrievedAt:now,scope:'country'};live[k].series[c.id]=ser;live[k].retrievedAt=now;}
liveBusy=false;renderWorld();setTab(activeTab,false);$('liveCountryStatus').textContent=`Live retrieval ${new Date(now).toISOString().slice(11,19)} UTC for ${c.name}: ${results.filter(r=>r[1]).length} of ${keys.length} series returned. Values marked "live" come from the UN System Data Commons, not from the snapshot.`;toast('Live UN Data Commons values loaded for '+c.name+'.');}
catch(e){liveBusy=false;const b=$('liveCountry');if(b)b.disabled=false;st.textContent='Live retrieval failed ('+e.message+'). Snapshot values are unchanged.';}}
async function liveRecolour(){const k=globeMetric,u=def(k).undc;if(!u?.variable)return;const btn=$('liveGlobe');btn.disabled=true;btn.textContent='Retrieving…';
try{const r=await fetch(observationUrl(u.variable,{date:''}),{signal:AbortSignal.timeout(30000)});if(!r.ok)throw Error('HTTP '+r.status);const j=await r.json();const series=parseObservations(j,u.variable,u.factor);const n=Object.keys(series).length;if(!n)throw Error('no country series in response');
live[k]={series,retrievedAt:Date.now(),scope:'all'};renderWorld();setTab(activeTab,false);toast(`${def(k).short}: ${n} economies recoloured from the UN System Data Commons (live).`);}
catch(e){toast('Live retrieval failed: '+e.message+'. Snapshot values are unchanged.');}
finally{const b=$('liveGlobe');if(b){b.disabled=false;b.textContent=live[globeMetric]?.scope==='all'?'Live values shown · refresh ↗':'Recolour from UN Data Commons (live) ↗';}}}
function renderScatter(){const rows=visibleCountries().map(c=>({c,x:obs(c,'spending'),y:obs(c,metric),p:value(c,'population','latest')})),valid=rows.filter(r=>r.x&&r.x.value>0&&r.y&&Number.isFinite(r.y.value)&&(def(metric).scale!=='log'||r.y.value>0));const d=def(metric);
const W=Math.max(310,$('worldChart').clientWidth),H=W<500?380:440,m={l:56,r:24,t:26,b:60},pw=W-m.l-m.r,ph=H-m.t-m.b;
const svg=S('svg',{viewBox:`0 0 ${W} ${H}`,class:'world-svg',role:'img','aria-label':`Health expenditure per capita versus ${d.short} (${yearLabel()}); each circle is one economy; correlation is not causation.`});$('worldChart').replaceChildren(svg);
if(!valid.length){draw(svg,'text',{x:W/2,y:H/2,'text-anchor':'middle',fill:'#657a72','font-size':12},'No economies have both observations for this selection.');$('worldMissing').textContent='';return;}
const xs=valid.map(r=>r.x.value),ys=valid.map(r=>r.y.value);
const xmin=Math.log10(Math.min(...xs))-.08,xmax=Math.log10(Math.max(...xs))+.08;
let ymin,ymax;if(d.scale==='log'){ymin=Math.log10(Math.min(...ys))-.05;ymax=Math.log10(Math.max(...ys))+.05;}else{const lo=Math.min(...ys),hi=Math.max(...ys),pad=(hi-lo||1)*.06;ymin=Math.max(d.unit.startsWith('%')?0:-Infinity,lo-pad);ymax=hi+pad;if(d.target&&d.target>0)ymax=Math.max(ymax,d.target*1.05);}
const x=v=>m.l+(Math.log10(v)-xmin)/(xmax-xmin||1)*pw,y=v=>m.t+ph-((d.scale==='log'?Math.log10(v):v)-ymin)/(ymax-ymin||1)*ph;
const yticks=d.scale==='log'?logTicks(ymin,ymax):niceTicks(ymin,ymax,6);
for(const t of yticks){draw(svg,'line',{x1:m.l,x2:W-m.r,y1:y(t),y2:y(t),stroke:'#dce3d8','stroke-width':.7});draw(svg,'text',{x:m.l-8,y:y(t)+3,'text-anchor':'end',fill:'#657a72','font-size':10},fmtTick(t));}
for(const t of logTicks(xmin,xmax)){draw(svg,'line',{x1:x(t),x2:x(t),y1:m.t,y2:m.t+ph,stroke:'#eef1ea','stroke-width':.7});draw(svg,'text',{x:x(t),y:H-m.b+20,'text-anchor':'middle',fill:'#657a72','font-size':10},'$'+fmtTick(t));}
if(Number.isFinite(d.target)&&d.target>0){draw(svg,'line',{x1:m.l,x2:W-m.r,y1:y(d.target),y2:y(d.target),stroke:'#bd7842','stroke-dasharray':'5 4','stroke-width':1});draw(svg,'text',{x:W-m.r,y:y(d.target)-5,'text-anchor':'end',fill:'#9a5a28','font-size':9},'LCoGS 2030 target');}
draw(svg,'text',{x:m.l,y:13,fill:'#4b675e','font-size':10},`${d.short} · ${d.unit}${d.scale==='log'?' (log scale)':''}`);draw(svg,'text',{x:m.l+pw/2,y:H-10,'text-anchor':'middle',fill:'#4b675e','font-size':10},'Current health expenditure per person · US$ (log scale)');
const popMax=Math.max(...valid.map(r=>r.p||0),1);
const ordered=valid.sort((a,b)=>(b.p||0)-(a.p||0)).sort((a,b)=>(a.c.id===country?1:0)-(b.c.id===country?1:0));
for(const r of ordered){const cx=x(r.x.value),cy=y(r.y.value),selected=r.c.id===country,rad=selected?9:3+Math.sqrt((r.p||0)/popMax)*9;const g=S('g',{role:'button',tabindex:0,'aria-label':`${r.c.name}: ${fmt(r.x.value)} dollars per person (${r.x.year}), ${fmtValue(metric,r.y.value)} ${d.unit} (${r.y.year}). Select economy.`});svg.append(g);const col=regionColor(r.c.region);draw(g,'circle',{cx,cy,r:Math.max(rad,7),fill:'transparent'});draw(g,'circle',{cx,cy,r:rad,fill:selected?'#147d70':col,'fill-opacity':selected?1:.55,stroke:selected?'#153438':col,'stroke-width':selected?2:.8});draw(g,'title',{},`${r.c.name} · ${r.c.region}\n$${fmt(r.x.value)} per person (${r.x.year}) · ${fmtValue(metric,r.y.value)} ${d.unit} (${r.y.year})`);if(selected){const lx=cx>W-110?cx-14:cx+14,anchor=cx>W-110?'end':'start';draw(g,'text',{x:lx,y:cy+4,'text-anchor':anchor,'font-size':12,fill:'#125a4e','font-weight':700,'paint-order':'stroke',stroke:'#fff','stroke-width':3},r.c.name);}const select=()=>{country=r.c.id;renderWorld();saveState();};g.onclick=select;g.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();select();}};}
const missing=rows.length-valid.length;
$('scatterDeck').textContent=`Each circle is one economy, sized by population and coloured by World Bank region. ${year==='latest'?'Latest available observations; years differ between economies and axes.':'Observations for '+year+' only.'} The selected economy is labelled; hover or focus any circle for its values and years.`;
$('worldMissing').textContent=`${valid.length} of ${rows.length} ${region==='all'?'economies':'economies in '+region} plotted. ${missing?missing+' lack one or both observations'+(year==='latest'?'':' in '+year)+'.':'All have both observations.'} No interpolation or carry-forward.${year!=='latest'&&valid.lengthr!=='all').map(r=>[r,regionColor(r)]).map(([r,col])=>``).join('')+``;$('regionLegend').querySelectorAll('button').forEach(b=>b.onclick=()=>{region=b.dataset.region;renderWorld();saveState();});}
function niceTicks(lo,hi,n){const span=hi-lo||1,raw=span/n,mag=10**Math.floor(Math.log10(raw)),step=[1,2,2.5,5,10].map(s=>s*mag).find(s=>span/s<=n)||mag*10;const out=[];for(let t=Math.ceil(lo/step)*step;t<=hi+1e-9;t+=step)out.push(+t.toFixed(10));return out;}
function logTicks(lo,hi){const out=[];for(let e=Math.floor(lo);e<=Math.ceil(hi);e++)for(const k of [1,2,5]){const v=k*10**e;if(Math.log10(v)>=lo&&Math.log10(v)<=hi)out.push(v);}return out.length>8?out.filter(v=>String(v)[0]==='1'):out;}
function fmtTick(v){return v>=1e6?compact(v):fmt(v,v<1?2:v<10?1:0);}
function renderTable(){const keys=['name','region','income',metric,'spending','physicians','life','uhc'].filter((k,i,a)=>a.indexOf(k)===i);const q=$('tableFilter').value.trim().toLowerCase();let rows=visibleCountries().filter(c=>!q||c.name.toLowerCase().includes(q)||c.id.toLowerCase().includes(q)||c.region.toLowerCase().includes(q)||c.income.toLowerCase().includes(q));
const get=(c,k)=>['name','region','income'].includes(k)?c[k]:value(c,k);
rows.sort((a,b)=>{const va=get(a,tableSort.key),vb=get(b,tableSort.key);if(va===null||va===undefined)return 1;if(vb===null||vb===undefined)return -1;return (typeof va==='string'?va.localeCompare(vb):va-vb)*tableSort.dir;});
const head=keys.map(k=>`
`).join('')}`;
$('observationTable').querySelectorAll('.sort').forEach(b=>b.onclick=()=>{const k=b.dataset.sort;tableSort=tableSort.key===k?{key:k,dir:-tableSort.dir}:{key:k,dir:['name','region','income'].includes(k)?1:-1};renderTable();});
$('observationTable').querySelectorAll('tbody tr').forEach(tr=>tr.onclick=()=>{country=tr.dataset.country;renderWorld();saveState();});
$('tableNote').textContent=`${rows.length} economies shown · ${yearLabel()} · click a column to sort, a row to select. Small years next to values mark the observation year in latest-available mode.`;}
function selectSystem(id){if(!data.countries.some(c=>c.id===id))return;country=id;renderWorld();saveState();}
function syncGlobe(){
const d=def(globeMetric),c=data.countries.find(c=>c.id===country),o=c?obs(c,globeMetric):null;
$('globeMetric').value=globeMetric;$('globeCountry').value=country;
$('globeSelected').textContent=`${c?.name||country} · ${o?fmtValue(globeMetric,o.value)+' ('+o.year+')':'no observation'}`;
const spec={getValue:cc=>value(cc,globeMetric),metricLabel:d.short+' ('+d.unit+')',year:yearLabel(),scale:d.scale,format:v=>fmtValue(globeMetric,v)};
if(!globe){globe=createGlobe($('worldGlobe'),{countries:data.countries,onSelect:selectSystem,onHover:showTip,selectedCountry:country,...spec});globe.setAutoRotate(globeAutoRotate);window.ASCEND_GLOBE=globe;}
const stats=globe.setMetric(spec)||{};globe.setSelection(country);globe.setActive(activeTab==='world');syncOverlay();
const min=Number($('worldGlobe').dataset.metricMin),max=Number($('worldGlobe').dataset.metricMax),observed=Number($('worldGlobe').dataset.metricObserved||stats.observed||0);
$('globeLegend').innerHTML=`${esc(d.short)} · ${esc(yearLabel())}
${esc(s.description||s.purpose||s.note||'Public-source data; inspect provenance in the export.')}
Source & documentation ↗`).join('');$('snapshotNote').textContent=`Snapshot retrieved ${String(data.accessedAt||data.retrievedAt||data.generatedAt||'Date unavailable').slice(0,10)}. Sources can revise historical observations. Data are cached; the app is not a continuously updating monitoring service.`;}
function safeURL(u){try{const v=new URL(u);return ['https:','http:'].includes(v.protocol)?v.href:'https://europepmc.org';}catch{return'https://europepmc.org';}}
async function searchPapers(){const q=$('paperSearch').value.trim();if(!q){toast('Enter a literature search term first.');return;}searchController?.abort();searchController=new AbortController();const timer=setTimeout(()=>searchController.abort(),20000);$('liveSearch').disabled=true;$('searchStatus').textContent='Retrieving matching publications from Europe PMC…';try{const url='https://www.ebi.ac.uk/europepmc/webservices/rest/search?'+new URLSearchParams({query:q,format:'json',resultType:'core',pageSize:'12'});const r=await fetch(url,{signal:searchController.signal});if(!r.ok)throw Error('HTTP '+r.status);const j=await r.json();liveSearchMeta={query:q,url,retrievedAt:new Date().toISOString()};livePapers=(j.resultList?.result||[]).map(p=>({title:p.title,year:p.pubYear,authors:p.authorString,abstract:p.abstractText,pmid:p.pmid|| (p.source==='MED'?p.id:null),doi:p.doi,url:p.doi?'https://doi.org/'+p.doi:'https://europepmc.org/article/'+p.source+'/'+p.id}));evidenceFilter='all';document.querySelectorAll('[data-evidence]').forEach(b=>b.classList.toggle('active',b.dataset.evidence==='all'));$('searchStatus').textContent=`Retrieved ${livePapers.length} of ${fmt(j.hitCount)} matches. Search relevance is not evidence quality.`;renderEvidence();saveState();}catch(e){$('searchStatus').textContent='Live retrieval unavailable. The saved evidence snapshot is still available; no missing data were replaced.';livePapers=null;renderEvidence();}finally{clearTimeout(timer);$('liveSearch').disabled=false;}}
function download(name,content,type){const a=document.createElement('a');const u=URL.createObjectURL(new Blob([content],{type}));a.href=u;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1000);}
function exportDecision(){const r=evaluate(params);download('ASCEND-decision.json',JSON.stringify({exportedAt:new Date().toISOString(),prototypeVersion:data.version,warning:'Illustrative scenario, not calibrated or a causal estimate.',view:{tab:activeTab,country,year,metric,globeMetric,globeCamera:globe?{longitude:$('worldGlobe').dataset.cameraLongitude,latitude:$('worldGlobe').dataset.cameraLatitude,distance:$('worldGlobe').dataset.cameraDistance}:null,evidenceFilter,query:$('paperSearch').value.trim()},assumptions:params,results:r,ranking:recommendation(r),dataSnapshot:data,sources:data.sources,liveSearchResults:livePapers,liveSearchMeta},null,2),'application/json');toast('Decision, assumptions, data and sources exported.');}
function exportSVG(){const svg=$('terrain').cloneNode(true);svg.setAttribute('xmlns','http://www.w3.org/2000/svg');svg.style.background='#102b31';svg.style.fontFamily='Arial, sans-serif';const W=$('terrain').clientWidth,H=$('terrain').clientHeight;svg.setAttribute('width',W);svg.setAttribute('height',H+48);svg.setAttribute('viewBox',`0 0 ${W} ${H+48}`);draw(svg,'rect',{x:0,y:H,width:W,height:48,fill:'#102b31'});draw(svg,'text',{x:15,y:H+20,fill:'#d2e5da','font-size':10},`ASCEND · ILLUSTRATIVE scenario · budget ${money(params.budget)} · not a causal estimate`);draw(svg,'text',{x:15,y:H+37,fill:'#d2e5da','font-size':9},'Legend: gray=current; green=capacity; violet=referral; amber=combined.');download('ASCEND-decision-landscape.svg',new XMLSerializer().serializeToString(svg),'image/svg+xml');toast('Labeled SVG exported.');}
function wire(){wireGlobe();document.querySelector('.skip').onclick=e=>{e.preventDefault();$('main').focus();$('main').scrollIntoView();};document.querySelectorAll('[data-tab]').forEach(b=>b.onclick=()=>setTab(b.dataset.tab));for(const k of ['budget','capacity','referralGain','arrivals','unitCost'])$(k).addEventListener('input',()=>{const n=Number($(k).value);if(!Number.isFinite(n)||$(k).value==='')return;params=normalizeState({...params,[k]:n});renderModel();saveState();});$('year').innerHTML=''+Array.from({length:data.yearRange[1]-data.yearRange[0]+1},(_,i)=>data.yearRange[1]-i).map(y=>``).join('');$('year').onchange=()=>{year=$('year').value==='latest'?'latest':Number($('year').value);renderWorld();saveState();};$('regionFilter').onchange=()=>{region=$('regionFilter').value;renderWorld();saveState();};$('worldMetric').onchange=()=>{metric=$('worldMetric').value;renderWorld();saveState();};$('reset').onclick=()=>{params={...DEFAULTS};country=DEFAULT_VIEW.country;year=DEFAULT_VIEW.year;metric=DEFAULT_VIEW.metric;globeMetric=DEFAULT_VIEW.globeMetric;region='all';overlayOn=false;overlaySurface='motorized';live={};$('tableFilter').value='';livePapers=null;liveSearchMeta=null;$('paperSearch').value='';evidenceFilter='all';document.querySelectorAll('[data-evidence]').forEach(b=>b.classList.toggle('active',b.dataset.evidence==='all'));$('searchStatus').textContent='';setTab(activeTab,false);toast('Default view restored.');};$('export').onclick=exportDecision;$('saveChart').onclick=exportSVG;$('share').onclick=async()=>{saveState();try{await navigator.clipboard.writeText(location.href);toast('View link copied. Country, year, indicator and scenario settings are included.');}catch{download('ASCEND-view-link.txt',location.href,'text/plain');toast('View link saved.');}};$('storyPreset').onclick=()=>{params={...DEFAULTS,capacity:145,referralGain:.25};renderModel();saveState();toast('Referral bottleneck selected. Try increasing surgical capacity: it adds no benefit here.');};$('guided').onclick=e=>{e.preventDefault();setTab('landscape');$('storyPreset').click();$('landscape').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth'});};$('toEvidence').onclick=()=>{setTab('evidence');$('evidence').scrollIntoView({behavior:'smooth'});};$('methods').onclick=$('methodsFooter').onclick=()=>$('methodDialog').showModal();$('closeMethods').onclick=()=>$('methodDialog').close();$('methodDialog').addEventListener('click',e=>{if(e.target===$('methodDialog')){const r=$('methodDialog').getBoundingClientRect();if(e.clientXr.right||e.clientYr.bottom)$('methodDialog').close();}});$('paperSearch').oninput=()=>{renderEvidence();saveState();};$('paperSearch').onkeydown=e=>{if(e.key==='Enter')searchPapers();};$('liveSearch').onclick=searchPapers;document.querySelectorAll('[data-evidence]').forEach(b=>b.onclick=()=>{evidenceFilter=b.dataset.evidence;document.querySelectorAll('[data-evidence]').forEach(x=>x.classList.toggle('active',x===b));renderEvidence();saveState();});window.addEventListener('popstate',()=>{readState();setTab(activeTab,false);});window.addEventListener('hashchange',()=>{readState();setTab(activeTab,false);});const ro=new ResizeObserver(()=>{if(activeTab==='landscape')renderTerrain();else if(activeTab==='world')renderWorld();});ro.observe($('terrainWrap'));ro.observe($('worldChart'));}
async function init(){try{data=window.ASCEND_DATA||await fetch('./data.json').then(r=>{if(!r.ok)throw Error('Data snapshot could not be loaded');return r.json();});readState();$('terrainLegend').innerHTML=STRATEGIES.map(s=>`${esc(shortNames[s.id])}`).join('');wire();setTab(activeTab,false);$('footerNote').textContent+=` Snapshot ${String(data.accessedAt).slice(0,10)} · v${data.version}.`;window.ASCEND_READY=true;}catch(e){document.querySelector('main').innerHTML='
Data snapshot unavailable
The app could not load its source data. Use the supplied standalone HTML or serve the project folder. No synthetic data have been substituted.