import {createGlobe} from './globe.mjs'; import {DEFAULTS,STRATEGIES,evaluate,landscape,recommendation,normalizeState} from './model.mjs'; import {RAMP,rampColor,scaleFraction} from './globe-math.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,globeMetric=DEFAULT_VIEW.globeMetric,globeAutoRotate=!matchMedia('(prefers-reduced-motion: reduce)').matches; 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()});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';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`:'RETRIEVED SOURCESDiscovery leads · not approved model parameters';$('saveChart').hidden=tab!=='landscape';if(tab==='world')renderWorld();if(tab==='evidence')renderEvidence();if(tab==='landscape')renderModel();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=`
AT THESE ASSUMPTIONS

${esc(shortNames[winner.id])}${ties.length?' · tied':''}

${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=`
${stages.map(([label,v,b])=>`
${label}${fmt(v,1)}
${winner.bindingConstraints.includes(b)?'BINDING GATE':''}
`).join('')}
${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=>`
${esc(shortNames[r.id])}${winner.tiedIds.includes(r.id)?' *':''}${r.feasible?money(r.cost)+' committed':esc(r.reason)}
${r.feasible?'+'+fmt(r.incremental,1):'—'}
`).join('')+`

* 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=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=>`${g.indicators.filter(k=>includePopulation||k!=='population').map(k=>``).join('')}`).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=`

${esc(c.name)}

${esc(c.region)} · ${esc(c.income)}${pop?` · ${compact(pop.value)} people (${pop.year})`:''}

${data.indicatorGroups.map(g=>`
${g.indicators.filter(k=>k!=='population').map(k=>{const o=obs(c,k);return `
${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):''}
`;}).join('')}
`).join('')}

Country selection changes context only. It does not calibrate the illustrative scenario.

`;} 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').innerHTML=`${head}${rows.map(c=>`${keys.map(k=>{if(['name','region','income'].includes(k))return `${esc(c[k])}`;const o=obs(c,k);return `${o?fmtValue(k,o.value):'—'}${o&&year==='latest'?`${o.year}`:''}`;}).join('')}`).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'); 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())}
${Number.isFinite(min)?fmtValue(globeMetric,min):'—'}${d.scale==='log'&&Number.isFinite(min)&&min>0?`${fmtValue(globeMetric,Math.sqrt(min*max))}`:''}${Number.isFinite(max)?fmtValue(globeMetric,max):'—'}
${esc(d.unit)} · ${d.scale==='log'?'log':'linear'} scale, 2nd–98th percentile${d.better?' · '+(d.better==='higher'?'higher is better':'lower is better'):''}`; $('globeCoverage').textContent=`${observed} of ${data.countries.length} economies observed · ${data.countries.length-observed} without observation (grey) · gold ring = selected system`; $('globeRotate').textContent=globeAutoRotate?'Pause rotation':'Start rotation';$('globeRotate').setAttribute('aria-pressed',String(globeAutoRotate)); } function showTip(c,info){const tip=$('globeTip');if(!c){tip.hidden=true;return;}const rect=$('worldGlobe').getBoundingClientRect();tip.hidden=false;tip.innerHTML=`${esc(c.name)}${esc(def(globeMetric).short)}: ${esc(info.display||'no observation')}`;const o=obs(c,globeMetric);if(o)tip.innerHTML+=`${o.year} · ${esc(def(globeMetric).unit)}`;tip.style.left=Math.min(rect.width-tip.offsetWidth-8,Math.max(8,info.x-rect.left+14))+'px';tip.style.top=Math.max(8,info.y-rect.top-tip.offsetHeight-12)+'px';} function wireGlobe(){ $('globeCountry').innerHTML=data.countries.map(c=>``).join('');$('globeCountry').onchange=()=>selectSystem($('globeCountry').value); $('globeMetric').innerHTML=metricOptions(globeMetric);$('globeMetric').onchange=()=>{globeMetric=$('globeMetric').value;renderWorld();saveState();}; $('worldMetric').innerHTML=metricOptions(metric);$('regionFilter').innerHTML=regions().map(r=>``).join(''); $('rotateWest').onclick=()=>{globeAutoRotate=false;globe.setAutoRotate(false);globe.rotateBy(-20,0);syncGlobe();};$('rotateEast').onclick=()=>{globeAutoRotate=false;globe.setAutoRotate(false);globe.rotateBy(20,0);syncGlobe();}; $('globeZoomIn').onclick=()=>globe?.zoomBy(.86);$('globeZoomOut').onclick=()=>globe?.zoomBy(1.16);$('globeReset').onclick=()=>{globe?.reset();}; $('globeRotate').onclick=()=>{globeAutoRotate=!globeAutoRotate;globe?.setAutoRotate(globeAutoRotate);syncGlobe();}; $('worldGlobe').addEventListener('pointerdown',()=>{globeAutoRotate=false;globe?.setAutoRotate(false);$('globeRotate').textContent='Start rotation';$('globeRotate').setAttribute('aria-pressed','false');}); $('saveGlobe').onclick=()=>{const url=globe?.capture();if(!url){toast('Globe image export is unavailable on this device.');return;}const a=document.createElement('a');a.href=url;a.download=`ASCEND-globe-${globeMetric}-${year}.png`;a.click();toast('Globe image saved. Indicator and year are shown in this view.');}; $('tableFilter').oninput=()=>renderTable(); } function plain(s){return String(s??'').replace(/<[^>]*>/g,' ').replace(/\s+/g,' ').trim();} function topic(p){const t=(p.title+' '+(p.abstract||'')).toLowerCase();return ['capacity','access','financing'].filter(k=>({capacity:/capacity|workforce|surgeon|infrastructure|hospital/,access:/access|referral|delay|equity|inequal/,financing:/financ|cost|fund|expend|insurance|economic/})[k].test(t));} function papers(){return livePapers||data.evidence?.publications||[];} function renderEvidence(){if(!data)return;const q=$('paperSearch').value.trim().toLowerCase();const ps=papers().filter(p=>(evidenceFilter==='all'||topic(p).includes(evidenceFilter))&&(!q||`${p.title} ${p.abstract||''} ${p.authors||''}`.toLowerCase().includes(q)));$('evidenceCards').innerHTML=ps.length?ps.map(p=>`
${esc(p.year||'YEAR UNAVAILABLE')} · ${esc(p.pmid?'PUBMED '+p.pmid:'EUROPE PMC')}

