RRosternomics

Cardinals STL

GM: Dal Maxvill

Overview

WINDOW ↘ cooling
cheap & winning · year 1 in this regime · payroll 0.87× league · TAB +1.8
+1.8TAB — talent above budget12th/26
+3.9WAB — realized wins (TAB + luck)9th/26
7.0depth — 2+ WAR regulars8th/26
-5.5dead WAR — sub-replacement drag18th/26
14%dead money %3rd/26
0.519win %8th/26
34.3roster fWAR13th/26
+1surplus $M (value vs cost)6th/26
$20Mpayroll $M17th/26

Roster

CFRay Lankford 149GRex Hudler 21GMilt Thompson 12G
LFBernard Gilkey 74GMilt Thompson 72GRex Hudler 27G
RFFelix Jose 153GRex Hudler 11GMilt Thompson 11G
SSOzzie Smith 150GJosé Oquendo 22GTim Jones 14G
2BJosé Oquendo 118GGeronimo Pena 83GLuis Alicea 11G
3BTodd Zeile 154GCraig Wilson 12GStan Royer 5G
1BPedro Guerrero 112GGerald Perry 61GRod Brewer 15G
STARTERSBryn Smith 2.9WARBob Tewksbury 2.7WARKen Hill 2.0WAROmar Olivares 1.9WARRheal Cormier 1.6WAR
CTom Pagnozzi 139GRich Gedman 43GRay Stephens 6G
BULLPEN Lee Smith 47svJuan Agosto 0hScott Terry 0hCris Carpenter 0hWillie Fraser 0hBob McClure 0hMike Perez 0hTim Sherrill 0h
CTom Pagnozzi 139 · Rich Gedman 43 · Ray Stephens 6
1BPedro Guerrero 112 · Gerald Perry 61 · Rod Brewer 15
2BJosé Oquendo 118 · Geronimo Pena 83 · Luis Alicea 11
3BTodd Zeile 154 · Craig Wilson 12 · Stan Royer 5
SSOzzie Smith 150 · José Oquendo 22 · Tim Jones 14
LFBernard Gilkey 74 · Milt Thompson 72 · Rex Hudler 27
CFRay Lankford 149 · Rex Hudler 21 · Milt Thompson 12
RFFelix Jose 153 · Rex Hudler 11 · Milt Thompson 11
SPBryn Smith 2.9 · Bob Tewksbury 2.7 · Ken Hill 2.0 · Omar Olivares 1.9 · Rheal Cormier 1.6
BP Lee Smith 47 · Juan Agosto 0 · Scott Terry 0 · Cris Carpenter 0 · Willie Fraser 0 · Bob McClure 0 · Mike Perez 0 · Tim Sherrill 0

Each position lists its starter then backups by games (so platoons & bench show). Starters (mound) & bullpen (left, foul territory) are ranked by WAR; marks the closer (most save chances). MLB Stats API.

