๐ FIFA World Cup 2026 โ Bracket & Winner Predictor
AUTO-REFRESH OFF โ๏ธ Admin
USA ยท Canada ยท Mexico ยท June 11 โ July 19, 2026 ยท 48 teams. Click teams to advance them, run scenarios, dial upsets. Connect a live feed in โ๏ธ Admin.
Last updated: โ ๐ Refresh now
0/31
Matches Decided
0
Upsets
โ
Predicted Champion
โ
Their Title Odds
๐๏ธ Bracket๐ Probability๐ฏ Best / Worst Case๐ก Live Data
๐ Scenario Presets โ each fills the entire bracket (real results stay locked)
๐ ChalkFavorites always win ๐ช๏ธ ChaosMaximum upsets ๐ฒ Realistic SimWeighted by probability ๐ด Morocco RunDark horse to final ๐บ๐ธ USA CinderellaHost upset run ๐ฅ Argentina vs BrazilDream final ๐ฅ No.1 Seeds FallTop 3 out early ๐ฏ One Big UpsetSingle shock, rest to form
๐ฒ Re-Simulate โบ Reset to Real Results โฝ Show xG
๐ช๏ธ Upset rate0%
Advancing Upset winner Higher win probability Toss-up / xG Live / final result
๐ Run Monte Carlo (10k) โก Quick (2k)
๐ช๏ธ Upset rate0%
Run a simulation to see each team's chance of reaching every round. Eliminated teams show 0%.
Analyze team: ๐ฏ Analyze
๐ก Auto-refresh & live status โ how it works
A browser-only page can't scrape FIFA/ESPN directly (CORS). Three ways to feed it, configured in โ๏ธ Admin: (1) manual score entry, (2) a sports-data API key, or (3) your own backend proxy. The app uses adaptive polling โ it only calls the API when a match is near kickoff or in play, so you don't burn your quota.
recommendedAdaptive polling schedule
No game today โ idle (no calls). Game within 30 min โ every 5 min. Game live โ every 60โ90 sec. Just finished โ every 2 min for 10 min, then stop. This keeps you under free-tier caps (football-data.org: 10 req/min; API-Football free: 100 req/day).
noteLive scores often need a paid tier
On many free tiers, scores are delayed rather than real-time; full goal-by-goal usually needs a paid livescores add-on. The adaptive schedule still works โ results just arrive with a short lag.
productionBackend proxy
For a public site, run a tiny server (Node / Cloudflare Worker) that holds your key and forwards the feed. Set its URL in Admin โ "Proxy URL".
Status: idle. Open โ๏ธ Admin to configure a feed.
๐ฐ Latest confirmed results baked in
Canada 1โ0 South Africa (FT) ยท Brazil 2โ1 Japan (FT) ยท Paraguay stunned Germany 1โ1 (4โ3 pens). 15 Round-of-32 ties still to play.
โ๏ธ Admin / Settings
Configure the live data feed and auto-refresh. Settings are saved in your browser (localStorage) and persist across reloads.
โ ๏ธ Security: anything entered here lives in the browser and is visible in DevTools. For a public site, use a Proxy URL (option 3) so your real API key stays on the server.
Data source Manual entry (no API) football-data.org (X-Auth-Token) API-Football (x-apisports-key) My backend proxy URL
API key
Get a free key: football-data.org (10 req/min) or API-Football (100 req/day).
Proxy URL
Should return JSON: array of {home, away, homeScore, awayScore, status}.
Auto-refresh mode
Adaptive Fixed Off
Adaptive = poll fast only when a game is live/near. Best for quota.
Fixed interval (minutes)
Used only in Fixed mode. 5 min is a safe default; 1โ2 min during live play.
๐พ Save settings ๐ Test fetch
๐๏ธ Clear saved picks Close
Settings idle.
Built by a football fan (not a developer) to settle family arguments. Planned with Claude & ChatGPT, built and refined with ChatterMate AI by CleverCreator.ai. Team ratings are illustrative estimates, not official rankings; win probabilities are model outputs, not guarantees. Tournament data is demo/sample unless a verified live feed is connected. For fun and scenario-exploration โ not a betting guide.
<script> const T = { "South Africa":{r:1480,f:"๐ฟ๐ฆ"}, "Canada":{r:1620,f:"๐จ๐ฆ"}, "Netherlands":{r:1990,f:"๐ณ๐ฑ"}, "Morocco":{r:1810,f:"๐ฒ๐ฆ"}, "Germany":{r:1960,f:"๐ฉ๐ช"}, "Paraguay":{r:1560,f:"๐ต๐พ"}, "France":{r:2080,f:"๐ซ๐ท"}, "Sweden":{r:1640,f:"๐ธ๐ช"}, "Belgium":{r:1900,f:"๐ง๐ช"}, "Senegal":{r:1750,f:"๐ธ๐ณ"}, "USA":{r:1700,f:"๐บ๐ธ"}, "Bosnia and Herzegovina":{r:1540,f:"๐ง๐ฆ"}, "Spain":{r:2050,f:"๐ช๐ธ"}, "Austria":{r:1680,f:"๐ฆ๐น"}, "Portugal":{r:2010,f:"๐ต๐น"}, "Croatia":{r:1880,f:"๐ญ๐ท"}, "Brazil":{r:2070,f:"๐ง๐ท"}, "Japan":{r:1780,f:"๐ฏ๐ต"}, "Cรดte d'Ivoire":{r:1660,f:"๐จ๐ฎ"}, "Norway":{r:1720,f:"๐ณ๐ด"}, "Mexico":{r:1710,f:"๐ฒ๐ฝ"}, "Ecuador":{r:1650,f:"๐ช๐จ"}, "England":{r:2030,f:"๐ด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ"}, "DR Congo":{r:1570,f:"๐จ๐ฉ"}, "Switzerland":{r:1790,f:"๐จ๐ญ"}, "Algeria":{r:1690,f:"๐ฉ๐ฟ"}, "Colombia":{r:1920,f:"๐จ๐ด"}, "Ghana":{r:1600,f:"๐ฌ๐ญ"}, "Australia":{r:1670,f:"๐ฆ๐บ"}, "Egypt":{r:1640,f:"๐ช๐ฌ"}, "Argentina":{r:2100,f:"๐ฆ๐ท"}, "Cabo Verde":{r:1450,f:"๐จ๐ป"}, }; const R32 = [ ["South Africa","Canada","2026-06-28T20:00:00",{h:0,a:1,winner:"Canada"}], ["Netherlands","Morocco","2026-07-01T21:00:00"], ["Germany","Paraguay","2026-06-30T16:30:00",{h:1,a:1,winner:"Paraguay",pen:"3-4"}], ["France","Sweden","2026-06-30T17:00:00"], ["Belgium","Senegal","2026-07-01T16:00:00"], ["USA","Bosnia and Herzegovina","2026-07-01T20:00:00"], ["Spain","Austria","2026-07-02T15:00:00"], ["Portugal","Croatia","2026-07-02T19:00:00"], ["Brazil","Japan","2026-06-29T13:00:00",{h:2,a:1,winner:"Brazil"}], ["Cรดte d'Ivoire","Norway","2026-06-30T13:00:00"], ["Mexico","Ecuador","2026-06-30T21:00:00"], ["England","DR Congo","2026-07-01T12:00:00"], ["Switzerland","Algeria","2026-07-02T23:00:00"], ["Colombia","Ghana","2026-07-03T21:30:00"], ["Australia","Egypt","2026-07-03T14:00:00"], ["Argentina","Cabo Verde","2026-07-03T18:00:00"], ]; const roundNames=["Round of 32","Round of 16","Quarter-finals","Semi-finals","Final"]; const reachLabels=["R16","QF","SF","Final","Champion"]; let upsetRate=0, showXG=false; let cfg={source:"manual",key:"",proxy:"",mode:"adaptive",interval:5}; let pollTimer=null, countdownTimer=null, nextPollAt=0;
function fmtDate(iso){try{return new Date(iso).toLocaleString(undefined,{weekday:'short',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return iso;}} function winProb(a,b){if(!a||!b)return .5;return 1/(1+Math.pow(10,(T[b].r-T[a].r)/400));} function isUpset(w,l){return T[w].r<T[l].r;} function xg(a,b){return Math.max(0.3,1.35+(T[a].r-T[b].r)/400*0.9);} function scoreline(a,b){return `${xg(a,b).toFixed(1)} โ ${xg(b,a).toFixed(1)}`;} function adjProb(a,b){const p=winProb(a,b);return p+(0.5-p)*upsetRate;}
let rounds=[]; function buildInitial(){ rounds=[]; rounds[0]=R32.map(m=>({a:m[0],b:m[1],iso:m[2],date:fmtDate(m[2]),result:m[3]||null,winner:m[3]?m[3].winner:null,ft:!!m[3],live:false,liveScore:null})); for(let r=1;r<5;r++){rounds[r]=[];for(let i=0;i<rounds[r-1].length/2;i++)rounds[r].push({a:null,b:null,date:"TBD",winner:null,live:false});} propagate(); } function propagate(){for(let r=1;r<rounds.length;r++){rounds[r].forEach((m,i)=>{const na=rounds[r-1][i*2].winner,nb=rounds[r-1][i*2+1].winner;if(m.a!==na||m.b!==nb){m.a=na;m.b=nb;}if(m.winner&&m.winner!==m.a&&m.winner!==m.b)m.winner=null;});}} function setWinner(r,i,team){rounds[r][i].winner=team;propagate();for(let rr=r+1;rr<rounds.length;rr++)rounds[rr].forEach(m=>{if(m.winner&&m.winner!==m.a&&m.winner!==m.b)m.winner=null;});savePicks();render();}
function render(){ const br=document.getElementById('bracket');br.innerHTML=''; for(let r=0;r<rounds.length;r++){ const col=document.createElement('div');col.className='round'; const t=document.createElement('div');t.className='round-title'; const dec=rounds[r].filter(m=>m.winner).length; t.innerHTML=`${roundNames[r]}<small>${dec}/${rounds[r].length} decided</small>`; col.appendChild(t); rounds[r].forEach((m,i)=>col.appendChild(matchEl(m,r,i))); br.appendChild(col); } const cc=document.createElement('div');cc.className='round'; const ct=document.createElement('div');ct.className='round-title';ct.innerHTML='Champion';cc.appendChild(ct); const champ=rounds[4][0].winner; const cd=document.createElement('div');cd.className='champ'+(champ?'':' empty'); cd.innerHTML=champ?`<div class="ttl">๐ Champion</div><div class="cflag">${T[champ].f}</div><div class="cname">${champ}</div>`:`<div class="ttl">Champion</div><div class="cflag" style="opacity:.3">๐</div><div class="cname">TBD</div>`; cc.appendChild(cd);br.appendChild(cc);updateStats(); } function teamRow(team,other,m,r,i,side){ const row=document.createElement('div');row.className='team'; if(!team){row.classList.add('tbd');row.innerHTML=`<span class="flag">ยท</span><span class="name">TBD</span>`;return row;} const p=winProb(team,other); if(m.winner===team){row.classList.add('winner');if(other&&isUpset(team,other))row.classList.add('upset');} let goals=''; if(m.result){const g=side==='a'?m.result.h:m.result.a;goals=`<span class="gs">${g}</span>`;} else if(m.live&&m.liveScore){const g=side==='a'?m.liveScore.h:m.liveScore.a;goals=`<span class="gs" style="color:#fca5a5">${g}</span>`;} else if(showXG&&other){goals=`<span class="xg">${xg(team,other).toFixed(1)}</span>`;} const showProb=(other&&!m.result&&!m.live); row.innerHTML=`<span class="flag">${T[team].f}</span><span class="name">${team}</span><span class="upbadge">UPSET</span>${goals}<span class="prob">${showProb?Math.round(p*100)+'%':((m.result||m.live)?'':'โ')}</span><div class="probbar"><span style="width:${showProb?p*100:0}%"></span></div>`; row.onclick=(e)=>{e.stopPropagation();if(other&&!m.ft)setWinner(r,i,team);}; return row; } function matchEl(m,r,i){ const el=document.createElement('div');el.className='match'; if(m.live)el.classList.add('islive'); else if(m.a&&m.b&&!m.result&&Math.abs(winProb(m.a,m.b)-.5)<.08)el.classList.add('tossup'); const d=document.createElement('div');d.className='match-date'; if(m.live)d.innerHTML=`<span class="livedot"></span><span class="livetag">LIVE</span>`; else if(m.ft)d.innerHTML=`<span class="ft">${m.result&&m.result.pen?'FT (pens '+m.result.pen+')':'Full Time'}</span>`; else d.innerHTML=m.date||'TBD'; d.onclick=(e)=>{e.stopPropagation();if(m.a&&m.b)openModal(m,r,i);}; el.appendChild(d);el.appendChild(teamRow(m.a,m.b,m,r,i,'a'));el.appendChild(teamRow(m.b,m.a,m,r,i,'b')); return el; }
function fillBracket(decider){for(let r=0;r<rounds.length;r++){rounds[r].forEach((m,i)=>{if(!m.a||!m.b)return;if(m.ft&&m.winner)return;m.winner=decider(m.a,m.b,r,i);});propagate();}savePicks();render();} function scenChalk(){buildInitial();applySaved();fillBracket((a,b)=>winProb(a,b)>=.5?a:b);flash("๐ Chalk โ every favorite advances.");} function scenChaos(){buildInitial();applySaved();fillBracket((a,b)=>{const p=winProb(a,b);if(Math.abs(p-.5)<.06)return Math.random()<.5?a:b;return p>=.5?b:a;});flash("๐ช๏ธ Chaos โ underdogs everywhere.");} function scenRandom(){buildInitial();applySaved();fillBracket((a,b)=>Math.random()<adjProb(a,b)?a:b);flash(`๐ฒ Realistic sim @ ${Math.round(upsetRate*100)}% upset rate.`);} function scenDarkHorse(team){buildInitial();applySaved();fillBracket((a,b)=>{if(a===team)return team;if(b===team)return team;return Math.random()<adjProb(a,b)?a:b;});flash(`๐ด ${team} run โ ${rounds[4][0].winner===team?'they win it all!':'see how far they get.'}`);} function scenClash(x,y){buildInitial();applySaved();fillBracket((a,b,r)=>{if(r<4){if(a===x||a===y)return a;if(b===x||b===y)return b;}return Math.random()<adjProb(a,b)?a:b;});const f=rounds[4][0];const made=f.a&&f.b&&((f.a===x&&f.b===y)||(f.a===y&&f.b===x));flash(made?`๐ฅ Dream final: ${x} vs ${y}!`:`๐ฅ Pushed ${x} & ${y} as far as possible.`);} function scenSeedFalls(){buildInitial();applySaved();const top=Object.keys(T).sort((a,b)=>T[b].r-T[a].r).slice(0,3);fillBracket((a,b,r)=>{if(r===0){if(top.includes(a))return b;if(top.includes(b))return a;}return Math.random()<adjProb(a,b)?a:b;});flash(`๐ฅ ${top.join(", ")} crash out early.`);} function scenOneUpset(){buildInitial();applySaved();const open=[];rounds[0].forEach((m,i)=>{if(!m.ft)open.push(i);});if(!open.length){flash("All R32 ties already decided.");return;}const idx=open[Math.floor(Math.random()*open.length)];fillBracket((a,b,r,i)=>{if(r===0&&i===idx)return winProb(a,b)>=.5?b:a;return winProb(a,b)>=.5?a:b;});const m=rounds[0][idx];flash(`๐ฏ One big upset: ${m.winner} shocks the bracket.`);} function resetBracket(){buildInitial();render();flash("โบ Reset to latest real results.");} function toggleXG(){showXG=!showXG;document.getElementById('xgToggle').textContent=showXG?'โฝ Hide xG':'โฝ Show xG';render();} function onSlider(){upsetRate=+document.getElementById('upsetSlider').value/100;syncSliders('upsetSlider');} function onSlider2(){upsetRate=+document.getElementById('upsetSlider2').value/100;syncSliders('upsetSlider2');} function syncSliders(src){const v=Math.round(upsetRate*100);['upsetSlider','upsetSlider2'].forEach(id=>{if(id!==src)document.getElementById(id).value=v;});document.getElementById('upsetVal').textContent=v+'%';document.getElementById('upsetVal2').textContent=v+'%';}
function openModal(m,r,i){ const a=m.a,b=m.b,pa=winProb(a,b),pb=1-pa; const fc=(n,va,vb)=>({n,va,vb}); const factors=[fc("Overall Strength",T[a].r,T[b].r),fc("Attack",T[a].r*.96+seed(a,1)*40,T[b].r*.96+seed(b,1)*40),fc("Defense",T[a].r*.94+seed(a,2)*40,T[b].r*.94+seed(b,2)*40),fc("Recent Form",T[a].r*.5+seed(a,3)*900,T[b].r*.5+seed(b,3)*900)]; const editable=!m.ft; document.getElementById('modal').innerHTML=` <h3>${roundNames[r]} ยท Matchup</h3><div class="mdate">${m.date||''}</div> <div class="vs-row"> <div class="vs-team"><div class="bigflag">${T[a].f}</div><div class="tn">${a}</div><div class="pct" style="color:${pa>=pb?'#818cf8':'#94a3b8'}">${Math.round(pa*100)}%</div><div class="score">xG ${xg(a,b).toFixed(2)}</div></div> <div class="vs-mid">vs</div> <div class="vs-team"><div class="bigflag">${T[b].f}</div><div class="tn">${b}</div><div class="pct" style="color:${pb>pa?'#fbbf24':'#94a3b8'}">${Math.round(pb*100)}%</div><div class="score">xG ${xg(b,a).toFixed(2)}</div></div> </div> <div style="text-align:center;font-size:12px;color:var(--muted);margin:-4px 0 4px">Projected: <b style="color:var(--gold)">${scoreline(a,b)}</b></div> <div class="probtrack"><div class="a" style="width:${pa*100}%"></div><div class="b" style="width:${pb*100}%"></div></div> <div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted)"><span>${a}</span><span>${b}</span></div> ${editable?`<div class="scoreset"><input type="number" id="scH" min="0" value="0"><span style="color:var(--muted);font-weight:700">โ</span><input type="number" id="scA" min="0" value="0"><button class="btn-live" onclick="enterScore(${r},${i})">Enter final score</button></div>`:`<div style="text-align:center;margin:10px 0;color:var(--gold);font-weight:700">Final: ${a} ${m.result.h}โ${m.result.a} ${b}${m.result.pen?' (pens '+m.result.pen+')':''}</div>`} <div class="factors">${factors.map(f=>{const t=f.va+f.vb,wa=f.va/t*100;return `<div class="factor"><span class="lbl">${f.n}</span><div class="fbar"><span style="width:${wa}%;background:${wa>=50?'#6366f1':'#f59e0b'}"></span></div><span style="min-width:64px;text-align:right;font-weight:700">${Math.round(wa)}/${100-Math.round(wa)}</span></div>`;}).join('')}</div> ${editable?`<div class="pick-btns"><button class="btn-ghost" onclick="setWinner(${r},${i},${JSON.stringify(a)});closeModal()">Advance ${a}</button><button class="btn-ghost" onclick="setWinner(${r},${i},${JSON.stringify(b)});closeModal()">Advance ${b}</button></div>`:''} <button class="btn-primary modal-close" onclick="closeModal()">Close</button>`; document.getElementById('overlay').classList.add('open'); } function enterScore(r,i){const h=+document.getElementById('scH').value,a=+document.getElementById('scA').value;const m=rounds[r][i];let winner=h>a?m.a:(a>h?m.b:(winProb(m.a,m.b)>=.5?m.a:m.b));m.result={h,a,winner,pen:h===a?'model':null};m.ft=true;m.live=false;m.winner=winner;propagate();savePicks();render();closeModal();flash(`โ ${m.a} ${h}โ${a} ${m.b} recorded.`);} function closeModal(){document.getElementById('overlay').classList.remove('open');} function seed(name,s){let h=s*131;for(const c of name)h=(h*31+c.charCodeAt(0))%9973;return(h%100)/100;}
function simulateOnce(){let layer=rounds[0].map(m=>m.ft?m.winner:(Math.random()<adjProb(m.a,m.b)?m.a:m.b));const reach={};layer.forEach(t=>reach[t]=1);let rd=1;while(layer.length>1){const nx=[];for(let i=0;i<layer.length;i+=2){const a=layer[i],b=layer[i+1];nx.push(Math.random()<adjProb(a,b)?a:b);}layer=nx;rd++;layer.forEach(t=>reach[t]=rd);}reach[layer[0]]=5;return reach;} function runMonteCarlo(runs){const info=document.getElementById('runinfo');info.innerHTML=`<span class="loading">Running ${runs.toLocaleString()} simulationsโฆ</span>`;setTimeout(()=>{const counts={};Object.keys(T).forEach(t=>counts[t]=[0,0,0,0,0]);for(let n=0;n<runs;n++){const reach=simulateOnce();for(const t in reach){const mx=reach[t];for(let lvl=1;lvl<=mx;lvl++)counts[t][lvl-1]++;}}const rows=Object.keys(T).map(t=>({t,p:counts[t].map(c=>c/runs)})).sort((a,b)=>b.p[4]-a.p[4]);let html=`<thead><tr><th>Team</th>${reachLabels.map(l=>`<th class="num">${l}</th>`).join('')}</tr></thead><tbody>`;rows.forEach(r=>{html+=`<tr><td class="tname">${T[r.t].f} ${r.t}</td>`;r.p.forEach((v,idx)=>{const col=idx===4?'#f59e0b':'#6366f1';html+=`<td class="num cellbar"><div class="fill" style="background:${col};width:${v*100}%"></div><span>${(v*100).toFixed(1)}%</span></td>`;});html+='</tr>';});html+='</tbody>';document.getElementById('probTable').innerHTML=html;info.textContent=`${runs.toLocaleString()} simulations complete @ ${Math.round(upsetRate*100)}% upset rate.`;},30);}
function runBestWorst(){const team=document.getElementById('bwTeam').value;const runs=6000;const finishes=[];let champ=0;for(let n=0;n<runs;n++){const reach=simulateOnce();finishes.push(reach[team]||0);if(reach[team]===5)champ++;}finishes.sort((a,b)=>a-b);const pct=q=>finishes[Math.floor(q*finishes.length)];const fmt=v=>["Out (R32)","Reached R16","Reached QF","Reached SF","Runner-up","๐ Champion"][v];document.getElementById('floorCeil').innerHTML=`<div class="fc"><div class="lab">๐ Floor (5th %ile)</div><div class="res" style="color:var(--lose)">${fmt(pct(0.05))}</div></div><div class="fc"><div class="lab">๐ Median</div><div class="res" style="color:var(--primary-hi)">${fmt(pct(0.5))}</div></div><div class="fc"><div class="lab">๐ Ceiling (95th %ile)</div><div class="res" style="color:var(--win)">${fmt(pct(0.95))}</div></div><div class="fc"><div class="lab">๐ Title odds</div><div class="res" style="color:var(--gold)">${(champ/runs*100).toFixed(1)}%</div></div>`;const slot=R32.findIndex(m=>m[0]===team||m[1]===team);const best=computePath(team,slot,'easy'),worst=computePath(team,slot,'hard');const ph=(p,cls)=>p.map((s,i)=>`<div class="pathstep"><span class="rd">${roundNames[i]}</span><span class="opp">${T[s.opp].f} ${s.opp}</span><span class="pp" style="color:${cls==='best'?'#10b981':'#ef4444'}">${Math.round(winProb(team,s.opp)*100)}%</span></div>`).join('');document.getElementById('bwGrid').innerHTML=`<div class="bw-card best"><h4>๐ข Best Case Path <span style="color:var(--muted);font-weight:500;font-size:11px">(weakest opponents)</span></h4>${ph(best,'best')}<div style="margin-top:10px;font-size:12px;color:var(--muted)">Combined opp rating: <b style="color:var(--win)">${best.reduce((a,s)=>a+T[s.opp].r,0).toLocaleString()}</b></div></div><div class="bw-card worst"><h4>๐ด Worst Case Path <span style="color:var(--muted);font-weight:500;font-size:11px">(toughest opponents)</span></h4>${ph(worst,'worst')}<div style="margin-top:10px;font-size:12px;color:var(--muted)">Combined opp rating: <b style="color:var(--lose)">${worst.reduce((a,s)=>a+T[s.opp].r,0).toLocaleString()}</b></div></div>`;} function computePath(team,slot,mode){const path=[];for(let r=0;r<5;r++){const bs=Math.pow(2,r+1);const start=Math.floor(slot/bs)*bs;const half=bs/2;const first=(slot-start)<half;const os=first?start+half:start;const c=[];for(let s=os;s<os+half;s++){c.push(R32[s][0],R32[s][1]);}const sorted=c.filter(x=>x!==team).sort((a,b)=>T[a].r-T[b].r);path.push({opp:mode==='easy'?sorted[0]:sorted[sorted.length-1]});}return path;}
function openDrawer(){document.getElementById('drawer').classList.add('open');document.getElementById('drawerBg').classList.add('open');loadSettingsToForm();} function closeDrawer(){document.getElementById('drawer').classList.remove('open');document.getElementById('drawerBg').classList.remove('open');} function onSourceChange(){const s=document.getElementById('cfgSource').value;document.getElementById('keyField').style.display=(s==='footballdata'||s==='apifootball')?'block':'none';document.getElementById('proxyField').style.display=(s==='proxy')?'block':'none';} function setMode(m){cfg.mode=m;document.querySelectorAll('#modeSeg button').forEach(b=>b.classList.toggle('on',b.dataset.mode===m));document.getElementById('fixedField').style.display=(m==='fixed')?'block':'none';const hints={adaptive:"Adaptive = poll fast only when a game is live/near. Best for quota.",fixed:"Fixed = poll every N minutes regardless of schedule.",off:"Off = no automatic polling. Use Refresh now / manual entry."};document.getElementById('modeHint').textContent=hints[m];} function loadSettingsToForm(){document.getElementById('cfgSource').value=cfg.source;document.getElementById('cfgKey').value=cfg.key;document.getElementById('cfgProxy').value=cfg.proxy;document.getElementById('cfgInterval').value=cfg.interval;setMode(cfg.mode);onSourceChange();} function saveSettings(){cfg.source=document.getElementById('cfgSource').value;cfg.key=document.getElementById('cfgKey').value.trim();cfg.proxy=document.getElementById('cfgProxy').value.trim();cfg.interval=Math.max(1,+document.getElementById('cfgInterval').value||5);try{localStorage.setItem('wc2026cfg',JSON.stringify(cfg));}catch(e){}adminLog("๐พ Settings saved.");scheduleNextPoll();updateLivePill();flash("๐พ Settings saved & applied.");} function loadSettings(){try{const s=localStorage.getItem('wc2026cfg');if(s)cfg=Object.assign(cfg,JSON.parse(s));}catch(e){}} function adminLog(msg){const el=document.getElementById('adminLog');if(el)el.textContent=new Date().toLocaleTimeString()+" "+msg;} function liveLog(msg){const el=document.getElementById('liveLog');if(el)el.textContent=new Date().toLocaleTimeString()+" "+msg;}
function adaptiveIntervalMs(){ const now=Date.now();let best=Infinity; rounds.flat().forEach(m=>{ if(!m.iso||m.ft)return; const k=new Date(m.iso).getTime();const diff=k-now; if(diff<=0&&diff>-120*60000)best=Math.min(best,75*1000); else if(diff>0&&diff<=30*60000)best=Math.min(best,5*60000); else if(diff<=-120*60000&&diff>-130*60000)best=Math.min(best,2*60000); }); return best===Infinity?0:best; } function scheduleNextPoll(){ clearTimeout(pollTimer);clearInterval(countdownTimer); if(cfg.mode==='off'){nextPollAt=0;updateNextLabel();updateLivePill();return;} let ms=(cfg.mode==='fixed')?cfg.interval*60000:adaptiveIntervalMs(); if(!ms){nextPollAt=0;updateNextLabel();updateLivePill();pollTimer=setTimeout(scheduleNextPoll,5*60000);return;} nextPollAt=Date.now()+ms; pollTimer=setTimeout(()=>{pollNow();scheduleNextPoll();},ms); countdownTimer=setInterval(updateNextLabel,1000); updateNextLabel();updateLivePill(); } function updateNextLabel(){ const el=document.getElementById('nextPoll'); if(!nextPollAt){el.textContent=cfg.mode==='off'?'ยท auto-refresh off':'ยท idle (no game near)';return;} const s=Math.max(0,Math.round((nextPollAt-Date.now())/1000)); el.innerHTML=`ยท next refresh in <b>${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}</b> (${cfg.mode})`; } function updateLivePill(){ const anyLive=rounds.flat().some(m=>m.live&&!m.ft); const pill=document.getElementById('livePill'),txt=document.getElementById('liveStatusText'); if(anyLive){pill.classList.remove('off');txt.textContent="LIVE ยท match in play";} else if(cfg.mode==='off'){pill.classList.add('off');txt.textContent="AUTO-REFRESH OFF";} else{pill.classList.add('off');txt.textContent="WATCHING ยท "+(cfg.source==='manual'?'manual':cfg.mode);} } async function pollNow(){ document.getElementById('lastUpdated').textContent=new Date().toLocaleTimeString(); if(cfg.source==='manual'){liveLog("Manual mode โ no API call. Enter scores via match modal.");adminLog("Manual mode: nothing to fetch.");updateLivePill();return;} try{ let matches=[]; if(cfg.source==='footballdata'){ if(!cfg.key){liveLog("No API key set (Admin).");return;} const res=await fetch("https://api.football-data.org/v4/competitions/WC/matches",{headers:{"X-Auth-Token":cfg.key}}); if(!res.ok)throw new Error("HTTP "+res.status); const d=await res.json();matches=(d.matches||[]).map(m=>({home:norm(m.homeTeam&&m.homeTeam.name),away:norm(m.awayTeam&&m.awayTeam.name),hs:m.score&&m.score.fullTime&&m.score.fullTime.home,as:m.score&&m.score.fullTime&&m.score.fullTime.away,status:m.status})); }else if(cfg.source==='apifootball'){ if(!cfg.key){liveLog("No API key set (Admin).");return;} const res=await fetch("https://v3.football.api-sports.io/fixtures?league=1&season=2026",{headers:{"x-apisports-key":cfg.key}}); if(!res.ok)throw new Error("HTTP "+res.status); const d=await res.json();matches=(d.response||[]).map(x=>({home:norm(x.teams&&x.teams.home&&x.teams.home.name),away:norm(x.teams&&x.teams.away&&x.teams.away.name),hs:x.goals&&x.goals.home,as:x.goals&&x.goals.away,status:x.fixture&&x.fixture.status&&x.fixture.status.short})); }else if(cfg.source==='proxy'){ if(!cfg.proxy){liveLog("No proxy URL set (Admin).");return;} const res=await fetch(cfg.proxy);if(!res.ok)throw new Error("HTTP "+res.status); const d=await res.json();matches=(Array.isArray(d)?d:d.matches||[]).map(m=>({home:norm(m.home),away:norm(m.away),hs:m.homeScore,as:m.awayScore,status:m.status})); } const applied=applyFeed(matches); liveLog(`โ Fetched ${matches.length} matches, applied ${applied}.`);adminLog(`โ ${applied} updates applied.`); }catch(e){ liveLog(`โ ${e.message}. Browser CORS may block direct API calls โ use a proxy URL for production.`); adminLog(`โ Fetch failed: ${e.message}`); } scheduleNextPoll();updateLivePill(); } function applyFeed(matches){ let applied=0; matches.forEach(fm=>{ if(!fm.home||!fm.away)return; const idx=rounds[0].findIndex(x=>(x.a===fm.home&&x.b===fm.away)||(x.a===fm.away&&x.b===fm.home)); if(idx<0)return;const m=rounds[0][idx]; if(m.ft)return; // never overwrite a locked real result const swap=m.a!==fm.home; const HH=swap?fm.as:fm.hs, AA=swap?fm.hs:fm.as; const live=["IN_PLAY","PAUSED","1H","2H","HT","ET","LIVE"].includes(fm.status); const done=["FINISHED","FT","AET","PEN"].includes(fm.status); if(done&&HH!=null&&AA!=null){const w=HH>AA?m.a:(AA>HH?m.b:m.a);m.result={h:HH,a:AA,winner:w};m.ft=true;m.live=false;m.winner=w;applied++;} else if(live&&HH!=null&&AA!=null){m.live=true;m.liveScore={h:HH,a:AA};applied++;} }); if(applied){propagate();render();} return applied; } function norm(n){if(!n)return n;const map={"United States":"USA","USA":"USA","Ivory Coast":"Cรดte d'Ivoire","Cรดte d'Ivoire":"Cรดte d'Ivoire","Congo DR":"DR Congo","DR Congo":"DR Congo","Cape Verde":"Cabo Verde","Cabo Verde":"Cabo Verde","Bosnia-Herzegovina":"Bosnia and Herzegovina","Bosnia and Herzegovina":"Bosnia and Herzegovina"};return map[n]||n;}
function savePicks(){try{const picks=rounds.map(rd=>rd.map(m=>m.winner));localStorage.setItem('wc2026picks',JSON.stringify(picks));}catch(e){}} function applySaved(){try{const s=localStorage.getItem('wc2026picks');if(!s)return;const picks=JSON.parse(s);for(let r=0;r<rounds.length;r++){rounds[r].forEach((m,i)=>{const w=picks[r]&&picks[r][i];if(w&&!m.ft&&(w===m.a||w===m.b))m.winner=w;});propagate();}}catch(e){}} function clearSavedBracket(){try{localStorage.removeItem('wc2026picks');}catch(e){}buildInitial();render();adminLog("๐๏ธ Saved picks cleared.");flash("๐๏ธ Saved picks cleared.");}
function updateStats(){const all=rounds.flat();const dec=all.filter(m=>m.winner).length;document.getElementById('stFilled').textContent=`${dec}/${all.length}`;const ups=all.filter(m=>m.winner&&m.a&&m.b&&isUpset(m.winner,m.winner===m.a?m.b:m.a)).length;document.getElementById('stUpsets').textContent=ups;const champ=rounds[4][0].winner;if(champ){document.getElementById('stChamp').textContent=champ;document.getElementById('stOdds').textContent=quickOdds(champ)+'%';}else{document.getElementById('stChamp').textContent='โ';document.getElementById('stOdds').textContent='โ';}} function quickOdds(team){let w=0,runs=1500;for(let n=0;n<runs;n++){if(simulateOnce()[team]===5)w++;}return Math.round(w/runs*100);} function switchTab(tab){document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active',p.id==='panel-'+tab));} function flash(msg){let f=document.getElementById('flash');if(!f){f=document.createElement('div');f.id='flash';f.style.cssText='position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--surface2);border:1px solid var(--border);padding:11px 18px;border-radius:11px;font-size:12.5px;font-weight:600;box-shadow:0 8px 30px rgba(0,0,0,.4);z-index:99;transition:opacity .3s;max-width:90vw;text-align:center;';document.body.appendChild(f);}f.textContent=msg;f.style.opacity='1';clearTimeout(f._t);f._t=setTimeout(()=>f.style.opacity='0',3000);}
(function(){const sel=document.getElementById('bwTeam');Object.keys(T).sort((a,b)=>T[b].r-T[a].r).forEach(t=>{const o=document.createElement('option');o.value=t;o.textContent=t;sel.appendChild(o);});})(); loadSettings();buildInitial();applySaved();render();scheduleNextPoll(); document.getElementById('lastUpdated').textContent=new Date().toLocaleTimeString(); </script>