${esc(p.title)}

${esc(Array.isArray(p.authors)?p.authors.join(', '):p.authors||'Author information unavailable')}

${topic(p).length?topic(p).map(t=>({capacity:'Capacity',access:'Access & referral',financing:'Financing'})[t]).join(' · '):'Related research'} DISCOVERY LEAD

${p.abstract?`
Read abstract

${esc(plain(p.abstract))}

`:'

Abstract unavailable in this snapshot.

'}Open source ↗
`).join(''):'

No matching sources in this snapshot. Try a broader term or search Europe PMC.

'; const studies=data.trials?.studies||[];$('trials').innerHTML=studies.length?studies.map(t=>`

${esc(t.title)}

${esc(t.studyType||'')} ${esc(Array.isArray(t.phase)?t.phase.join(', '):t.phase||'')}

${esc((t.countries||[]).slice(0,4).join(' · ')||'Sites not listed')}${t.countries?.length>4?' + more':''}

View registration ↗
`).join(''):'

Trial registry snapshot unavailable. This is a data-access gap, not an absence of studies.

'; const registry=data.apiRegistry||[];$('sourceRegistry').innerHTML=registry.map(s=>`

${esc(s.name||s.id)}

${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';$('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.

'+esc(e.message)+'

';console.error(e);}} init();