1991 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Ozzie Smith 36 FA-elig 1505.1 2.2 +0.8
Tom Pagnozzi 28 arb 1403.4 0.3 +1.7
Milt Thompson 32 FA-elig 1153.2 1.5 +0.4
Felix Jose 26 arb 1543.0 0.2 +1.6
Bryn Smith 35 FA-elig 312.9 2.1 -0.4
Bob Tewksbury 30 arb 302.7 0.2 +1.5
Todd Zeile 25 pre-arb 1552.6 0.2 +1.4
Lee Smith 33 FA-elig 672.0 2.8 -1.6
Ken Hill 25 arb 302.0 0.2 +1.0
Omar Olivares 23 pre-arb 281.9 0.1 +1.0
Ray Lankford 24 pre-arb 1511.7 0.1 +0.9
José Oquendo 27 FA-elig 1271.7 0.8 +0.2
Rheal Cormier 24 111.6
Jose DeLeon 30 FA-elig 281.3 2.4 -1.6
Scott Terry 31 arb 651.2 0.3 +0.4
Geronimo Pena 24 pre-arb 1041.0 0.1 +0.5
Pedro Guerrero 35 FA-elig 1150.8 2.3 -1.8
Gerald Perry 30 FA-elig 1090.6 1.2 -0.8
Bernard Gilkey 24 pre-arb 810.5 0.1 +0.2
Cris Carpenter 26 arb 590.4 0.1 +0.1
Bob McClure↔ ANA 39 FA-elig 320.3 0.5 -0.3
Stan Royer 23 90.0
Mark Grater 27 3-0.0
Ray Stephens 28 6-0.1
Mike Perez 26 pre-arb 14-0.1 0.1 -0.2
Tim Sherrill 25 10-0.1
Tim Jones 28 16-0.2
Mark Clark 23 pre-arb 7-0.2 0.1 -0.2
Luis Alicea 25 56-0.2
Jamie Moyer 28 arb 8-0.2 0.2 -0.3
Juan Agosto 33 FA-elig 72-0.3 1.2 -1.4
Rod Brewer 25 19-0.3
Craig Wilson 26 pre-arb 60-0.8 0.1 -0.6
Rich Gedman 31 FA-elig 46-0.9 0.1 -0.7
Willie Fraser↔ TOR 27 35-0.9
Rex Hudler 30 FA-elig 101-1.1 0.3 -0.9

Key additions (new vs 1990)

PlayerfWAR
Rheal Cormier1.6
Gerald Perry0.6
Bob McClure0.3
Stan Royer0.0
Mark Grater-0.0
Mark Clark-0.2

Key subtractions (gone from 1990)

Player1990 fWAR
Willie McGee4.2
Joe Magrane3.4
Vince Coleman3.3
John Tudor2.3
Ken Dayley0.5
Terry Pendleton0.4

Current & Historical Payroll

1991 payroll
$19.8M

What the front office was committed to as of 1991, 1991–1997. Pre-2015 has no contract-terms data — forward italic $ are realized actuals (hindsight, not commitments known then). Control labels (ARB/FA) are point-in-time.

current $ guaranteed $ actual $ (hindsight) arbitration (proj) pre-arb (proj) free agent

Guaranteed / signed (12)

PlayerAgeSvc1991199219931994199519961997
Lee Smith3311.000$2.8M$2.8M$2.3MFREE AGENT
Jose DeLeon308.000$2.4M$2.2MFREE AGENT
Pedro Guerrero3513.000$2.3M$2.4MFREE AGENT
Ozzie Smith3613.000$2.2M$2.0M$3.0M$3.0M$3.0M$3.5MFREE AGENT
Bryn Smith3510.000$2.1M$2.2MFREE AGENT
Milt Thompson327.000$1.5M$1.6MFREE AGENT
Gerald Perry308.000$1.2M$1.2M$967k$650k$299kFREE AGENT
Juan Agosto3310.000$1.2M$940kFREE AGENT
José Oquendo278.000$775k$2.0M$2.0M$2.1M$2.2MFREE AGENT
Bob McClure3916.000$498k$500kFREE AGENT
Rex Hudler307.000$300k$400kFREE AGENT
Rich Gedman3111.000$150k$200kFREE AGENT

Arbitration-eligible (10)

PlayerAgeSvc1991199219931994199519961997
Scott Terry315.000$350kFREE AGENT
Tom Pagnozzi284.000$310kARB 3FREE AGENT
Jamie Moyer285.000$200kFREE AGENT
Felix Jose263.000$160kARB 2ARB 3FREE AGENT
Bob Tewksbury305.000$160kFREE AGENT
Ken Hill253.000$155kARB 2ARB 3FREE AGENT
Cris Carpenter263.000$120kARB 2ARB 3FREE AGENT
Luis Alicea253.000ARB 2ARB 3FREE AGENT
Willie Fraser275.000FREE AGENT
Tim Jones283.000ARB 2ARB 3FREE AGENT

