I-94 CORRIDOR COLLABORATIVE

Dashboard Mockup


<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Helvetica Neue',Arial,sans-serif}

.db{background:var(--color-background-primary)}
.topbar{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:0.5px solid var(--color-border-tertiary);background:var(--color-background-secondary);flex-wrap:nowrap;overflow-x:auto}
.topbar label{font-size:12px;color:var(--color-text-secondary);white-space:nowrap}
.topbar select{font-size:13px;padding:4px 10px;border-radius:var(--border-radius-md);border:0.5px solid var(--color-border-secondary);background:var(--color-background-primary);color:var(--color-text-primary);font-weight:500}
#fSeason{min-width:130px}
#fProd{min-width:160px}
#fArea{min-width:180px}
.tabrow{display:flex;gap:5px;padding:8px 14px;border-bottom:0.5px solid var(--color-border-tertiary);flex-wrap:wrap}
.tab{padding:5px 11px;border-radius:var(--border-radius-md);border:0.5px solid var(--color-border-tertiary);font-size:11px;cursor:pointer;background:var(--color-background-primary);color:var(--color-text-secondary);transition:all .15s;white-space:nowrap}
.tab.on{background:var(--color-background-secondary);color:var(--color-text-primary);border-color:var(--color-border-secondary);font-weight:500}
.view{padding:14px;display:none}
.view.on{display:block}
.desc{font-size:11px;color:var(--color-text-secondary);line-height:1.5;margin-bottom:10px}
.story-box{border-radius:var(--border-radius-md);padding:8px 12px;margin-bottom:12px;font-size:11px;line-height:1.6}
.story-surplus{background:#EAF3DE;color:#27500A}
.story-gap{background:#FCEBEB;color:#791F1F}
.story-mixed{background:#FAEEDA;color:#633806}
.story-neutral{background:var(--color-background-secondary);color:var(--color-text-secondary)}
.story-label{font-weight:500;margin-right:4px}
.mrow{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:12px}
.mc{background:var(--color-background-secondary);border-radius:var(--border-radius-md);padding:8px 10px}
.mc .ml{font-size:10px;color:var(--color-text-secondary);margin-bottom:2px}
.mc .mv{font-size:18px;font-weight:500;color:var(--color-text-primary)}
.mc .ms{font-size:10px;color:var(--color-text-secondary)}
.hm-wrap{overflow-x:auto}
.hmt{border-collapse:collapse;width:100%;min-width:520px}
.hmt th{font-size:9px;font-weight:500;color:var(--color-text-secondary);padding:2px 3px;text-align:center;width:38px}
.rl{font-size:10px;font-weight:500;color:var(--color-text-primary);text-align:right;padding-right:6px;white-space:nowrap;vertical-align:middle}
.hc{width:38px;height:20px;border-radius:2px}
.s0{background:#f0efe8}.s1{background:#C0DD97}.s2{background:#639922}.s3{background:#27500A}
.d0{background:#f0efe8}.d1{background:#F5C4B3}.d2{background:#D85A30}.d3{background:#993C1D}
.mm{outline:2px solid #BA7517;outline-offset:-2px}
.lgrow{display:flex;gap:16px;margin-top:10px;flex-wrap:wrap;align-items:center;font-size:10px;color:var(--color-text-secondary)}
.sws{display:flex;gap:2px}
.sw{width:12px;height:12px;border-radius:2px;flex-shrink:0}
.clegend{display:flex;gap:16px;margin-bottom:6px;font-size:11px;color:var(--color-text-secondary);flex-wrap:wrap}
.cleg-item{display:flex;align-items:center;gap:4px}
.cleg-sq{width:11px;height:11px;border-radius:2px;flex-shrink:0}
.net-area{overflow-x:auto}
.net-svg{width:100%;min-width:480px}
.spag-svg{width:100%;min-width:480px}
.context-note{font-size:11px;color:var(--color-text-secondary);background:var(--color-background-secondary);border-radius:var(--border-radius-md);padding:7px 10px;margin-bottom:12px;line-height:1.5}
</style>

<h2 style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)">I-94 Corridor 5-view supply chain dashboard</h2>

<div class="db">
<div class="topbar">
  <span id="seasonFilter" style="display:flex;align-items:center;gap:8px">
    <label>Season</label>
    <select id="fSeason"><option>All seasons</option><option>Spring</option><option>Summer</option><option>Fall</option><option>Winter</option></select>
  </span>
  <label style="margin-left:8px">Product</label>
  <select id="fProd">
    <option value="all">All products</option>
    <option value="veg">Vegetables</option>
    <option value="fruit">Fruit</option>
    <option value="eggs">Liquid eggs</option>
    <option value="poultry">Poultry</option>
    <option value="dairy">Dairy</option>
    <option value="grain">Grain</option>
  </select>
  <label style="margin-left:8px">Area</label>
  <select id="fArea">
    <option value="all">All areas</option>
    <option value="mn">Minneapolis / MN</option>
    <option value="wi-west">Eau Claire / WI</option>
    <option value="wi-south">Madison / WI</option>
    <option value="il-rock">Rockford / IL</option>
    <option value="il-chi">Chicagoland / IL</option>
  </select>
</div>

<div class="tabrow">
  <button class="tab on" data-v="sd">1. Supply & demand</button>
  <button class="tab" data-v="gap">2. Supply & demand gap</button>
  <button class="tab" data-v="net">3. Network</button>
  <button class="tab" data-v="spag">4. Spaghetti map</button>
  <button class="tab" data-v="wf">5. Cost-stack waterfall</button>
</div>

<!-- VIEW 1 -->
<div class="view on" id="v-sd">
  <p class="desc">Each product shows paired rows: <em>supply</em> from corridor growers (green, top) and <em>demand</em> from anchor institutions (red, bottom). Amber outline = critical mismatch.</p>
  <div id="storyBox" class="story-box story-neutral"><span class="story-label">Select filters</span> to see the supply/demand story for that combination.</div>
  <div class="mrow">
    <div class="mc"><div class="ml">Surplus months</div><div class="mv" id="sdSurplus">—</div></div>
    <div class="mc"><div class="ml">Gap months</div><div class="mv" id="sdGap">—</div></div>
    <div class="mc"><div class="ml">Peak supply</div><div class="mv" id="sdPeak">—</div></div>
    <div class="mc"><div class="ml">Critical mismatches</div><div class="mv" id="sdMM">—</div></div>
  </div>
  <div class="hm-wrap"><table class="hmt" id="hmtable"></table></div>
  <div class="lgrow">
    <span style="font-weight:500">Supply:</span>
    <div class="sws"><div class="sw s3"></div><div class="sw s2"></div><div class="sw s1"></div><div class="sw s0" style="border:0.5px solid #ddd"></div></div>
    <span>High · Mod · Low · Off</span>
    <span style="margin-left:8px;font-weight:500">Demand:</span>
    <div class="sws"><div class="sw d3"></div><div class="sw d2"></div><div class="sw d1"></div><div class="sw d0" style="border:0.5px solid #ddd"></div></div>
    <span>High · Mod · Low · Off</span>
    <span style="display:flex;align-items:center;gap:4px;margin-left:8px"><div class="sw s1 mm"></div> Gap / mismatch</span>
  </div>
</div>

<!-- VIEW 2 -->
<div class="view" id="v-gap">
  <p class="desc">Total corridor supply vs. institutional demand by month. Green = surplus. Red = procurement gap.</p>
  <div class="mrow">
    <div class="mc"><div class="ml">Months with gap</div><div class="mv" id="gM">-</div></div>
    <div class="mc"><div class="ml">Annual shortfall</div><div class="mv" id="gS">-</div><div class="ms">thousand lbs</div></div>
    <div class="mc"><div class="ml">Surplus window</div><div class="mv" id="gW" style="font-size:14px;margin-top:2px">-</div></div>
    <div class="mc"><div class="ml">Peak supply month</div><div class="mv" id="gP">-</div></div>
  </div>
  <div class="clegend">
    <span class="cleg-item"><span style="width:20px;height:3px;background:#27500A;display:inline-block"></span> Supply</span>
    <span class="cleg-item"><span style="width:20px;height:0;border-top:2px dashed #993C1D;display:inline-block"></span> Demand</span>
    <span class="cleg-item"><span class="cleg-sq" style="background:#EAF3DE"></span> Surplus</span>
    <span class="cleg-item"><span class="cleg-sq" style="background:#FCEBEB"></span> Gap</span>
  </div>
  <div style="position:relative;width:100%;height:240px"><canvas id="gapC" role="img" aria-label="Monthly supply vs demand gap chart">Supply and demand comparison.</canvas></div>
</div>

<!-- VIEW 3: NETWORK -->
<div class="view" id="v-net">
  <p class="desc">Nodes grouped by functional role, edges weighted by active flow. Faded nodes = inactive for this filter combination. Edge thickness = relative volume. Amber edges = bottleneck or broker dependency.</p>
  <div class="mrow" style="grid-template-columns:repeat(3,minmax(0,1fr))">
    <div class="mc"><div class="ml">Active nodes</div><div class="mv" id="nNodes">—</div></div>
    <div class="mc"><div class="ml">Active edges</div><div class="mv" id="nEdges">—</div></div>
    <div class="mc"><div class="ml">Bottleneck role</div><div class="mv" style="font-size:14px;margin-top:2px" id="nBottle">—</div></div>
  </div>
  <div id="netStory" class="context-note"></div>
  <div class="clegend">
    <span class="cleg-item"><span style="width:11px;height:11px;border-radius:50%;background:#639922;display:inline-block"></span> Farm</span>
    <span class="cleg-item"><span style="width:11px;height:11px;border-radius:2px;background:#1D9E75;display:inline-block"></span> Aggregator</span>
    <span class="cleg-item"><svg width="13" height="13" viewBox="0 0 13 13"><polygon points="6.5,0 13,13 0,13" fill="#BA7517"/></svg> Processor</span>
    <span class="cleg-item"><svg width="13" height="13" viewBox="0 0 13 13"><polygon points="6.5,1 12,6.5 6.5,12 1,6.5" fill="#378ADD"/></svg> Wholesaler</span>
    <span class="cleg-item"><span style="width:11px;height:11px;border-radius:2px;background:#D85A30;display:inline-block"></span> Institution</span>
    <span class="cleg-item"><span style="width:11px;height:11px;border-radius:50%;background:none;border:2px solid #7F77DD;display:inline-block"></span> Broker</span>
  </div>
  <div class="net-area"><svg id="netSvg" class="net-svg" viewBox="0 0 560 380"></svg></div>
</div>

<!-- VIEW 4 -->
<div class="view" id="v-spag">
  <p class="desc">Every product movement drawn as an arc. Line weight = volume, color = product family.</p>
  <div class="mrow">
    <div class="mc"><div class="ml">Truck-miles / week</div><div class="mv">18,400</div></div>
    <div class="mc"><div class="ml">Avg load factor</div><div class="mv">62%</div></div>
    <div class="mc"><div class="ml">Cross-corridor trips</div><div class="mv">14</div></div>
    <div class="mc"><div class="ml">Distinct lanes</div><div class="mv">23</div></div>
  </div>
  <div class="clegend">
    <span class="cleg-item"><span class="cleg-sq" style="background:#639922"></span> Vegetables</span>
    <span class="cleg-item"><span class="cleg-sq" style="background:#D85A30"></span> Fruit</span>
    <span class="cleg-item"><span class="cleg-sq" style="background:#378ADD"></span> Eggs / dairy</span>
    <span class="cleg-item"><span class="cleg-sq" style="background:#7F77DD"></span> Poultry / grain</span>
  </div>
  <div style="overflow-x:auto"><svg id="spagSvg" class="spag-svg" viewBox="0 0 560 300"></svg></div>
</div>

<!-- VIEW 5 -->
<div class="view" id="v-wf">
  <p class="desc">How $1 of farmgate price becomes the institution-paid price. Costs shift by product, season, and delivery area.</p>
  <div id="wfNote" class="context-note"></div>
  <div class="mrow">
    <div class="mc"><div class="ml">Farmgate</div><div class="mv" id="wfFG">-</div></div>
    <div class="mc"><div class="ml">Institution price</div><div class="mv" id="wfIP">-</div></div>
    <div class="mc"><div class="ml">Markup</div><div class="mv" id="wfMU">-</div></div>
    <div class="mc"><div class="ml">Farmer share</div><div class="mv" id="wfFS">-</div></div>
  </div>
  <div class="clegend">
    <span class="cleg-item"><span class="cleg-sq" style="background:#639922"></span> Farmgate</span>
    <span class="cleg-item"><span class="cleg-sq" style="background:#85B7EB"></span> Supply chain costs</span>
    <span class="cleg-item"><span class="cleg-sq" style="background:#D85A30"></span> Institution price</span>
  </div>
  <div style="position:relative;width:100%;height:300px"><canvas id="wfC" role="img" aria-label="Cost-stack waterfall">Cost waterfall from farmgate to institution price.</canvas></div>
</div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<script>
const MONTHS=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const VIEWS_WITHOUT_SEASON=['sd','gap'];
function updateSeasonVisibility(v){document.getElementById('seasonFilter').style.display=VIEWS_WITHOUT_SEASON.includes(v)?'none':'flex';}

/* ── ALL NETWORK NODES ─────────────────────────────────────────────── */
const ALL_NODES = [
  {id:'tga_cluster',  col:0,row:0, type:'farm',  label:'TGA Clusters MN',  area:'mn',      prods:['veg','poultry','eggs','dairy','fruit'], seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'ferndale',     col:0,row:1, type:'farm',  label:'Ferndale Turkey',  area:'mn',      prods:['poultry'],                               seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'garden_eggs',  col:0,row:2, type:'farm',  label:'Garden Harvest Eggs', area:'mn',   prods:['eggs'],                                  seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'door_orchards',col:0,row:3, type:'farm',  label:'Door Co. Orchards',area:'wi-west', prods:['fruit'],                                 seasons:['Summer','Fall','All seasons']},
  {id:'stcroix',      col:0,row:4, type:'farm',  label:'St. Croix Cheese', area:'mn',      prods:['dairy'],                                 seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'meadowlark',   col:0,row:5, type:'farm',  label:'Meadowlark Grain', area:'wi-south',prods:['grain'],                                 seasons:['Fall','Winter','All seasons']},
  {id:'tga',          col:1,row:0, type:'agg',   label:'The Good Acre',    area:'mn',      prods:['veg','fruit','eggs','poultry','dairy'],   seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'wifh',         col:1,row:2, type:'agg',   label:'WI Food Hub',      area:'wi-west', prods:['veg','fruit'],                           seasons:['Spring','Summer','Fall','All seasons']},
  {id:'fairshare',    col:1,row:4, type:'agg',   label:'FairShare CSA',    area:'wi-south',prods:['veg','grain'],                           seasons:['Spring','Summer','Fall','All seasons']},
  {id:'cmgl',         col:1,row:5, type:'agg',   label:'Common Mkt GL',    area:'il-chi',  prods:['veg','fruit','grain'],                   seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'snopac',       col:2,row:0, type:'proc',  label:'Sno Pac (IQF)',    area:'mn',      prods:['veg'],                                   seasons:['Summer','Fall','Winter','All seasons']},
  {id:'ediblecuts',   col:2,row:2, type:'proc',  label:'Edible Cuts',      area:'il-chi',  prods:['veg','fruit'],                           seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'broker_mn',    col:2,row:1, type:'broker',label:'Broker (MN)',      area:'mn',      prods:['eggs','poultry'],                        seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'broker_wi',    col:2,row:3, type:'broker',label:'Broker (WI)',      area:'wi-west', prods:['veg','fruit'],                           seasons:['Spring','Summer','Fall','All seasons']},
  {id:'mwf_chi',      col:3,row:0, type:'whl',   label:'MWF Chicago',      area:'il-chi',  prods:['veg','fruit','eggs','poultry'],          seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'mwf_mid',      col:3,row:2, type:'whl',   label:'Midwest Foods',    area:'wi-south',prods:['veg','grain','dairy'],                   seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'mwf_packers',  col:3,row:4, type:'whl',   label:'MWF Packers',      area:'wi-west', prods:['poultry','dairy'],                       seasons:['Fall','Winter','All seasons']},
  {id:'mayo',         col:4,row:0, type:'inst',  label:'Mayo Clinic',      area:'mn',      prods:['veg','eggs','poultry'],                  seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'uwhealth',     col:4,row:1, type:'inst',  label:'UW Health',        area:'wi-south',prods:['veg','grain','dairy'],                   seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'cps',          col:4,row:2, type:'inst',  label:'Chicago PS',       area:'il-chi',  prods:['veg','fruit','eggs'],                    seasons:['Spring','Fall','Winter','All seasons']},
  {id:'advocate',     col:4,row:3, type:'inst',  label:'Advocate Rush',    area:'il-chi',  prods:['veg','eggs','grain'],                    seasons:['Spring','Summer','Fall','Winter','All seasons']},
  {id:'rockford_isd', col:4,row:4, type:'inst',  label:'Rockford ISD',     area:'il-rock', prods:['veg','fruit'],                           seasons:['Spring','Summer','Fall','Winter','All seasons']},
];

/* ── ALL EDGES ─────────────────────────────────────────────────────── */
const ALL_EDGES = [
  {from:'tga_cluster', to:'tga',        prods:['veg','poultry','dairy','fruit'], seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:3},
  {from:'ferndale',    to:'tga',        prods:['poultry'],                       seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'garden_eggs', to:'tga',        prods:['eggs'],                          seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'stcroix',     to:'tga',        prods:['dairy'],                         seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:1},
  {from:'door_orchards',to:'wifh',      prods:['fruit'],                         seasons:['Summer','Fall','All seasons'],                   vol:2},
  {from:'meadowlark',  to:'fairshare',  prods:['grain'],                         seasons:['Fall','Winter','All seasons'],                   vol:2},
  {from:'tga',         to:'snopac',     prods:['veg'],                           seasons:['Summer','Fall','Winter','All seasons'],          vol:2},
  {from:'tga',         to:'broker_mn',  prods:['eggs','poultry'],                seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'tga',         to:'mayo',       prods:['veg','eggs','poultry'],          seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'wifh',        to:'broker_wi',  prods:['veg','fruit'],                   seasons:['Spring','Summer','Fall','All seasons'],          vol:2},
  {from:'wifh',        to:'mwf_packers',prods:['veg'],                           seasons:['Summer','Fall','All seasons'],                   vol:1},
  {from:'fairshare',   to:'mwf_mid',    prods:['veg','grain'],                   seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'fairshare',   to:'uwhealth',   prods:['veg','grain'],                   seasons:['Spring','Summer','Fall','All seasons'],          vol:1},
  {from:'snopac',      to:'mwf_chi',    prods:['veg'],                           seasons:['Winter','Fall','All seasons'],                   vol:2},
  {from:'ediblecuts',  to:'mwf_chi',    prods:['veg','fruit'],                   seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'broker_mn',   to:'mwf_chi',    prods:['eggs','poultry'],                seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'broker_wi',   to:'mwf_mid',    prods:['veg','fruit'],                   seasons:['Spring','Summer','Fall','All seasons'],          vol:2},
  {from:'broker_wi',   to:'ediblecuts', prods:['veg','fruit'],                   seasons:['Summer','Fall','All seasons'],                   vol:1},
  {from:'mwf_chi',     to:'cps',        prods:['veg','fruit','eggs'],            seasons:['Spring','Fall','Winter','All seasons'],          vol:3},
  {from:'mwf_chi',     to:'advocate',   prods:['veg','eggs'],                    seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'mwf_mid',     to:'uwhealth',   prods:['veg','grain','dairy'],           seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'mwf_mid',     to:'rockford_isd',prods:['veg'],                          seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:1},
  {from:'mwf_packers', to:'rockford_isd',prods:['poultry','dairy'],              seasons:['Fall','Winter','All seasons'],                   vol:1},
  {from:'cmgl',        to:'cps',        prods:['veg','fruit'],                   seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
  {from:'cmgl',        to:'advocate',   prods:['veg','grain'],                   seasons:['Spring','Summer','Fall','Winter','All seasons'], vol:2},
];

/* ── NETWORK STORIES ───────────────────────────────────────────────── */
const NET_STORIES = {
  buildKey: (prod,season,area) => `${prod}|${season}|${area}`,
  get: (prod,season,area) => {
    const stories = {
      'all|All seasons|all':   {bottle:'Brokers', text:'Full corridor network. The Good Acre and MWF Chicago are the two highest-degree nodes — most product flows through one of them. Brokers are the structural bottleneck: removing either broker disconnects the MN supply from Chicago institutions.'},
      'all|Summer|all':        {bottle:'Processors', text:'Summer: IQF processors (Sno Pac, Edible Cuts) are the bottleneck — peak harvest surplus must move through processing before winter institutional sales. Broker load is lower as fresh product flows direct.'},
      'all|Winter|all':        {bottle:'Processors', text:'Winter: Sno Pac IQF is the critical node. All fresh veg supply has stopped; frozen product from summer processing is the only corridor supply. Edible Cuts handles fruit. Network is thinner — many farm and aggregator nodes go inactive.'},
      'all|Spring|all':        {bottle:'Brokers', text:'Spring: supply chain is thin. Few farm nodes are active. Brokers absorb coordination burden as aggregators wait for harvest ramp-up. CPS and Advocate Rush depend heavily on broker-mediated MN supply.'},
      'all|Fall|all':          {bottle:'Aggregators', text:'Fall: peak network activity. Most farm and aggregator nodes active simultaneously. The Good Acre and WI Food Hub both running near capacity. Volume is high enough that aggregators — not brokers — become the binding constraint.'},
      'veg|All seasons|all':   {bottle:'Brokers', text:'Vegetables only. TGA Clusters → The Good Acre → Sno Pac/Broker MN is the primary chain. WI Food Hub handles WI-origin veg. FairShare feeds Madison. Two broker nodes sit between MN supply and Chicago buyers.'},
      'veg|Summer|all':        {bottle:'Aggregators', text:'Summer vegetables: maximum supply, maximum aggregation load. The Good Acre and WI Food Hub both active. Sno Pac begins absorbing surplus for IQF. Direct institutional sales possible without broker mediation.'},
      'veg|Winter|all':        {bottle:'Processors', text:'Winter vegetables: only Sno Pac IQF product available. Farm and field aggregator nodes go dark. MWF Chicago distributes frozen product. Network shrinks to processor → wholesaler → institution.'},
      'fruit|All seasons|all': {bottle:'Broker (WI)', text:'Fruit network is WI-heavy. Door County Orchards → WI Food Hub → Broker WI is the supply spine. Broker WI routes to Edible Cuts for processing and to MWF Mid for distribution. No MN fruit supply of significance.'},
      'fruit|Summer|all':      {bottle:'Edible Cuts', text:'Peak fruit season. Door County cherries and Driftless fruit flow at volume. Edible Cuts (Kenosha) is the processing bottleneck for cut/processed fruit products destined for Chicago institutions.'},
      'fruit|Winter|all':      {bottle:'Processors', text:'Winter fruit: supply drops sharply. Only storage apples and IQF berry product available. Network contracts — most farm and WI aggregator nodes inactive. Edible Cuts and Sno Pac carry remaining flow.'},
      'eggs|All seasons|all':  {bottle:'Broker (MN)', text:'Liquid eggs flow almost entirely from MN. Garden Harvest → The Good Acre → Broker MN → MWF Chicago is the primary chain. The MN Broker is a single point of failure: it is the only route from MN egg supply to Chicago healthcare buyers.'},
      'eggs|Winter|all':       {bottle:'Broker (MN)', text:'Winter egg crisis scenario. Garden Harvest is year-round but all distribution passes through Broker MN. With no WI/IL egg supply, the $70K documented supply gap replicates if this broker relationship fails.'},
      'poultry|All seasons|all':{bottle:'Broker (MN)', text:'Poultry network is MN-concentrated. Ferndale Turkey → The Good Acre → Broker MN is the dominant path. MWF Packers serves the WI-west corridor. No Chicagoland or IL poultry production of significance.'},
      'dairy|All seasons|all': {bottle:'Aggregators', text:'Dairy is the most stable network. St. Croix Cheese flows through TGA year-round. WI dairy (Lamers via MWF Mid) covers the Madison and Rockford nodes. No broker dependency — direct aggregator-to-institution relationships.'},
      'grain|All seasons|all': {bottle:'FairShare CSA', text:'Grain network is Madison-centered. Meadowlark Organics → FairShare → MWF Mid is the primary chain. FairShare is the single aggregator for grain — a structural bottleneck. Chicagoland grain flows via Common Market GL from IL sources.'},
      'grain|Fall|all':        {bottle:'FairShare CSA', text:'Fall is peak grain season (post-harvest). Meadowlark and Artisan Grain both active. FairShare CSA manages the aggregation surge. UW Health and Advocate Rush are the primary buyers. Volume is highest of the year.'},
      'all|All seasons|mn':    {bottle:'Broker (MN)', text:'MN area filter: network shows MN-origin supply chains only. The Good Acre is the dominant aggregator. Mayo Clinic is the primary institutional buyer. Broker MN routes eggs and poultry to out-of-area buyers not shown.'},
      'all|All seasons|wi-west':{bottle:'WI Food Hub', text:'WI-West area: Wisconsin Food Hub is the hub node. Door County Orchards and WI veg farms feed into it. Gap: WIFH has limited sales capacity and relies on MN brokers for institutional connections to Chicago.'},
      'all|All seasons|wi-south':{bottle:'FairShare CSA', text:'Madison area: FairShare and MWF Mid dominate. Strong grain and dairy supply (Meadowlark, Lamers). UW Health is anchor institution. FairShare CSA is the structural bottleneck for grain aggregation.'},
      'all|All seasons|il-rock':{bottle:'MWF Mid', text:'Rockford is a supply-thin demand node. All product arrives via MWF Mid from WI/MN. Rockford ISD is the only active institution. MWF Mid is the single point of failure for all Rockford supply.'},
      'all|All seasons|il-chi': {bottle:'MWF Chicago', text:'Chicagoland: massive demand, minimal supply. MWF Chicago and Common Market GL are the two wholesaler nodes connecting corridor supply to CPS, Advocate Rush, and U of Chicago. Edible Cuts (Kenosha) provides local processing.'},
    };
    const key = `${prod}|${season}|${area}`;
    return stories[key] || stories[`${prod}|All seasons|all`] || stories[`all|${season}|all`] || stories['all|All seasons|all'];
  }
};

function getArea(){return document.getElementById('fArea').value;}
function getSeason(){return document.getElementById('fSeason').value;}
function getProd(){return document.getElementById('fProd').value;}

function nodeActive(n, prod, season, area){
  const prodOk  = prod==='all'  || n.prods.includes(prod);
  const seasOk  = season==='All seasons' || n.seasons.includes(season);
  const areaOk  = area==='all'  || n.area===area || n.type==='broker';
  return prodOk && seasOk && areaOk;
}
function edgeActive(e, prod, season, area){
  const prodOk  = prod==='all'  || e.prods.includes(prod);
  const seasOk  = season==='All seasons' || e.seasons.includes(season);
  const fromN   = ALL_NODES.find(n=>n.id===e.from);
  const toN     = ALL_NODES.find(n=>n.id===e.to);
  const areaOk  = area==='all'  ||
    (fromN&&(fromN.area===area||fromN.type==='broker')) ||
    (toN&&(toN.area===area||toN.type==='broker'));
  return prodOk && seasOk && areaOk;
}

function buildNet(){
  const prod=getProd(), season=getSeason(), area=getArea();
  const svg=document.getElementById('netSvg');
  const COLS=[{label:'Farms',x:20},{label:'Aggregators',x:135},{label:'Processors',x:250},{label:'Wholesalers',x:365},{label:'Institutions',x:475}];
  const rowH=54, padY=44;
  function nx(n){return COLS[n.col].x+38}
  function ny(n){return padY+n.row*rowH}

  const fills={farm:'#639922',agg:'#1D9E75',proc:'#BA7517',whl:'#378ADD',inst:'#D85A30'};
  const strks={farm:'#27500A',agg:'#085041',proc:'#633806',whl:'#0C447C',inst:'#712B13'};

  const activeNodes = new Set(ALL_NODES.filter(n=>nodeActive(n,prod,season,area)).map(n=>n.id));
  const activeEdges = ALL_EDGES.filter(e=>edgeActive(e,prod,season,area)&&activeNodes.has(e.from)&&activeNodes.has(e.to));
  const nCount = activeNodes.size;
  const eCount = activeEdges.length;

  const story = NET_STORIES.get(prod, season, area);
  document.getElementById('nNodes').textContent = nCount;
  document.getElementById('nEdges').textContent = eCount;
  document.getElementById('nBottle').textContent = story.bottle;
  document.getElementById('netStory').textContent = story.text;

  let html='';
  COLS.forEach(c=>{
    html+=`<text x="${c.x+38}" y="20" text-anchor="middle" font-size="11" fill="var(--color-text-secondary)">${c.label}</text>`;
  });

  activeEdges.forEach(e=>{
    const na=ALL_NODES.find(n=>n.id===e.from), nb=ALL_NODES.find(n=>n.id===e.to);
    if(!na||!nb) return;
    const isBroker=na.type==='broker'||nb.type==='broker';
    const sw = Math.max(1, Math.min(e.vol, 3));
    const color = isBroker ? '#BA7517' : '#bbb';
    const op = isBroker ? 0.85 : 0.55;
    html+=`<line x1="${nx(na)}" y1="${ny(na)}" x2="${nx(nb)}" y2="${ny(nb)}" stroke="${color}" stroke-width="${sw}" opacity="${op}" fill="none"/>`;
  });

  ALL_NODES.forEach(n=>{
    const x=nx(n), y=ny(n);
    const active=activeNodes.has(n.id);
    const op=active?1:0.12;
    const f=fills[n.type]||'#888', s=strks[n.type]||'#555';
    let shape='';
    if(n.type==='broker'){
      shape=`<circle cx="${x}" cy="${y}" r="9" fill="none" stroke="${active?'#7F77DD':'#ccc'}" stroke-width="2" opacity="${op}"/>`;
    } else if(n.type==='proc'){
      shape=`<polygon points="${x},${y-11} ${x+10},${y+7} ${x-10},${y+7}" fill="${f}" stroke="${s}" stroke-width="1.5" opacity="${op}"/>`;
    } else if(n.type==='whl'){
      shape=`<polygon points="${x},${y-10} ${x+10},${y} ${x},${y+10} ${x-10},${y}" fill="${f}" stroke="${s}" stroke-width="1.5" opacity="${op}"/>`;
    } else {
      shape=`<circle cx="${x}" cy="${y}" r="10" fill="${f}" stroke="${s}" stroke-width="1.5" opacity="${op}"/>`;
    }
    html+=shape;
    const lbl=n.label.length>14?n.label.slice(0,13)+'…':n.label;
    html+=`<text x="${x}" y="${y+22}" text-anchor="middle" font-size="8" fill="var(--color-text-secondary)" opacity="${op}" style="pointer-events:none">${lbl}</text>`;
  });

  svg.innerHTML=html;
}

/* ── SUPPLY/DEMAND DATA ─────────────────────────────────────────────── */
const SUPPLY_BY_AREA={
  veg:{all:[1,1,1,2,2,3,3,3,3,2,1,0],mn:[1,1,2,2,3,3,3,3,3,2,2,1],'wi-west':[1,1,1,2,2,3,3,3,2,2,1,0],'wi-south':[0,0,1,1,2,2,2,2,2,1,1,0],'il-rock':[0,0,0,1,1,2,2,2,1,1,0,0],'il-chi':[0,0,0,0,1,1,2,2,1,0,0,0]},
  fruit:{all:[1,0,0,0,0,1,2,3,3,3,2,1],mn:[0,0,0,0,0,1,1,2,3,3,2,1],'wi-west':[0,0,0,0,0,1,2,3,3,3,2,1],'wi-south':[0,0,0,0,0,0,1,2,2,2,1,0],'il-rock':[0,0,0,0,0,0,1,2,2,1,1,0],'il-chi':[0,0,0,0,0,0,0,1,2,2,1,0]},
  eggs:{all:[0,0,0,1,2,2,2,2,2,1,1,0],mn:[1,1,1,2,2,3,3,3,3,2,2,1],'wi-west':[0,0,0,1,1,2,2,2,1,1,0,0],'wi-south':[0,0,0,0,1,1,2,2,1,1,0,0],'il-rock':[0,0,0,0,0,1,1,1,1,0,0,0],'il-chi':[0,0,0,0,0,0,1,1,1,0,0,0]},
  poultry:{all:[1,1,1,1,2,2,2,2,2,2,1,1],mn:[2,2,2,2,3,3,3,3,3,2,2,2],'wi-west':[1,1,1,1,2,2,2,2,2,1,1,1],'wi-south':[0,0,1,1,1,2,2,2,2,1,1,0],'il-rock':[0,0,0,1,1,1,1,1,1,1,0,0],'il-chi':[0,0,0,0,1,1,2,2,1,1,0,0]},
  dairy:{all:[2,2,2,2,3,3,3,3,2,2,2,2],mn:[2,2,2,3,3,3,3,3,3,2,2,2],'wi-west':[2,2,2,2,3,3,3,3,2,2,2,2],'wi-south':[2,2,2,2,3,3,3,3,3,2,2,2],'il-rock':[1,1,1,1,2,2,2,2,1,1,1,1],'il-chi':[1,1,1,1,1,2,2,1,1,1,1,1]},
  grain:{all:[1,1,1,0,0,0,0,0,2,3,3,2],mn:[1,1,1,0,0,0,0,0,2,3,3,2],'wi-west':[1,1,0,0,0,0,0,0,1,2,2,1],'wi-south':[2,2,1,0,0,0,0,0,2,3,3,2],'il-rock':[1,1,1,0,0,0,0,0,2,3,3,2],'il-chi':[0,0,0,0,0,0,0,0,1,2,2,1]}
};
const DEMAND_BY_AREA={
  veg:{all:[2,2,2,2,2,2,2,2,2,2,2,2],mn:[2,2,2,2,2,2,2,2,2,2,2,2],'wi-west':[1,1,1,1,2,2,2,2,2,2,1,1],'wi-south':[2,2,2,2,2,2,2,2,2,2,2,2],'il-rock':[2,2,2,2,3,3,3,3,2,2,2,2],'il-chi':[3,3,3,3,3,3,3,3,3,3,3,3]},
  fruit:{all:[2,2,1,1,1,1,1,1,2,2,2,2],mn:[1,1,1,1,1,1,1,1,2,2,2,1],'wi-west':[1,1,1,1,1,1,2,2,2,2,1,1],'wi-south':[2,2,1,1,1,1,1,1,2,2,2,2],'il-rock':[2,2,2,1,1,1,1,1,2,2,2,2],'il-chi':[3,3,2,2,2,1,1,1,2,3,3,3]},
  eggs:{all:[3,3,3,3,2,2,2,2,3,3,3,3],mn:[2,2,2,2,2,2,2,2,2,2,2,2],'wi-west':[1,1,1,2,2,2,2,2,2,1,1,1],'wi-south':[2,2,2,2,2,2,2,2,2,2,2,2],'il-rock':[2,2,2,2,2,2,2,2,2,2,2,2],'il-chi':[3,3,3,3,3,2,2,2,3,3,3,3]},
  poultry:{all:[2,2,1,1,1,2,2,2,2,2,2,2],mn:[2,2,2,1,1,2,2,2,2,2,2,2],'wi-west':[1,1,1,1,1,2,2,2,2,1,1,1],'wi-south':[2,2,1,1,1,1,2,2,2,2,2,2],'il-rock':[2,2,2,2,2,2,2,2,2,2,2,2],'il-chi':[3,3,2,2,2,2,2,2,2,2,3,3]},
  dairy:{all:[2,2,2,2,1,1,1,1,2,2,2,2],mn:[2,2,2,2,1,1,1,1,1,2,2,2],'wi-west':[1,1,1,1,1,1,1,1,1,1,1,1],'wi-south':[2,2,2,1,1,1,1,1,2,2,2,2],'il-rock':[2,2,2,2,2,1,1,1,2,2,2,2],'il-chi':[3,3,3,2,2,1,1,1,2,3,3,3]},
  grain:{all:[2,2,1,1,1,1,1,1,2,2,2,2],mn:[1,1,1,1,1,1,1,1,2,2,2,1],'wi-west':[1,1,1,1,1,1,1,1,2,2,1,1],'wi-south':[2,2,2,1,1,1,1,1,2,2,2,2],'il-rock':[2,2,2,1,1,1,1,1,2,2,2,2],'il-chi':[3,3,2,2,1,1,1,1,2,3,3,3]}
};
const STORIES={
  all:{'All seasons':{type:'mixed',text:'Full corridor view. Vegetables and fruit show strong summer surplus in MN/WI nodes; liquid eggs and poultry face year-round demand gaps at Chicagoland end.'},Spring:{type:'gap',text:'Spring is the hardest season. Vegetable supply is ramping, fruit hasn\'t started. Egg and poultry demand spikes while supply lags.'},Summer:{type:'surplus',text:'Summer surplus window. Vegetables and fruit peak across MN and WI nodes. Best season for FOB inter-hub transfers.'},Fall:{type:'mixed',text:'Fall is the strongest balance season. Fruit and veg peak. Grain harvest online. Chicago demand rises ahead of winter.'},Winter:{type:'gap',text:'Winter is highest-risk. Fresh supply nearly absent for veg and fruit. Egg and poultry demand stays high with reduced supply.'}},
  mn:{'All seasons':{type:'surplus',text:'Minneapolis/MN is the supply origin. Vegetables, eggs, and poultry consistently well-supplied from TGA clusters. Demand moderate — Mayo Clinic anchors healthcare.'},Spring:{type:'surplus',text:'MN spring: TGA clusters ramp up veg earlier than WI/IL. Eggs from Garden Harvest year-round.'},Summer:{type:'surplus',text:'MN summer peak: strongest corridor supply window. Sno Pac IQF absorbs surplus veg.'},Fall:{type:'surplus',text:'MN fall harvest: Whistland Well apples, storage veg from TGA clusters. Strong export season.'},Winter:{type:'mixed',text:'MN winter: fresh veg and fruit drop. Eggs and poultry remain from year-round producers.'}},
  'wi-west':{'All seasons':{type:'mixed',text:'Eau Claire/WI strong on veg and fruit. Eggs and poultry depend on MN imports. Indian Head institutions moderate demand.'},Spring:{type:'gap',text:'WI-West spring: supply thin before local harvest. Eggs and dairy must be sourced from MN.'},Summer:{type:'surplus',text:'WI-West summer: Door County cherries and Driftless produce create seasonal surplus.'},Fall:{type:'surplus',text:'WI-West fall: tree fruit peak. WIFH aggregation at full capacity.'},Winter:{type:'gap',text:'WI-West winter: significant supply gap. WIFH warehouse underutilized.'}},
  'wi-south':{'All seasons':{type:'mixed',text:'Madison/WI is the grain and dairy hub. Meadowlark Organics and Lamers Dairy give year-round strength. UW Health and Epic are anchor buyers.'},Spring:{type:'gap',text:'Madison spring: grain in storage — supply good. Fresh veg not yet available. Egg supply depends on MN.'},Summer:{type:'mixed',text:'Madison summer: veg and fruit come online. Grain and dairy remain strong.'},Fall:{type:'surplus',text:'Madison fall is the strongest season. Grain harvest peaks. UW Health demand well-matched.'},Winter:{type:'mixed',text:'Madison winter: grain and dairy carry the season. Eggs and poultry gap opens.'}},
  'il-rock':{'All seasons':{type:'gap',text:'Rockford is a demand node with limited local supply. Rockford ISD drives year-round demand.'},Spring:{type:'gap',text:'Rockford spring: no local supply. MWF must run to WI/MN for all categories.'},Summer:{type:'gap',text:'Rockford summer: thin local sourcing. Gap narrows for veg but remains for eggs and grain.'},Fall:{type:'gap',text:'Rockford fall: best local supply window, still below ISD demand.'},Winter:{type:'gap',text:'Rockford winter: most acute gap. No local production. Highest delivery cost in corridor.'}},
  'il-chi':{'All seasons':{type:'gap',text:'Chicagoland is the highest-demand, lowest-supply node. CPS, Advocate Rush, U of Chicago create massive year-round demand.'},Spring:{type:'gap',text:'Chicago spring: critical gap. School year and hospital programs at peak. Almost entirely sourcing from outside corridor.'},Summer:{type:'gap',text:'Chicago summer: demand eases slightly. MN/WI surplus flows south via MWF and Common Market GL.'},Fall:{type:'gap',text:'Chicago fall: demand surges as school year resumes. Veg and grain gap narrows. Poultry and eggs undersupplied.'},Winter:{type:'gap',text:'Chicago winter: largest annual gap. Demand at peak. Supply entirely depends on IQF frozen from Sno Pac and Edible Cuts.'}}
};
const WF_BASE={veg:{fg:0.38,agg:0.10,wh:0.16,del:0.13,hdl:0.08,unit:'$/lb'},fruit:{fg:1.20,agg:0.32,wh:0.48,del:0.38,hdl:0.36,unit:'$/lb'},eggs:{fg:3.10,agg:0.38,wh:0.58,del:0.32,hdl:0.18,unit:'$/lb liquid'},poultry:{fg:2.20,agg:0.28,wh:0.52,del:0.42,hdl:0.22,unit:'$/lb'},dairy:{fg:0.55,agg:0.16,wh:0.30,del:0.20,hdl:0.12,unit:'$/lb'},grain:{fg:0.45,agg:0.14,wh:0.26,del:0.16,hdl:0.10,unit:'$/lb'}};
const SEASON_MODS={'All seasons':{agg:1.00,wh:1.00,del:1.00,hdl:1.00,note:'Average across all seasons.'},Spring:{agg:1.10,wh:1.05,del:1.05,hdl:1.05,note:'Spring: supply ramp-up, higher aggregation cost.'},Summer:{agg:0.85,wh:0.95,del:0.95,hdl:0.90,note:'Summer: peak harvest — lowest per-unit cost.'},Fall:{agg:0.90,wh:1.00,del:1.00,hdl:0.95,note:'Fall: strong harvest, efficient runs.'},Winter:{agg:1.25,wh:1.15,del:1.15,hdl:1.20,note:'Winter: off-peak, IQF processing adds cost.'}};
const AREA_MODS={all:{del:1.00,hdl:1.00,note:'Blended corridor average.'},mn:{del:0.85,hdl:0.90,note:'MN: home market, shortest hauls.'},'wi-west':{del:1.00,hdl:0.95,note:'Eau Claire: moderate distance, WIFH consolidation.'},'wi-south':{del:1.10,hdl:1.00,note:'Madison: ~250 mi cross-state freight.'},'il-rock':{del:1.22,hdl:1.08,note:'Rockford: ~330 mi, MWF distribution step.'},'il-chi':{del:1.38,hdl:1.18,note:'Chicagoland: ~400 mi, highest cold-chain cost.'}};
const PROD_NOTES={veg:'Carrots, slaw mix, specialty veg.',fruit:'Apples, peaches, cherries — EcoCertified premium.',eggs:'Liquid eggs; processing cost absorbed in aggregation.',poultry:'Ferndale Turkey regenerative protocol.',dairy:'St. Croix Cheese; consistent year-round supply.',grain:'Meadowlark Organics / Artisan Grain Collective.',all:'Blended average across all corridor product categories.'};
const ALL_PROD_KEYS=['veg','fruit','eggs','poultry','dairy','grain'];
const PROD_LABELS={veg:'Vegetables',fruit:'Fruit',eggs:'Liquid eggs',poultry:'Poultry',dairy:'Dairy',grain:'Grain'};

function getVisibleProds(){const p=getProd();return p==='all'?ALL_PROD_KEYS:[p];}

function buildHM(){
  const area=getArea(),prodKeys=getVisibleProds();
  let totalSurplus=0,totalGap=0,totalMM=0,peakMonth='',peakVal=0;
  let html='<thead><tr><th style="text-align:right;width:88px;padding-right:6px"></th>';
  MONTHS.forEach(m=>{html+=`<th style="font-size:9px;font-weight:500;color:var(--color-text-primary);padding:2px 3px;text-align:center;width:38px">${m}</th>`;});
  html+='</tr></thead><tbody>';
  prodKeys.forEach((key,pi)=>{
    const supply=SUPPLY_BY_AREA[key][area]||SUPPLY_BY_AREA[key]['all'];
    const demand=DEMAND_BY_AREA[key][area]||DEMAND_BY_AREA[key]['all'];
    if(pi>0) html+=`<tr><td colspan="13" style="height:5px"></td></tr>`;
    ['supply','demand'].forEach(type=>{
      html+='<tr>';
      if(type==='supply') html+=`<td class="rl" rowspan="2">${PROD_LABELS[key]}</td>`;
      MONTHS.forEach((_,i)=>{
        const val=type==='supply'?supply[i]:demand[i];
        const mm=(type==='supply'&&demand[i]>=2&&supply[i]===0)?' mm':'';
        if(type==='supply'){if(supply[i]>demand[i])totalSurplus++;if(demand[i]>supply[i])totalGap++;if(demand[i]>=2&&supply[i]===0)totalMM++;if(supply[i]>peakVal){peakVal=supply[i];peakMonth=MONTHS[i];}}
        html+=`<td style="padding:1px 2px"><div class="hc ${type[0]}${val}${mm}"></div></td>`;
      });
      html+='</tr>';
    });
  });
  document.getElementById('hmtable').innerHTML=html+'</tbody>';
  document.getElementById('sdSurplus').textContent=totalSurplus;
  document.getElementById('sdGap').textContent=totalGap;
  document.getElementById('sdPeak').textContent=peakMonth||'—';
  document.getElementById('sdMM').textContent=totalMM;
  const storyKey=area==='all'?'all':area;
  const sd=(STORIES[storyKey]&&STORIES[storyKey]['All seasons'])||STORIES['all']['All seasons'];
  const box=document.getElementById('storyBox');
  box.className='story-box story-'+sd.type;
  const tl={surplus:'Surplus',gap:'Gap',mixed:'Mixed',neutral:''}[sd.type]||'';
  box.innerHTML=`<span class="story-label">${tl}${tl?' — ':''}</span>${sd.text}`;
}

let gapChart=null;
function buildGap(){
  const area=getArea(),prodKeys=getVisibleProds();
  const aS={all:1,mn:1.3,'wi-west':0.9,'wi-south':0.75,'il-rock':0.45,'il-chi':0.3};
  const dS={all:1,mn:0.6,'wi-west':0.4,'wi-south':0.65,'il-rock':0.7,'il-chi':1.4};
  const sB={veg:[5,5,5,8,14,22,25,28,20,15,8,0],fruit:[8,3,3,0,0,5,18,45,65,62,50,25],eggs:[0,0,0,5,15,20,22,20,18,10,8,0],poultry:[8,8,8,8,10,12,12,12,10,10,8,8],dairy:[16,16,16,16,20,22,22,20,16,16,16,16],grain:[8,8,6,0,0,0,0,0,14,20,18,12]};
  const dB={veg:[10,10,10,10,10,10,10,10,10,10,10,10],fruit:[28,28,27,26,19,5,5,5,27,28,28,26],eggs:[25,25,25,22,20,18,18,18,22,25,25,25],poultry:[10,10,8,8,8,10,10,10,10,10,10,10],dairy:[16,16,16,16,12,12,12,12,16,16,16,16],grain:[10,10,8,8,8,8,8,8,10,10,10,10]};
  const supply=MONTHS.map((_,i)=>Math.round(prodKeys.reduce((s,k)=>s+(sB[k][i]*(aS[area]||1)),0)));
  const demand=MONTHS.map((_,i)=>Math.round(prodKeys.reduce((s,k)=>s+(dB[k][i]*(dS[area]||1)),0)));
  const gapMos=demand.filter((d,i)=>d>supply[i]).length;
  const shortfall=Math.round(demand.reduce((s,d,i)=>s+Math.max(0,d-supply[i]),0));
  const surpMos=MONTHS.filter((_,i)=>supply[i]>demand[i]);
  const peakIdx=supply.indexOf(Math.max(...supply));
  document.getElementById('gM').textContent=gapMos;
  document.getElementById('gS').textContent=shortfall+'K';
  document.getElementById('gW').textContent=surpMos.length?(surpMos[0]+(surpMos.length>1?'–'+surpMos[surpMos.length-1]:'')):'None';
  document.getElementById('gP').textContent=MONTHS[peakIdx];
  if(gapChart){gapChart.destroy();gapChart=null;}
  gapChart=new Chart(document.getElementById('gapC'),{type:'line',data:{labels:MONTHS,datasets:[
    {label:'Supply',data:supply,borderColor:'#27500A',backgroundColor:'rgba(0,0,0,0)',borderWidth:2,pointBackgroundColor:'#27500A',pointRadius:3,tension:.4,order:1},
    {label:'Demand',data:demand,borderColor:'#993C1D',backgroundColor:'rgba(0,0,0,0)',borderWidth:2,borderDash:[5,4],pointBackgroundColor:'#993C1D',pointRadius:3,tension:.4,order:2},
    {label:'Surplus',data:supply.map((s,i)=>s>=demand[i]?s:demand[i]),borderColor:'rgba(0,0,0,0)',backgroundColor:'rgba(100,180,60,0.15)',fill:'-2',pointRadius:0,tension:.4,order:3},
    {label:'Gap',data:demand.map((d,i)=>d>supply[i]?d:supply[i]),borderColor:'rgba(0,0,0,0)',backgroundColor:'rgba(220,80,50,0.12)',fill:'-2',pointRadius:0,tension:.4,order:4}
  ]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{callbacks:{label:c=>c.dataset.label+': '+Math.round(c.parsed.y)+'K lbs'}}},scales:{x:{grid:{color:'rgba(128,128,128,0.08)'},ticks:{font:{size:10},autoSkip:false}},y:{grid:{color:'rgba(128,128,128,0.08)'},ticks:{font:{size:10},callback:v=>v+'K'},title:{display:true,text:'thousand lbs',font:{size:10},color:'#888'}}}}});
}

function buildSpag(){
  const svg=document.getElementById('spagSvg');
  const areas=[{id:'mn',label:'Minneapolis/MN',x:80,y:70},{id:'wi-west',label:'Eau Claire/WI',x:185,y:95},{id:'wi-south',label:'Madison/WI',x:240,y:155},{id:'il-rock',label:'Rockford/IL',x:330,y:185},{id:'il-chi',label:'Chicagoland/IL',x:450,y:220}];
  const fArea=getArea(),prodV=getProd();
  const PC={veg:'#639922',fruit:'#D85A30',eggs:'#378ADD',dairy:'#378ADD',poultry:'#7F77DD',grain:'#7F77DD'};
  const routes=[{from:'mn',to:'wi-west',prod:'veg',vol:3},{from:'mn',to:'wi-south',prod:'fruit',vol:2},{from:'mn',to:'il-chi',prod:'eggs',vol:2},{from:'wi-west',to:'wi-south',prod:'veg',vol:2},{from:'wi-west',to:'il-chi',prod:'fruit',vol:3},{from:'wi-west',to:'mn',prod:'veg',vol:1},{from:'wi-south',to:'il-rock',prod:'grain',vol:2},{from:'wi-south',to:'il-chi',prod:'veg',vol:2},{from:'wi-south',to:'mn',prod:'dairy',vol:1},{from:'il-rock',to:'il-chi',prod:'poultry',vol:2},{from:'mn',to:'il-rock',prod:'eggs',vol:1},{from:'wi-west',to:'il-rock',prod:'veg',vol:1},{from:'il-chi',to:'wi-south',prod:'dairy',vol:1},{from:'mn',to:'wi-south',prod:'poultry',vol:2}];
  const nm={};areas.forEach(a=>{nm[a.id]=a;});
  let html='';
  [{x:20,y:20,w:155,h:220,label:'Minnesota'},{x:155,y:40,w:190,h:200,label:'Wisconsin'},{x:320,y:110,w:210,h:150,label:'Illinois'}].forEach(b=>{html+=`<rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="4" fill="none" stroke="var(--color-border-tertiary)" stroke-width="1" stroke-dasharray="4 3"/><text x="${b.x+8}" y="${b.y+14}" font-size="10" fill="var(--color-text-tertiary)">${b.label}</text>`;});
  routes.forEach(r=>{const fa=nm[r.from],ta=nm[r.to];if(!fa||!ta) return;const skip=(fArea!=='all'&&r.from!==fArea&&r.to!==fArea)||(prodV!=='all'&&r.prod!==prodV);const mx=(fa.x+ta.x)/2,my=Math.min(fa.y,ta.y)-28-r.vol*6;html+=`<path d="M${fa.x},${fa.y} Q${mx},${my} ${ta.x},${ta.y}" fill="none" stroke="${PC[r.prod]||'#888'}" stroke-width="${r.vol+0.5}" opacity="${skip?0.07:0.6}"/>`;});
  areas.forEach(a=>{const active=fArea==='all'||fArea===a.id;html+=`<circle cx="${a.x}" cy="${a.y}" r="9" fill="${active?'#378ADD':'#B5D4F4'}" stroke="${active?'#0C447C':'#85B7EB'}" stroke-width="1.5"/><text x="${a.x}" y="${a.y+20}" text-anchor="middle" font-size="9" fill="var(--color-text-secondary)">${a.label}</text>`;});
  svg.innerHTML=html;
}

let wfChart=null;
function buildWF(){
  const pk=getProd()==='all'?'fruit':getProd();const p=WF_BASE[pk]||WF_BASE.fruit;
  const sm=SEASON_MODS[getSeason()]||SEASON_MODS['All seasons'];const am=AREA_MODS[getArea()]||AREA_MODS['all'];
  const fg=p.fg,agg=+(p.agg*sm.agg).toFixed(2),wh=+(p.wh*sm.wh).toFixed(2),del=+(p.del*sm.del*am.del).toFixed(2),hdl=+(p.hdl*sm.hdl*am.hdl).toFixed(2);
  const total=+(fg+agg+wh+del+hdl).toFixed(2);
  document.getElementById('wfFG').textContent='$'+fg.toFixed(2);
  document.getElementById('wfIP').textContent='$'+total.toFixed(2);
  document.getElementById('wfMU').textContent=(total/fg).toFixed(1)+'x';
  document.getElementById('wfFS').textContent=Math.round(fg/total*100)+'%';
  const prodLabel={all:'All products',veg:'Vegetables',fruit:'Fruit',eggs:'Liquid eggs',poultry:'Poultry',dairy:'Dairy',grain:'Grain'}[getProd()]||'';
  const areaLabel={all:'all areas',mn:'Minneapolis/MN','wi-west':'Eau Claire/WI','wi-south':'Madison/WI','il-rock':'Rockford/IL','il-chi':'Chicagoland/IL'}[getArea()]||'';
  document.getElementById('wfNote').textContent=`${prodLabel}, ${getSeason().toLowerCase()}, ${areaLabel}. ${PROD_NOTES[getProd()]||''} ${sm.note} ${am.note}`.trim();
  const bases=[0,fg,+(fg+agg).toFixed(2),+(fg+agg+wh).toFixed(2),+(fg+agg+wh+del).toFixed(2),0];
  const vals=[fg,agg,wh,del,hdl,total];
  if(wfChart){wfChart.destroy();wfChart=null;}
  wfChart=new Chart(document.getElementById('wfC'),{type:'bar',data:{labels:['Farmgate','Aggregation','Wholesale margin','Delivery','Handling','Institution price'],datasets:[{label:'Base',data:bases,backgroundColor:'rgba(0,0,0,0)',borderWidth:0},{label:'Value',data:vals,backgroundColor:['#639922','#85B7EB','#85B7EB','#85B7EB','#85B7EB','#D85A30'],borderWidth:0,borderRadius:3}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{callbacks:{label:c=>c.datasetIndex===1?'$'+c.parsed.y.toFixed(2):'',filter:i=>i.datasetIndex===1}}},scales:{x:{stacked:true,grid:{display:false},ticks:{font:{size:10},maxRotation:0}},y:{stacked:true,grid:{color:'rgba(128,128,128,0.08)'},ticks:{font:{size:10},callback:v=>'$'+v.toFixed(2)},title:{display:true,text:p.unit,font:{size:10},color:'#888'}}}}});
}

function refresh(){
  const v=document.querySelector('.tab.on').dataset.v;
  if(v==='sd') buildHM();
  if(v==='gap') buildGap();
  if(v==='net') buildNet();
  if(v==='spag') buildSpag();
  if(v==='wf') buildWF();
}
document.querySelectorAll('.tab').forEach(t=>{
  t.addEventListener('click',()=>{
    document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));
    document.querySelectorAll('.view').forEach(x=>x.classList.remove('on'));
    t.classList.add('on');
    document.getElementById('v-'+t.dataset.v).classList.add('on');
    updateSeasonVisibility(t.dataset.v);
    if(t.dataset.v==='sd') buildHM();
    if(t.dataset.v==='gap') buildGap();
    if(t.dataset.v==='net') buildNet();
    if(t.dataset.v==='spag') buildSpag();
    if(t.dataset.v==='wf') buildWF();
  });
});
['fSeason','fProd','fArea'].forEach(id=>document.getElementById(id).addEventListener('change',refresh));
updateSeasonVisibility('sd');
buildHM();
</script>