GENESIS • CLOSED ALPHA • ALPHA HISTORY MAY RESET AT HARD LAUNCH
Not signed in
Welcome to the EBL
Create Player Account
Login
Password Recovery
EBL Headlines
Recent Performance
Genesis begins at zero. Your recent-game line will build here.
EBL Community Chat
Team Chat
EBL NEWSROOM
The stories, results, rivalries, and moments shaping the Elite Baseball League.
EBL COMMUNITY
The history the league creates together.
Rivalry Board
EBL Record Book
PRIVATE MESSAGES
Talk directly with coaches, teammates, and other EBL members.
Contacts
Conversation
Create Your EBL Player
1 • BASIC INFO
2 • APPEARANCE
3 • ATTRIBUTES
4 • REVIEW
Franchise Operations
Manage contracts, batting order, field alignment, rotation, bullpen roles, and defensive tactics.
League
EBL Analytics
Award Races
Genesis races update as the season develops. Statistical titles are earned on the field; major awards use performance-based rankings.
GameCast Center
Completed games become watchable pitch-by-pitch archives. Live reveal mode is the foundation for community game nights.
EBL Rules & Policies
Commissioner / Admin
Reset Test Account
Moderation Queue
async function api(url,opt={}){let r=await fetch(url,{credentials:'same-origin',headers:{'Content-Type':'application/json'},...opt});let j=await r.json();if(!r.ok)throw j;return j}
let ME=null,PLAYER=null,LEAGUE=null,SCHEDULE=[],GG=null,gi=0,timer=null,D=null;
const HA=['CON','POW','VIS','DISC','TIM','SPD','FLD','ARM','ACC','REAC'], PA=['STA','H9','K9','BB9','HR9','PCLT','CTRL','VEL','BRK','FLD','ARM','ACC','REAC'];
const cost=v=>v<25?1:v<50?2:v<70?3:v<85?5:v<95?8:12;
document.querySelectorAll('.navbtn').forEach(b=>b.onclick=()=>showPage(b.dataset.page,b));
function showPage(id,b){document.querySelectorAll('.page,.navbtn').forEach(x=>x.classList.remove('active'));document.querySelector('#'+id).classList.add('active');b.classList.add('active')}
function switchPane(sel,id,b){let root=b.closest('.card');root.querySelectorAll(sel+',.subtab').forEach(x=>x.classList.remove('active'));root.querySelector('#'+id).classList.add('active');b.classList.add('active')}
function leaguePane(id,b){switchPane('.subpane',id,b)} function analyticsPane(id,b){switchPane('.subpane',id,b)} function awardPane(id,b){switchPane('.subpane',id,b)}
async function recoverAccount(){
try{
let j=await api('/api/account/recover',{method:'POST',body:JSON.stringify({username:recUser.value,recovery_code:recCode.value,new_password:recPw.value})});
recMsg.innerHTML=`Password reset. Save your NEW recovery code somewhere safe: ${j.new_recovery_code}`;
}catch(e){recMsg.textContent=e.error||'Recovery failed'}
}
async function requestReset(){
try{await api('/api/account/request-password-reset',{method:'POST',body:JSON.stringify({email:resetEmail.value})});recMsg.textContent='If that verified email exists, a reset link has been sent.'}catch(e){recMsg.textContent='Request could not be processed.'}
}
async function register(){try{let j=await api('/api/register',{method:'POST',body:JSON.stringify({username:ru.value,password:rp.value,email:re.value})});ME=j.user;if(j.recovery_code)alert('SAVE THIS RECOVERY CODE:\n'+j.recovery_code+'\n\nIt is required to reset your password in closed alpha.');await refresh()}catch(e){loginMsg.textContent=e.error||'Registration failed'}}
async function login(){try{let j=await api('/api/login',{method:'POST',body:JSON.stringify({username:lu.value,password:lp.value})});ME=j.user;await refresh()}catch(e){loginMsg.textContent=e.error||'Login failed'}}
async function logout(){await api('/api/logout',{method:'POST'});location.reload()}
async function refresh(){
let me=await api('/api/me');ME=me.user;
if(!ME){accountTag.textContent='Not signed in';return}
accountTag.innerHTML=`${ME.username} • ${ME.role} `;
loginCard.classList.add('hidden');dashboard.classList.remove('hidden');adminNav.classList.toggle('hidden',ME.role!=='COMMISSIONER');coachNav.classList.toggle('hidden',!['COACH','COMMISSIONER'].includes(ME.role));
await Promise.all([loadLeague(),loadSchedule(),loadAnalytics(),loadAwards(),loadPlayer(),loadNews(),loadReadiness(),loadCommunity(),loadDMContacts()]);if(['COACH','COMMISSIONER'].includes(ME.role))await loadCoachHub();
await Promise.all([loadChat('EBL'),loadChat('TEAM')]);
renderHome();
}
function teamName(fid){let t=LEAGUE?.teams?.find(x=>x.id===fid);return t?t.name:fid}
async function loadPlayer(){try{let j=await api('/api/my-player');PLAYER=j.player;renderPlayer()}catch{PLAYER=null}}
function initials(n){return (n||'EB').split(/\s+/).slice(0,2).map(x=>x[0]).join('').toUpperCase()}
function fmt3(v){return Number(v||0).toFixed(3).replace(/^0/,'')}
function statBox(k,v){return `
${k}${v}
`}
function hitterStats(p){
let s=p.season||{},ab=s.AB||0,pa=s.PA||0,h=s.H||0,bb=s.BB||0,tb=(s['1B']||0)+2*(s['2B']||0)+3*(s['3B']||0)+4*(s.HR||0);
let avg=ab?h/ab:0,obp=pa?(h+bb)/pa:0,slg=ab?tb/ab:0;
return [{id:'bat',title:'BATTING',stats:[['G',s.G||0],['PA',pa],['AB',ab],['H',h],['AVG',fmt3(avg)],['OBP',fmt3(obp)],['SLG',fmt3(slg)],['HR',s.HR||0],['2B',s['2B']||0],['3B',s['3B']||0],['BB',bb],['SO',s.SO||0],['R',s.R||0],['RBI',s.RBI||0]]},
{id:'fld',title:'FIELDING',stats:[['G',s.FG||0],['PO',s.PO||0],['A',s.A||0],['E',s.E||0],['DP',s.DP||0],['FLD%',s.FLD_PCT||'1.000'],['OAA',s.OAA||0]]},
{id:'run',title:'BASERUNNING',stats:[['SB',s.SB||0],['CS',s.CS||0],['SB%',(s.SB||s.CS)?((s.SB||0)/((s.SB||0)+(s.CS||0))*100).toFixed(1)+'%':'0.0%'],['XBT',s.XBT||0],['OOB',s.OOB||0],['BsR',s.BSR||'0.0']]}]}
function pitcherStats(p){let s=p.season||{},outs=s.OUTS||0,ip=(outs/3).toFixed(1),era=outs?(s.ER||0)*27/outs:0,whip=outs?((s.H||0)+(s.BB||0))/(outs/3):0;return [{id:'pit',title:'PITCHING',stats:[['G',s.G||0],['GS',s.GS||0],['IP',ip],['W',s.W||0],['L',s.L||0],['SV',s.SV||0],['H',s.H||0],['ER',s.ER||0],['BB',s.BB||0],['SO',s.SO||0],['ERA',era.toFixed(2)],['WHIP',whip.toFixed(2)]]},{id:'pfld',title:'FIELDING',stats:[['PO',s.PO||0],['A',s.A||0],['E',s.E||0],['DP',s.DP||0],['FLD%',s.FLD_PCT||'1.000']]}]}
function statTabs(defs){return `
${defs.map((d,i)=>``).join('')}
${defs.map((d,i)=>`
${d.stats.map(x=>statBox(x[0],x[1])).join('')}
`).join('')}`}
function playerStat(id,b){let card=b.closest('.card');card.querySelectorAll('.statPane,.statTabs .subtab').forEach(x=>x.classList.remove('active'));card.querySelector('#'+id).classList.add('active');b.classList.add('active')}
function renderPlayer(){
if(!PLAYER){playerMain.innerHTML='
No EBL player yet
Build your first Genesis player below.
';createCard.classList.remove('hidden');setCreatorStep(1);return}
createCard.classList.add('hidden');let p=PLAYER,team=p.team?.name||'Free Agent',defs=p.type==='P'?pitcherStats(p):hitterStats(p);
let f=FACE_PRESETS[(p.face_id||1)-1]||FACE_PRESETS[0];
let profileAvatar=`
`;
}
function avatarMarkup(){
let f=FACE_PRESETS[CREATOR.face_id-1];return `
`;
}
function pickFace(id){CREATOR.face_id=id;creatorAppearance()} function pickHair(id){CREATOR.hair_id=id;creatorAppearance()}
function beginAttributes(){
let pitcher=['SP','RP'].includes(CREATOR.position),names=pitcher?PA:HA;
if(!CREATOR.attributes || Object.keys(CREATOR.attributes).join(',')!==names.join(',')){CREATOR.attributes=Object.fromEntries(names.map(x=>[x,0]));CREATOR.pool=50}
setCreatorStep(3);
}
function creatorAttributes(){
D={...CREATOR,pool:CREATOR.pool,attributes:CREATOR.attributes};
const pitcher=['SP','RP'].includes(CREATOR.position);
const groups=pitcher
? [{id:'buildPitch',title:'PITCHING',attrs:['STA','H9','K9','BB9','HR9','PCLT','CTRL','VEL','BRK']},{id:'buildField',title:'FIELDING',attrs:['FLD','ARM','ACC','REAC']}]
: [{id:'buildHit',title:'HITTING',attrs:['CON','POW','VIS','DISC','TIM']},{id:'buildField',title:'FIELDING',attrs:['FLD','ARM','ACC','REAC']},{id:'buildRun',title:'BASERUNNING',attrs:['SPD']}];
creatorStage.innerHTML=`
STARTING POOL50 XP
REMAINING${CREATOR.pool} XP
SPENT${50-CREATOR.pool} XP
${groups.map((g,i)=>``).join('')}
${groups.map((g,i)=>`
${g.attrs.filter(a=>a in CREATOR.attributes).map(a=>creatorAttr(a)).join('')}
`).join('')}
`;
}
function creatorAttr(k){let v=CREATOR.attributes[k];return `
${k}
${v}
`}
function addCreatorAttr(k){if(CREATOR.pool>0){CREATOR.attributes[k]++;CREATOR.pool--;creatorAttributes()}}
function builderTab(id,b){let card=b.closest('.card')||document;card.querySelectorAll('.statPane,.statTabs .subtab').forEach(x=>x.classList.remove('active'));document.querySelector('#'+id).classList.add('active');b.classList.add('active')}
function attributeReview(){if(CREATOR.pool!==0)return alert('Spend all 50 XP first.');setCreatorStep(4)}
function creatorReview(){
creatorStage.innerHTML=`
Face ${CREATOR.face_id} • ${HAIR_NAMES[CREATOR.hair_id-1]}
`;
}
async function finishCreator(){
try{
let payload={name:CREATOR.name,position:CREATOR.position,bats:CREATOR.bats,throws:CREATOR.throws,face_id:CREATOR.face_id,hair_id:CREATOR.hair_id,attributes:CREATOR.attributes};
await api('/api/player/create',{method:'POST',body:JSON.stringify(payload)});
await loadPlayer();renderHome();
}catch(e){alert(e.error||'Could not create player')}
}
async function spend(a){if(!confirm(`Spend ${cost(PLAYER.attributes[a])} XP to increase ${a}?`))return;try{await api('/api/player/spend-xp',{method:'POST',body:JSON.stringify({attribute:a})});await loadPlayer();renderHome()}catch(e){alert(e.error)}}
async function market(){try{await api('/api/player/request-cpu-market',{method:'POST'});await loadPlayer()}catch(e){alert(e.error)}}
async function respond(id,action){try{await api('/api/player/respond-offer',{method:'POST',body:JSON.stringify({offer_id:id,action})});await loadPlayer();await loadLeague();renderHome()}catch(e){alert(e.error)}}
async function loadLeague(){let j=await api('/api/league');LEAGUE=j;renderStandings()}
function renderStandings(){if(!LEAGUE)return;let html=`
Season ${LEAGUE.season} • League Day ${LEAGUE.day}
`;for(let d of LEAGUE.divisions){let teams=LEAGUE.teams.filter(t=>t.division===d).sort((a,b)=>b.wins-a.wins||((b.runs_for-b.runs_against)-(a.runs_for-a.runs_against)));let lead=teams[0]?.wins||0;html+=`
`}standingsPane.innerHTML=html}
async function loadSchedule(){let j=await api('/api/schedule');SCHEDULE=j.games;renderSchedule();renderTicker();renderGamecastList()}
function renderSchedule(){let day=LEAGUE?.day||0,filterTeam=PLAYER?.franchise_id;let upcoming=SCHEDULE.filter(g=>g.league_day>=Math.max(1,day) && g.league_day<=day+7);schedulePane.innerHTML=`
${scheduleRows(upcoming,'ALL')}
`}
function scheduleRows(games,mode){let arr=mode==='MINE'&&PLAYER?.franchise_id?games.filter(g=>g.away_id===PLAYER.franchise_id||g.home_id===PLAYER.franchise_id):games;return arr.map(g=>`
Day ${g.league_day} • ${teamName(g.away_id)} @ ${teamName(g.home_id)}${g.status==='FINAL'?`${g.away_runs}-${g.home_runs} • WATCH`:'UPCOMING'}
`).join('')||'
No games in this window.
'}
function scheduleFilter(mode,b){b.parentElement.querySelectorAll('.subtab').forEach(x=>x.classList.remove('active'));b.classList.add('active');let day=LEAGUE?.day||0;document.querySelector('#scheduleRows').innerHTML=scheduleRows(SCHEDULE.filter(g=>g.league_day>=Math.max(1,day)&&g.league_day<=day+7),mode)}
function renderTicker(){let day=LEAGUE?.day||0,arr=SCHEDULE.filter(g=>g.league_day===Math.max(1,day)||g.league_day===day+1).slice(0,15);let items=arr.map(g=>`${g.status==='FINAL'?'FINAL':'NEXT'} ${teamName(g.away_id)} ${g.status==='FINAL'?g.away_runs:'@'} ${teamName(g.home_id)} ${g.status==='FINAL'?g.home_runs:''}`).join('');tickerTrack.innerHTML=items+items}
function renderGamecastList(){let finals=SCHEDULE.filter(g=>g.status==='FINAL').slice(-30).reverse();gamecastList.innerHTML=finals.length?finals.map(g=>`
${raceList(rows,x=>`${teamName(x.team)} • Genesis fielding race`)}`).join('')}
function raceList(rows,detail){return rows.length?rows.map((x,i)=>`
${i+1}
${x.name}
${detail(x)}
${i<3?'🏆':''}
`).join(''):'
Race begins on Opening Day.
'}
function renderHome(){
if(!PLAYER){homePlayer.innerHTML='
Create Your Player
Your Genesis career starts with 50 XP and a blank history.
';homeTeam.innerHTML='
No Team Yet
Create a player, enter the CPU market, and sign your first EBL contract.
';nextGame.innerHTML='
Next Game
Your schedule appears after signing.
';return}
let p=PLAYER,t=p.team;homePlayer.innerHTML=`
${initials(p.name)}
MY PLAYER
${p.name}
${p.primary_pos} • ${(+p.xp_wallet).toFixed(3)} XP available
`;
homeTeam.innerHTML=t?`MY TEAM
${t.name}
${t.wins}-${t.losses}
${t.division} Division
`:`MY TEAM
Free Agent
Enter the CPU market from the Player tab.
`;
let ng=p.franchise_id?SCHEDULE.find(g=>g.status==='SCHEDULED'&&(g.away_id===p.franchise_id||g.home_id===p.franchise_id)&&g.league_day>=(LEAGUE?.day||0)):null;
nextGame.innerHTML=ng?`NEXT GAME • DAY ${ng.league_day}
`).join('');box.scrollTop=box.scrollHeight}catch{}}
function escapeHtml(x){return x.replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
async function sendChat(ch){let input=ch==='EBL'?eblMsg:teamMsg,msg=input.value.trim();if(!msg)return;try{await api('/api/chat/send',{method:'POST',body:JSON.stringify({channel:ch,message:msg})});input.value='';loadChat(ch)}catch(e){alert(e.error)}}
let NEWS=[];
let DM_CONTACTS=[],DM_ACTIVE=null;
async function loadDMContacts(){
try{
let j=await api('/api/dm/contacts');DM_CONTACTS=j.contacts||[];
dmContacts.innerHTML=DM_CONTACTS.length?DM_CONTACTS.map(c=>`
';
}catch(e){}
}
async function openDM(id){
DM_ACTIVE=id;let c=DM_CONTACTS.find(x=>x.id===id);dmTitle.textContent=c?`Conversation with ${c.username}`:'Conversation';
await loadDMContacts();
let j=await api('/api/dm/thread/'+id);
dmThread.innerHTML=j.messages.map(m=>`
${m.sender_name} • ${m.created_at}
${escapeHtml(m.message)}
`).join('');
dmThread.scrollTop=dmThread.scrollHeight;
}
async function sendDM(){
if(!DM_ACTIVE)return alert('Choose a contact first.');
let msg=dmMsg.value.trim();if(!msg)return;
try{await api('/api/dm/send',{method:'POST',body:JSON.stringify({recipient_user_id:DM_ACTIVE,message:msg})});dmMsg.value='';await openDM(DM_ACTIVE)}catch(e){alert(e.error)}
}
async function loadCommunity(){
try{
let [rv,rc]=await Promise.all([api('/api/rivalries'),api('/api/records')]);
rivalryBoard.innerHTML=rv.rivalries.length?rv.rivalries.map(r=>`
${r.team_a_name} vs ${r.team_b_name}
${r.games} meetings • ${r.a_wins}-${r.b_wins} • ${r.one_run_games} one-run games
';
}catch(e){}
}
async function loadReadiness(){
try{
let r=await api('/api/league/readiness'),pct=r.total?Math.round(r.filled/r.total*100):0;
readinessBar.querySelector('span').style.width=pct+'%';
readinessText.innerHTML=`${r.filled}/${r.total} roster slots filled • ${r.human} human • ${r.cpu} CPU • Phase: ${r.phase}`;
positionReadiness.innerHTML=r.positions.map(p=>`
${p.position_group}${p.filled}/${p.total}${p.human} human
`).join('');
}catch(e){}
}
async function loadNews(){
try{
let j=await api('/api/news');NEWS=j.news||[];renderNews('ALL');renderHomeNews();
}catch(e){
if(newsFeed)newsFeed.innerHTML='
The newsroom is waiting for the first EBL games to create history.
';
}
}
function renderNews(filter){
if(!window.newsFeed)return;
let rows=filter==='ALL'?NEWS:NEWS.filter(n=>n.category===filter);
newsFeed.innerHTML=rows.length?rows.map(n=>`
${n.category.replaceAll('_',' ')}LEAGUE DAY ${n.league_day}
${n.headline}
${n.body}
`).join(''):'
No stories in this section yet. Genesis history is still being written.
';
}
function newsFilter(f,b){document.querySelectorAll('#news .subtab').forEach(x=>x.classList.remove('active'));b.classList.add('active');renderNews(f)}
function renderHomeNews(){
if(!window.homeNews)return;
let rows=NEWS.slice(0,5);
homeNews.innerHTML=rows.length?rows.map(n=>`
${n.category.replaceAll('_',' ')} • DAY ${n.league_day} ${n.headline}
`).join(''):'
No headlines yet. The first pitch of Genesis will start the story.
';
}
function goPage(id){let b=document.querySelector(`.navbtn[data-page="${id}"]`);if(b)b.click()}
function uniformOptions(selected){return ['WHITE','NAVY','RED','GRAY','BLACK','CREAM'].map(x=>``).join('')}
function teamLogoMarkup(style,p,s,a,name){
const shapes=['◆','⬟','★','⚾','A','E','EBL','▲','●','✦'];
return `
${shapes[(style-1)%shapes.length]}
${name}
`;
}
function updateBrandPreview(){
if(!window.brandPreview)return;
brandPreview.innerHTML=teamLogoMarkup(+brandLogo.value,brandPrimary.value,brandSecondary.value,brandAccent.value,brandName.value||'EBL TEAM');
}
async function saveBranding(){
try{
await api('/api/coach/branding',{method:'POST',body:JSON.stringify({display_name:brandName.value,logo_style:+brandLogo.value,primary_color:brandPrimary.value,secondary_color:brandSecondary.value,accent_color:brandAccent.value,uniform_home:brandHome.value,uniform_away:brandAway.value})});
alert('Team identity saved.');await loadLeague();await loadCoachHub();
}catch(e){alert(e.error)}
}
async function loadCoachHub(){
try{
let j=await api('/api/coach/team');
if(!j.team){coachHub.innerHTML='
No franchise assigned.
';return}
let H=j.roster.filter(x=>x.type==='H'),P=j.roster.filter(x=>x.type==='P'),SP=P.filter(x=>x.primary_pos==='SP');
let lineup=j.lineup||[],rot=j.rotation||[],bp=j.strategy?.bullpen||{},def=j.strategy?.defense||{};
coachHub.innerHTML=`
FRANCHISE${j.team.name}
RECORD${j.team.wins}-${j.team.losses}
XP BUDGET${(j.team.xp_budget-j.team.xp_spent).toFixed(1)}
`}
}
function coachPane(id,b){switchPane('.subpane',id,b)}
function playerOptions(arr,selected,multi=[]){return arr.map(x=>``).join('')}
function lineupRowHTML(i,id,H){
const posList=['C','1B','2B','3B','SS','LF','CF','RF','DH'];
return `
${i+1}
`;
}
function shiftRow(label,id,val){let opts=['STANDARD','PULL','OPPO','NO_DOUBLES','BUNT_DEFENSE','INFIELD_IN'];return `
${label}
`}
async function saveCoachLineup(){let ids=[...Array(9)].map((_,i)=>+document.querySelector('#bat'+i).value);try{await api('/api/coach/set-lineup',{method:'POST',body:JSON.stringify({batting_order:ids})});alert('Batting order saved.')}catch(e){alert(e.error)}}
function multiVals(id){return [...document.querySelector('#'+id).selectedOptions].map(x=>+x.value)}
async function savePitchingStaff(){
let rotation=[...Array(5)].map((_,i)=>+document.querySelector('#rot'+i).value);
try{
await api('/api/coach/set-rotation',{method:'POST',body:JSON.stringify({rotation})});
let bullpen={CL:valOrNull('bpCL'),SU1:valOrNull('bpSU1'),SU2:valOrNull('bpSU2'),MR:multiVals('bpMR'),LR:multiVals('bpLR'),EMERGENCY:multiVals('bpEmergency')};
let j=await api('/api/coach/team');await api('/api/coach/set-strategy',{method:'POST',body:JSON.stringify({bullpen,defense:currentDefense(),bench:j.strategy.bench||{},substitutions:j.strategy.substitutions||{}})});
alert('Pitching staff saved.');
}catch(e){alert(e.error)}
}
function valOrNull(id){let v=document.querySelector('#'+id).value;return v?+v:null}
function currentDefense(){return {default_shift:defDefault.value,vs_lhb:defL.value,vs_rhb:defR.value,corners_in:cornersIn.checked,infield_in:infieldIn.checked}}
async function saveDefense(){try{let j=await api('/api/coach/team');await api('/api/coach/set-strategy',{method:'POST',body:JSON.stringify({bullpen:j.strategy.bullpen,defense:currentDefense(),bench:j.strategy.bench||{},substitutions:j.strategy.substitutions||{}})});alert('Defensive plan saved.')}catch(e){alert(e.error)}}
function depthRow(pos,H,selected){
return `
${pos}
`;
}
function depthId(pos){return 'depth_'+pos.replace('1','one').replace('2','two').replace('3','three')}
async function saveDepthSubs(){
try{
let j=await api('/api/coach/team');
let bench={};
['C','1B','2B','3B','SS','LF','CF','RF','DH'].forEach(pos=>bench[pos]=multiVals(depthId(pos)));
let substitutions={
pinch_hit:multiVals('phOrder'),
pinch_run:multiVals('prOrder'),
def_replacement:multiVals('drOrder'),
catcher_backup:valOrNull('backupC'),
late_inning_defense_inning:+lateDefInning.value,
pinch_hit_threshold:phThresh.value,
steal_aggression:stealAgg.value,
bunt_aggression:buntAgg.value
};
await api('/api/coach/set-strategy',{method:'POST',body:JSON.stringify({bullpen:j.strategy.bullpen,defense:j.strategy.defense,bench,substitutions})});
alert('Depth chart and substitution preferences saved.');
}catch(e){alert(e.error||'Could not save depth chart')}
}
async function loadCoachFreeAgents(){try{let j=await api('/api/coach/free-agents');coachFAs.innerHTML=j.players.slice(0,75).map(p=>`
'}catch(e){alert(e.error)}}
async function coachOffer(pid){let bonus=+prompt('Signing bonus XP (0-25)','10'),salary=+prompt('Salary XP/game (.25,.30,.35,.40,.45)','.35'),years=+prompt('Years (1-3)','2');try{await api('/api/coach/offer',{method:'POST',body:JSON.stringify({player_id:pid,bonus,salary,years})});await loadCoachHub();alert('Offer sent.')}catch(e){alert(e.error)}}
async function cancelOffer(id){try{await api('/api/coach/cancel-offer',{method:'POST',body:JSON.stringify({offer_id:id})});await loadCoachHub()}catch(e){alert(e.error)}}
async function simDay(){try{let j=await api('/api/commish/sim-day',{method:'POST'});simout.textContent=`League Day ${j.day} complete — ${j.results.length} games`;await refresh()}catch(e){simout.textContent=e.error}}
async function resetTestAccount(){
const target=document.getElementById('resetTestTarget').value.trim();
const out=document.getElementById('resetTestOut');
if(!target){
out.textContent='Enter a username or email.';
return;
}
if(!confirm(`Reset test account "${target}"? This will delete that test account and its player.`)){
return;
}
out.textContent='Resetting account...';
try{
const j=await api('/api/commish/reset-test-account',{
method:'POST',
body:JSON.stringify({target})
});
out.textContent=`Reset complete: ${j.username}. Removed ${j.removed_players || 0} player(s) and restored ${j.restored_slots || 0} roster slot(s).`;
document.getElementById('resetTestTarget').value='';
await refresh();
}catch(e){
out.textContent='Reset failed: '+(e.error || e.message || 'Unknown error');
}
}
function policyPane(id,b){switchPane('.subpane',id,b)}
async function loadPolicies(){
communityRules.innerHTML=`
Community Rules
Keep EBL competitive, welcoming, and baseball-focused.
No harassment, threats, hate speech, sexual content, or targeted abuse.
No spam, scams, impersonation, or attempts to manipulate other users.
No cheating, exploits, multi-account collusion, or intentionally corrupting league systems.
Respect team and league moderation decisions. Use reports instead of escalating conflicts.
Private messages are not a loophole around community rules.
Franchise names, logos, chat, and player names must be appropriate for a general audience.
Violations may lead to warnings, mutes, suspensions, account removal, or competitive penalties.
`;
termsPolicy.innerHTML=`
Beta Terms
EBL is currently a beta service. Features, balance, league formats, virtual progression, and Genesis records may change or reset before the hard launch.
Users must provide accurate account information, protect their credentials, avoid exploiting the service, and follow community rules. EBL may suspend accounts or pause league activity to protect competitive integrity and service safety.
Virtual XP, awards, franchise rights, and other in-game items have no cash value unless a future published policy explicitly says otherwise.
These beta terms are a product draft and should receive legal review before commercial/public launch.
`;
privacyPolicy.innerHTML=`
Privacy Summary
EBL stores account credentials in hashed form, verified email addresses, login/session records, game activity, league statistics, chat/private messages, moderation reports, and technical security logs needed to operate the service.
Email addresses are used for account verification, password recovery, security notices, and service communications. EBL should not sell personal information.
Users can report abuse and block other users. Moderators may review reported community content and private messages when specifically reported or when required for safety/service integrity.
A jurisdiction-specific privacy policy and retention/deletion process should receive legal review before commercial/public launch.
`;
}
async function loadReports(){
try{let j=await api('/api/commish/reports');reportQueue.innerHTML=j.reports.length?j.reports.map(r=>`
`}
async function watch(id){let j=await api('/api/game/'+id);GG=j.game;gi=0;clearInterval(timer);renderGameShell();gc.showModal();timer=setInterval(step,450)}
function renderGameShell(){gcontent.innerHTML=`