Pre-arbitration (14)

PlayerAgeSvc1991199219931994199519961997
Todd Zeile252.000$160kARB 1ARB 2ARB 3FREE AGENT
Craig Wilson262.000$130kARB 1ARB 2ARB 3FREE AGENT
Bernard Gilkey241.000$125kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ray Lankford241.000$125kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Geronimo Pena241.000$120kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Omar Olivares231.000$120kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mike Perez261.000$115kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mark Clark230.000$100kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Rod Brewer251.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Rheal Cormier240.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mark Grater27
Stan Royer230.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tim Sherrill251.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Ray Stephens281.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Cardinals 1991; the black line = the league-average player at each slot that season. Staying above the line deep into the roster is the depth edge; dropping below it = holes & dead weight.

What each regime is worth — every settled team-season since 1985

RegimeSeasonsWin %Playoff %Pennant %WS %
WINDOW 28952.230.14.81.0
CONTEND 31156.863.019.310.3
FORCED-SPEND 26047.98.81.21.2
REBUILD 32843.31.20.30.3
const FONT={family:"'Roboto Mono', monospace",size:12}; const ORBIT = [[1985, 1.069, 16.47], [1986, 0.875, -4.71], [1987, 1.046, 1.67], [1988, 1.247, -7.23], [1989, 1.131, 10.44], [1990, 1.154, 1.97], [1991, 0.867, 1.78], [1992, 0.878, 3.34], [1993, 0.717, 4.95], [1994, 0.905, -2.82], [1995, 0.956, -8.39], [1996, 1.202, 0.25], [1997, 1.131, 5.0], [1998, 1.286, 3.59], [1999, 1.017, -3.03], [2000, 1.215, 9.66], [2001, 1.174, 7.13], [2002, 1.208, 8.04], [2003, 1.243, 4.92], [2004, 1.361, 12.09], [2005, 1.271, 5.55], [2006, 1.249, -5.69], [2007, 1.152, -8.12], [2008, 1.055, 7.7], [2009, 1.17, 5.07], [2010, 1.151, 0.15], [2011, 1.224, 4.54], [2012, 1.192, 8.25], [2013, 0.905, 9.76], [2014, 1.059, -0.95], [2015, 1.068, 9.12], [2016, 1.083, 6.89], [2017, 1.113, 5.24], [2018, 1.183, -3.7], [2019, 1.166, 0.06], [2021, 1.331, 0.51], [2022, 1.154, 9.59], [2023, 1.131, -2.95], [2024, 1.109, -3.12], [2025, 0.777, -0.79], [2026, 0.344, 7.3]], TEAMCOL = '#C41E3A'; function contrastColor(hex,dark){let r=parseInt(hex.slice(1,3),16)/255,g=parseInt(hex.slice(3,5),16)/255,b=parseInt(hex.slice(5,7),16)/255; const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h=0,s=0,l=(mx+mn)/2; if(mx!==mn){const d=mx-mn;s=l>0.5?d/(2-mx-mn):d/(mx+mn);h=mx===r?(g-b)/d+(g1)t-=1;if(t<1/6)return p+(q-p)*6*t;if(t<0.5)return q;if(t<2/3)return p+(q-p)*(2/3-t)*6;return p;} let R,G,B;if(s===0){R=G=B=L;}else{const q=L<0.5?L*(1+s):L+s-L*s,p=2*L-q;R=h2(p,q,h+1/3);G=h2(p,q,h);B=h2(p,q,h-1/3);} const x=v=>('0'+Math.round(v*255).toString(16)).slice(-2);return '#'+x(R)+x(G)+x(B);} const CURRENT_YEAR = 1991; function drawOrbit(){ const orbEl=document.getElementById('orbit'); if(!ORBIT || ORBIT.length<2){ orbEl.innerHTML='

Not enough history to chart.

'; return; } const f=+document.getElementById('yfrom').value, t=+document.getElementById('yto').value, w=+document.getElementById('win').value; document.getElementById('yflab').textContent=f; document.getElementById('ytlab').textContent=t; document.getElementById('winlab').textContent = w===1?'raw':(w+'-yr'); const dark=document.documentElement.getAttribute('data-theme')==='dark'; const ink=dark?'#cdd4dc':'#1d2630', grid=dark?'#2c313a':'#d6dbe2', refc=dark?'#5a636d':'#9aa3ad', dirc=dark?'#ffffff':'#1d2630'; const col=contrastColor(TEAMCOL,dark); function sm(a,w){const h=(w-1)/2,o=[];for(let i=0;id[0]>=f&&d[0]<=t); if(raw.length<2){ Plotly.purge('orbit'); orbEl.innerHTML='

Pick a wider year range.

'; return; } const s=sm(raw,w),n=s.length,X=s.map(d=>Math.round(d[1]*100)/100),Y=s.map(d=>Math.round(d[2]*10)/10),yr=s.map(d=>d[0]); let xa=Math.min(1,...X),xb=Math.max(1,...X),ya=Math.min(0,...Y),yb=Math.max(0,...Y); const px=(xb-xa)*.12+.04,py=(yb-ya)*.12+1.5;xa-=px;xb+=px;ya-=py;yb+=py; const k=Math.max(0,n-3), ann=[]; [['WINDOW','#15b6a0',xa<1&&yb>0,xa,yb,'left','top'],['CONTEND','#2f7ed8',xb>1&&yb>0,xb,yb,'right','top'], ['FORCED-SPEND','#ec5a3c',xb>1&&ya<0,xb,ya,'right','bottom'],['REBUILD','#f0a91c',xa<1&&ya<0,xa,ya,'left','bottom']].forEach(q=>{ if(q[2]) ann.push({x:q[3],y:q[4],xref:'x',yref:'y',text:q[0],showarrow:false,xanchor:q[5],yanchor:q[6],font:{size:10,color:q[1]},opacity:.85});}); ann.push({x:X[n-1],y:Y[n-1],ax:X[n-1]-0.5*(X[n-1]-X[k]),ay:Y[n-1]-0.5*(Y[n-1]-Y[k]),xref:'x',yref:'y',axref:'x',ayref:'y',showarrow:true,arrowhead:2,arrowsize:1.3,arrowwidth:1.7,arrowcolor:dirc,text:''}); Plotly.newPlot('orbit',[{x:X,y:Y,mode:'lines+markers',line:{color:col,width:2.6,shape:'spline',smoothing:0.7}, marker:{color:col,size:s.map((d,i)=>5+7*(i/Math.max(1,n-1))),line:{color:dark?'#1b1e23':'#fff',width:0.6}}, text:yr,hovertemplate:'%{text}
payroll %{x:.2f}× lg · TAB %{y:+.1f}
'+(w===1?'single season (raw)':w+'-yr smoothed avg')+''}], {margin:{l:50,r:14,t:10,b:40},paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',font:FONT, xaxis:{title:{text:'relative payroll (× lg avg)',font:{size:11}},range:[xa,xb],gridcolor:grid,zeroline:false}, yaxis:{title:{text:'TAB (talent above budget)',font:{size:11}},range:[ya,yb],gridcolor:grid,zeroline:false}, shapes:[{type:'line',x0:1,x1:1,y0:ya,y1:yb,line:{color:refc,width:1,dash:'dot'}},{type:'line',x0:xa,x1:xb,y0:0,y1:0,line:{color:refc,width:1,dash:'dot'}}], annotations:ann},{displayModeBar:false,responsive:true}); } (function initOrbitSliders(){ const yf=document.getElementById('yfrom'), yt=document.getElementById('yto'); if(ORBIT && ORBIT.length>=2){ const yrs=ORBIT.map(d=>d[0]), y0=Math.min(...yrs), y1=Math.max(...yrs); yf.min=y0; yf.max=y1; yf.value=y0; yt.min=y0; yt.max=y1; yt.value=Math.max(y0, Math.min(CURRENT_YEAR, y1)); yf.addEventListener('input',()=>{ if(+yf.value>+yt.value) yt.value=yf.value; drawOrbit(); }); yt.addEventListener('input',()=>{ if(+yt.value<+yf.value) yf.value=yt.value; drawOrbit(); }); document.getElementById('win').addEventListener('input',drawOrbit); } // only paint on load if the orbit pane is the visible default (else it'd draw into a 0-width hidden box) const _op=document.querySelector('.gpane[data-g="orbit"]'); if(!_op || _op.style.display!=='none') drawOrbit(); })(); new MutationObserver(()=>{ const p=document.querySelector('.gpane[data-g="orbit"]'); if(!p || p.style.display!=='none') drawOrbit(); }).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); // ---- roster depth curve (WAR by roster slot vs league-average-by-slot) ---- const WARDIST = {"cats": ["IF", "C", "OF", "OF", "SP", "SP", "IF", "RP", "SP", "SP", "OF", "IF", "SP", "SP", "RP", "IF", "IF", "IF", "OF", "RP", "RP", "IF", "RP", "C", "RP", "RP", "IF", "RP", "IF", "SP", "RP", "IF", "IF", "C", "RP", "OF", "\u2014", "\u2014", "\u2014", "\u2014"], "league": [5.94, 4.76, 3.83, 3.3, 2.9, 2.49, 2.19, 1.86, 1.71, 1.54, 1.27, 1.13, 1.02, 0.89, 0.75, 0.65, 0.53, 0.43, 0.33, 0.25, 0.2, 0.14, 0.09, 0.07, 0.03, 0.0, -0.02, -0.05, -0.08, -0.1, -0.14, -0.17, -0.23, -0.28, -0.32, -0.38, -0.43, -0.52, -0.49, -0.58], "names": ["Ozzie Smith", "Tom Pagnozzi", "Milt Thompson", "Felix Jose", "Bryn Smith", "Bob Tewksbury", "Todd Zeile", "Lee Smith", "Ken Hill", "Omar Olivares", "Ray Lankford", "Jos\u00e9 Oquendo", "Rheal Cormier", "Jose DeLeon", "Scott Terry", "Geronimo Pena", "Pedro Guerrero", "Gerald Perry", "Bernard Gilkey", "Cris Carpenter", "Bob McClure", "Stan Royer", "Mark Grater", "Ray Stephens", "Mike Perez", "Tim Sherrill", "Tim Jones", "Mark Clark", "Luis Alicea", "Jamie Moyer", "Juan Agosto", "Rod Brewer", "Craig Wilson", "Rich Gedman", "Willie Fraser", "Rex Hudler", "(empty slot)", "(empty slot)", "(empty slot)", "(empty slot)"], "ranks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], "team": [5.07, 3.39, 3.21, 2.96, 2.87, 2.73, 2.59, 2.0, 1.96, 1.89, 1.72, 1.7, 1.61, 1.3, 1.19, 0.99, 0.77, 0.57, 0.51, 0.38, 0.34, 0.04, -0.02, -0.05, -0.06, -0.14, -0.18, -0.22, -0.22, -0.24, -0.34, -0.34, -0.78, -0.9, -0.93, -1.06, 0.0, 0.0, 0.0, 0.0]}; let WD_MODE='raw', WC_MODE='team'; const POSCOL={SP:'#2f7ed8',RP:'#6fb1ec',IF:'#f0a91c',OF:'#15b6a0',C:'#ec5a3c'}; const POSLAB={SP:'Starting P',RP:'Bullpen',IF:'Infield',OF:'Outfield',C:'Catcher'}; function buildPosLegend(){ const el=document.getElementById('poslegend'); if(!el) return; el.style.display=WC_MODE==='pos'?'flex':'none'; el.innerHTML=['SP','RP','C','IF','OF'].map(k=>''+POSLAB[k]+'').join(''); } function drawWarDist(){ if(!WARDIST) return; const dark=document.documentElement.getAttribute('data-theme')==='dark'; const col=contrastColor(TEAMCOL,dark), line=dark?'#e8eef0':'#111', grid=dark?'#2c313a':'#d6dbe2', ink=dark?'#cdd4dc':'#1d2630'; let team=WARDIST.team.slice(), league=WARDIST.league.slice(); if(WD_MODE==='prop'){ const ts=team.reduce((a,b)=>a+Math.max(b,0),0)||1, ls=league.reduce((a,b)=>a+Math.max(b,0),0)||1; team=team.map(v=>v/ts); league=league.map(v=>v/ls); } const fmt=WD_MODE==='prop'?'.1%':'.1f'; const names=WARDIST.names||[], cats=WARDIST.cats||[]; const cd=WARDIST.ranks.map((_,i)=>[names[i]||'(empty slot)', WARDIST.team[i], POSLAB[cats[i]]||'']); const barcol=WC_MODE==='pos'?cats.map(c=>POSCOL[c]||'#9aa3ad'):col; const posbit=WC_MODE==='pos'?' · %{customdata[2]}':''; const htmpl=WD_MODE==='prop' ? '%{customdata[0]}'+posbit+'
slot %{x} · %{customdata[1]:.1f} WAR
%{y:.1%} of roster WAR' : '%{customdata[0]}'+posbit+'
slot %{x} · %{y:.1f} WAR'; buildPosLegend(); const _div=document.getElementById('wardist'); const traces=[ {type:'bar',x:WARDIST.ranks,y:team,name:'STL',marker:{color:barcol},customdata:cd,hovertemplate:htmpl}, {type:'scatter',x:WARDIST.ranks,y:league,name:'league avg',mode:'lines',line:{color:line,width:2,shape:'spline',smoothing:0.6},hovertemplate:'league slot %{x}: %{y:'+fmt+'}'} ]; const layout={margin:{l:48,r:12,t:6,b:38},paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',font:FONT, transition:{duration:360,easing:'cubic-in-out'}, xaxis:{title:{text:'roster slot (ranked by WAR)',font:{size:11}},gridcolor:grid,zeroline:false,color:ink}, yaxis:{title:{text:WD_MODE==='prop'?'share of roster WAR':'WAR',font:{size:11}},gridcolor:grid,zeroline:true,zerolinecolor:grid,color:ink,tickformat:WD_MODE==='prop'?'.0%':undefined}, showlegend:true,legend:{orientation:'h',y:1.13,x:0,font:{size:11}},bargap:0.14}; if(_div.data&&_div.data.length){ Plotly.react(_div,traces,layout,{displayModeBar:false,responsive:true}); } else { Plotly.newPlot(_div,traces,layout,{displayModeBar:false,responsive:true}); } } window.wdMode=function(m){WD_MODE=m;document.getElementById('wd-raw').classList.toggle('on',m==='raw');document.getElementById('wd-prop').classList.toggle('on',m==='prop');drawWarDist();}; window.wcToggle=function(){WC_MODE=WC_MODE==='pos'?'team':'pos';const b=document.getElementById('wc-btn');b.classList.toggle('on',WC_MODE==='pos');b.textContent=WC_MODE==='pos'?'Show team color':'Color by position';drawWarDist();}; // ---- Graphs dropdown switcher (orbit ⇄ roster value distribution) ---- let _wdRendered=false; window.showTeamGraph=function(g){ document.querySelectorAll('.gpane').forEach(p=>{ p.style.display=(p.dataset.g===g)?'':'none'; }); if(g==='orbit'){ drawOrbit(); } else if(g==='roster'){ if(!_wdRendered){ drawWarDist(); _wdRendered=true; } else { try{ Plotly.Plots.resize('wardist'); }catch(e){ drawWarDist(); } } } }; { const _ts=document.getElementById('tgsel'); if(_ts) _ts.addEventListener('change',()=>showTeamGraph(_ts.value)); } // initial paint of the default pane (roster when the team has roster data, else orbit already drawn above) if(WARDIST){ drawWarDist(); _wdRendered=true; } // redraw the roster pane on theme change only if it's been rendered and is visible if(WARDIST){ new MutationObserver(()=>{ const p=document.querySelector('.gpane[data-g="roster"]'); if(_wdRendered && p && p.style.display!=='none') drawWarDist(); }).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); } // ---- TOC scroll-spy ---- (function(){ const links=[...document.querySelectorAll('.toc a')]; const secs=links.map(a=>document.querySelector(a.getAttribute('href'))).filter(Boolean); links.forEach(a=>a.addEventListener('click',e=>{e.preventDefault();document.querySelector(a.getAttribute('href')).scrollIntoView({behavior:'smooth'});})); if('IntersectionObserver' in window && secs.length){ const obs=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting) links.forEach(l=>l.classList.toggle('on',l.getAttribute('href')==='#'+e.target.id));});},{rootMargin:'-15% 0px -75% 0px'}); secs.forEach(s=>obs.observe(s)); } })(); // ---- sortable roster table ---- const SVC_RANK={'pre-arb':0,'arb':1,'FA-elig':2,'?':3}; function cellNum(td){const n=parseFloat(td.textContent.replace(/[^0-9.\-]/g,''));return isNaN(n)?null:n;} function scale(v,mn,mx,g){ if(g==='diverge'){ if(v>=0){const t=mx>0?v/mx:0;return `rgba(26,125,90,${(0.10+0.55*t).toFixed(2)})`;} const t=mn<0?v/mn:0;return `rgba(192,73,43,${(0.10+0.55*t).toFixed(2)})`; } if(g==='salmag'){const t=mx>mn?(v-mn)/(mx-mn):0;return `rgba(128,135,143,${(0.05+0.45*t).toFixed(2)})`;} const t=mx>mn?(v-mn)/(mx-mn):0;return `rgba(26,125,90,${(0.05+0.55*t).toFixed(2)})`; } function applyGradients(table){ [...table.tHead.rows[0].cells].forEach((th,ci)=>{ if(!th.dataset.grad) return; const rows=[...table.tBodies[0].rows], vals=rows.map(r=>cellNum(r.cells[ci])).filter(v=>v!==null); if(!vals.length) return; const mn=Math.min(...vals),mx=Math.max(...vals); rows.forEach(r=>{const v=cellNum(r.cells[ci]);if(v!==null)r.cells[ci].style.background=scale(v,mn,mx,th.dataset.grad);}); }); } function makeSortable(table){ const ths=[...table.tHead.rows[0].cells]; ths.forEach((th,ci)=>{ th.dataset.label=th.textContent.trim(); th.addEventListener('click',()=>{ th._dir=(th._dir===-1)?1:-1; const dir=th._dir, tb=table.tBodies[0], svc=th.dataset.sortType==='service'; [...tb.rows].sort((a,b)=>{ let va,vb; if(svc){va=SVC_RANK[a.cells[ci].textContent.trim()]??9;vb=SVC_RANK[b.cells[ci].textContent.trim()]??9;} else{va=cellNum(a.cells[ci]);vb=cellNum(b.cells[ci]); if(va===null||vb===null){va=a.cells[ci].textContent.trim().toLowerCase();vb=b.cells[ci].textContent.trim().toLowerCase();}} return vavb?dir:0; }).forEach(r=>tb.appendChild(r)); ths.forEach(h=>h.innerHTML=h.dataset.label); th.innerHTML=th.dataset.label+' '+(dir===1?'▲':'▼')+''; }); }); } document.querySelectorAll('table.sortable').forEach(t=>{applyGradients(t);makeSortable(t);});