This proof of concept demonstrates a living economy, not a static shop.

Prices are not fixed.
Money is not a score.
Trading decisions create consequences.

The purpose of this POC is to show that resources have intent:

Money → enables action → action reshapes the market → profit enables scale


What This POC Is Demonstrating

This system is designed to prove four core ideas:

  1. Location matters
    Each station has different supply and demand. The same commodity can be cheap in one place and valuable in another.
  2. Time matters
    Markets drift continuously. While you travel, prices change. Waiting has a cost.
  3. Actions matter
    Your trades directly affect prices. Buying increases demand and reduces supply. Selling does the opposite. You can see this impact immediately.
  4. Scale compounds
    Profit is not the end goal. Profit buys upgrades. Upgrades increase cargo, speed, and information—raising profit per minute, not just total profit.

How to Use the Simulation

A simple loop drives the entire system:

  1. Select a commodity
    Choose what you want to trade and observe its buy/sell prices.
  2. Buy at the current station
    Your purchase immediately shifts supply and demand.
  3. Check the route intelligence
    The system suggests the best nearby sell destination based on price, distance, and your intel level.
  4. Travel to another station
    While traveling, markets continue to drift. Missed timing is real risk.
  5. Sell and realize profit
    Selling impacts the destination market and updates your profit metrics.
  6. Upgrade to scale
    Use profit to increase cargo capacity, travel speed, or market visibility.

Repeat the loop at a larger scale.


What to Pay Attention To

As you interact with the POC, watch for these signals:

If you can clearly see cause → effect in the chart and numbers, the economy is working.


What This POC Is Not

This is intentionally scoped:

Those systems come later.
This POC exists solely to prove that trading has meaning and money has purpose.


Why This Matters

A static market is decoration.
A dynamic market is gameplay.

This prototype shows how economic pressure can drive:

without scripted events or artificial rewards.

Everything flows from player choice.


Code for Above

<div style="max-width:1500px;margin:0 auto;">
  <iframe
    title="POC 10 — Market Buy/Sell (Redesigned UI)"
    scrolling="no"
    style="width:1500px;height:1500px;border:0;display:block;background:#000;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.55);"
    sandbox="allow-scripts allow-same-origin"
    srcdoc='
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>POC 10 — Market Buy/Sell (Redesigned UI)</title>
<style>
  :root{
    --bg0:#05070D;
    --bg1:#070B14;
    --panel:rgba(14,20,34,.86);
    --panel2:rgba(10,14,24,.80);
    --stroke:rgba(255,255,255,.10);
    --stroke2:rgba(77,163,255,.22);
    --text:#EAF1FF;
    --muted:#A8B6D6;
    --good:#2BE4A7;
    --warn:#FFD166;
    --bad:#FF4D6D;
    --blue:#4DA3FF;
    --gold:#FFC857;
    --shadow: 0 18px 55px rgba(0,0,0,.50);
    --r:16px;
  }

  *{box-sizing:border-box}
  html,body{width:100%;height:100%;margin:0;overflow:hidden;background:#000;color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
  #app{
    width:1500px;height:1500px;margin:0 auto;position:relative;overflow:hidden;
    background:
      radial-gradient(1100px 900px at 50% 15%, #0E1A3D 0%, #070A12 55%, #04050A 100%);
  }

  /* starfield */
  #stars{position:absolute;inset:0;opacity:.50}

  /* layout */
  .shell{position:absolute;inset:14px;display:grid;grid-template-rows:76px 1fr;gap:12px;z-index:2}
  .topbar{
    border-radius:18px;
    background:linear-gradient(90deg, rgba(77,163,255,.16), rgba(255,200,87,.08));
    border:1px solid rgba(255,255,255,.12);
    box-shadow: var(--shadow);
    display:flex;align-items:center;justify-content:space-between;gap:12px;
    padding:14px 16px;
  }
  .brand{display:flex;flex-direction:column;gap:3px}
  .brand .t{font-weight:900;letter-spacing:.2px}
  .brand .s{color:var(--muted);font-size:12px}
  .statusRow{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}
  .chip{
    display:inline-flex;align-items:center;gap:8px;
    padding:8px 10px;border-radius:999px;
    border:1px solid rgba(255,255,255,.14);
    background:rgba(0,0,0,.22);
    font-size:12px;font-weight:800;
  }
  .chip .dot{width:9px;height:9px;border-radius:999px;background:var(--blue)}
  .chip.good .dot{background:var(--good)}
  .chip.warn .dot{background:var(--warn)}
  .chip.bad .dot{background:var(--bad)}
  .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}

  .grid{
    display:grid;
    grid-template-columns: 360px 1fr 420px;
    grid-template-rows: 1fr 420px;
    gap:12px;
    min-height:0;
  }

  /* cards */
  .card{
    border-radius:18px;
    background:linear-gradient(180deg, rgba(18,25,45,.90), rgba(8,10,18,.86));
    border:1px solid rgba(255,255,255,.12);
    box-shadow: var(--shadow);
    overflow:hidden;
    min-height:0;
  }
  .hd{
    padding:12px 14px;
    border-bottom:1px solid rgba(255,255,255,.08);
    display:flex;align-items:center;justify-content:space-between;gap:10px;
    background:linear-gradient(90deg, rgba(77,163,255,.10), rgba(0,0,0,.02));
  }
  .hd .ttl{font-weight:900}
  .hd .sub{color:var(--muted);font-size:12px;margin-top:2px}
  .hd .right{display:flex;align-items:center;gap:10px}
  .body{padding:14px;min-height:0;height:100%}

  .stack{display:flex;flex-direction:column;gap:12px;min-height:0;height:100%}
  .row{display:flex;align-items:center;justify-content:space-between;gap:10px}
  .k{color:var(--muted);font-size:12px}
  .v{font-weight:900}
  .hint{color:var(--muted);font-size:12px;line-height:1.35}
  .sep{height:1px;background:rgba(255,255,255,.08);margin:8px 0}

  /* mini panels */
  .mini{
    border:1px solid rgba(255,255,255,.10);
    background:rgba(0,0,0,.18);
    border-radius:16px;
    padding:12px;
  }

  /* table */
  .table{
    width:100%;
    border-collapse:collapse;
    border-radius:14px;
    overflow:hidden;
    border:1px solid rgba(255,255,255,.10);
    background:rgba(0,0,0,.16);
  }
  .table th,.table td{
    padding:10px 10px;
    border-bottom:1px solid rgba(255,255,255,.08);
    font-size:12px;
    text-align:left;
    vertical-align:middle;
  }
  .table th{color:var(--muted);font-weight:900;background:rgba(255,255,255,.04)}
  .table tr:last-child td{border-bottom:0}
  .rightTxt{text-align:right}

  /* controls */
  .select, .input{
    width:100%;
    padding:12px 12px;
    border-radius:14px;
    border:1px solid rgba(255,255,255,.14);
    background:rgba(0,0,0,.26);
    color:var(--text);
    outline:none;
    font-weight:850;
  }
  .btn{
    appearance:none;
    border:1px solid rgba(255,255,255,.14);
    background:rgba(255,255,255,.06);
    color:var(--text);
    padding:12px 12px;
    border-radius:14px;
    cursor:pointer;
    font-weight:900;
    transition:transform .06s ease, background .15s ease, border-color .15s ease, filter .15s ease;
    user-select:none;
  }
  .btn:hover{background:rgba(255,255,255,.10);border-color:rgba(255,255,255,.22)}
  .btn:active{transform:translateY(1px)}
  .btn:disabled{opacity:.45;cursor:not-allowed}
  .btn.primary{background:rgba(77,163,255,.18);border-color:rgba(77,163,255,.35)}
  .btn.good{background:rgba(43,228,167,.14);border-color:rgba(43,228,167,.35)}
  .btn.bad{background:rgba(255,77,109,.14);border-color:rgba(255,77,109,.35)}
  .btn.ghost{background:rgba(0,0,0,.18)}
  .btn.small{padding:10px 10px;border-radius:12px;font-size:12px}

  .btnGroup{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
  .btnGroup2{display:grid;grid-template-columns:1fr 1fr;gap:10px}

  /* trade strip */
  .tradeStrip{
    display:grid;
    grid-template-columns: 1.1fr 1fr;
    gap:12px;
  }
  .bigPrice{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:10px;
  }
  .priceBox{
    border-radius:16px;
    padding:12px;
    border:1px solid rgba(255,255,255,.10);
    background:linear-gradient(180deg, rgba(0,0,0,.18), rgba(0,0,0,.10));
  }
  .priceBox .lab{color:var(--muted);font-size:12px;font-weight:900}
  .priceBox .amt{font-size:20px;font-weight:950;margin-top:4px}
  .priceBox.buy{border-color:rgba(77,163,255,.28)}
  .priceBox.sell{border-color:rgba(255,200,87,.28)}
  .priceBox.buy .amt{color:rgba(200,225,255,.98)}
  .priceBox.sell .amt{color:rgba(255,230,180,.98)}

  .stepper{
    display:grid;
    grid-template-columns: 54px 1fr 54px;
    gap:10px;
    align-items:center;
  }
  .stepBtn{
    height:44px;
    border-radius:14px;
    border:1px solid rgba(255,255,255,.14);
    background:rgba(255,255,255,.06);
    color:var(--text);
    font-weight:950;
    cursor:pointer;
  }
  .stepBtn:hover{background:rgba(255,255,255,.10)}
  .qtyRow{display:grid;grid-template-columns:1fr 1fr;gap:10px}

  /* bars */
  .bars{display:grid;grid-template-columns:1fr;gap:10px;margin-top:10px}
  .barline{display:flex;align-items:center;gap:10px}
  .barlabel{width:64px;color:var(--muted);font-size:12px;font-weight:900}
  .barwrap{flex:1;height:12px;border-radius:999px;overflow:hidden;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06)}
  .barfill{height:100%}
  .barvalue{width:44px;text-align:right;color:var(--muted);font-size:12px}

  /* dials */
  .dialGrid{display:grid;grid-template-columns: 1fr 1fr;gap:12px}
  .dial{
    display:grid;
    grid-template-columns: 82px 1fr;
    gap:12px;
    align-items:center;
    padding:10px;
    border-radius:16px;
    border:1px solid rgba(255,255,255,.10);
    background:rgba(0,0,0,.16);
  }
  .knob{
    width:76px;height:76px;border-radius:50%;
    border:1px solid rgba(255,255,255,.12);
    background:
      radial-gradient(circle at 35% 30%, rgba(255,255,255,.20), rgba(255,255,255,.06) 45%, rgba(0,0,0,.25) 70%),
      conic-gradient(from 225deg, rgba(77,163,255,.90) 0deg, rgba(77,163,255,.18) 0deg, rgba(255,255,255,.08) 270deg, rgba(255,255,255,.05) 360deg);
    box-shadow: inset 0 0 0 2px rgba(0,0,0,.20);
    position:relative;
  }
  .knob:after{
    content:"";
    position:absolute;
    left:50%;top:50%;
    width:3px;height:26px;
    transform: translate(-50%,-90%) rotate(0deg);
    transform-origin: 50% 85%;
    background:rgba(255,255,255,.92);
    border-radius:999px;
    opacity:.95;
  }
  .dial .meta{display:flex;flex-direction:column;gap:6px}
  .dial .name{font-size:12px;color:var(--muted);font-weight:900}
  .dial .val{font-size:14px;font-weight:950}
  .dial input[type="range"]{width:100%}

  input[type="range"]{
    -webkit-appearance:none;appearance:none;height:10px;border-radius:999px;
    background:rgba(255,255,255,.08);
    border:1px solid rgba(255,255,255,.10);
    outline:none;
  }
  input[type="range"]::-webkit-slider-thumb{
    -webkit-appearance:none;appearance:none;
    width:22px;height:22px;border-radius:50%;
    background:rgba(77,163,255,.95);
    border:1px solid rgba(255,255,255,.28);
    box-shadow: 0 8px 18px rgba(0,0,0,.35);
    cursor:pointer;
  }

  /* chart */
  #chart{
    width:100%;height:260px;
    border-radius:16px;
    border:1px solid rgba(255,255,255,.10);
    background:rgba(0,0,0,.16);
  }
  .legend{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:10px}
  .legLeft{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
  .ldot{width:10px;height:10px;border-radius:50%;border:1px solid rgba(255,255,255,.25)}
  .ldot.blue{background:var(--blue)}
  .ldot.gold{background:var(--gold)}
  .ldot.good{background:var(--good)}
  .ldot.bad{background:var(--bad)}
  .tiny{font-size:11px;color:var(--muted)}
  .pill{
    display:inline-flex;align-items:center;gap:8px;
    padding:8px 10px;border-radius:999px;
    border:1px solid rgba(255,255,255,.14);
    background:rgba(0,0,0,.22);
    font-size:12px;font-weight:900;
  }

  /* placement */
  #playerCard{grid-column:1;grid-row:1}
  #marketCard{grid-column:2;grid-row:1}
  #rightCard{grid-column:3;grid-row:1}
  #ledgerCard{grid-column:1 / 3;grid-row:2}
  #chartCard{grid-column:3;grid-row:2}

  /* scroll areas */
  .scroll{overflow:auto;min-height:0}
  .scroll::-webkit-scrollbar{width:10px}
  .scroll::-webkit-scrollbar-thumb{background:rgba(255,255,255,.10);border-radius:999px;border:2px solid rgba(0,0,0,.25)}
  .scroll::-webkit-scrollbar-track{background:rgba(0,0,0,.18);border-radius:999px}
</style>
</head>
<body>
<div id="app">
  <canvas id="stars" width="1500" height="1500"></canvas>

  <div class="shell">
    <!-- TOP BAR -->
    <div class="topbar">
      <div class="brand">
        <div class="t">POC 10 — Market Buy/Sell with Dynamic Pricing</div>
        <div class="s">Loop: Buy low → travel → sell high → upgrade → repeat. Prices change over time and react to your trades.</div>
      </div>
      <div class="statusRow">
        <div class="chip" id="chipLoc"><span class="dot"></span><span id="locName">—</span></div>
        <div class="chip" id="chipState"><span class="dot"></span><span id="stateName">Docked</span></div>
        <div class="chip"><span class="dot"></span><span>Credits</span> <span class="mono" id="credits">$0.00</span></div>
        <div class="chip"><span class="dot"></span><span>Cargo</span> <span class="mono" id="cargoCap">0/0</span></div>
      </div>
    </div>

    <!-- MAIN GRID -->
    <div class="grid">

      <!-- PLAYER -->
      <div class="card" id="playerCard">
        <div class="hd">
          <div>
            <div class="ttl">Player</div>
            <div class="sub">Upgrades increase profit per minute</div>
          </div>
          <div class="right">
            <span class="pill" id="feePill">Fee 2.0%</span>
          </div>
        </div>
        <div class="body">
          <div class="stack">
            <div class="mini">
              <div class="row">
                <div>
                  <div class="k">Net worth (credits + cargo)</div>
                  <div class="v mono" id="netWorth">$0.00</div>
                </div>
                <div style="text-align:right">
                  <div class="k">Travel timer</div>
                  <div class="v mono" id="eta">Docked</div>
                </div>
              </div>
              <div class="sep"></div>
              <div class="hint">
                Action order: choose a commodity → set quantity → buy/sell → travel if needed.
              </div>
            </div>

            <div class="mini">
              <div class="row" style="margin-bottom:10px">
                <div>
                  <div class="ttl" style="font-size:13px">Travel</div>
                  <div class="sub" style="margin-top:2px">Different stations = different prices</div>
                </div>
                <div class="pill" id="intelPill">Intel L1</div>
              </div>
              <select class="select" id="destSelect"></select>
              <div style="height:10px"></div>
              <div class="btnGroup2">
                <button class="btn good" id="travelBtn">Travel</button>
                <button class="btn" id="dockBtn" disabled>Dock</button>
              </div>
              <div class="sep"></div>
              <div class="hint" id="bestHint">Best sell hint: pick a commodity.</div>
              <div class="hint mono" id="bestHint2"></div>
            </div>

            <div class="mini">
              <div class="row" style="margin-bottom:10px">
                <div>
                  <div class="ttl" style="font-size:13px">Upgrades</div>
                  <div class="sub" style="margin-top:2px">Spend profit to increase scale</div>
                </div>
              </div>
              <div class="btnGroup">
                <button class="btn primary" id="upCargo">+ Cargo</button>
                <button class="btn primary" id="upEngine">+ Engine</button>
                <button class="btn primary" id="upIntel">+ Intel</button>
              </div>
              <div class="sep"></div>
              <div class="hint">
                Cargo: carry more each trip. Engine: faster trips. Intel: better routing hints.
              </div>
            </div>

            <div class="mini">
              <div class="row" style="margin-bottom:8px">
                <div class="ttl" style="font-size:13px">Event Log</div>
                <div class="k mono" id="tickLabel">00000</div>
              </div>
              <div class="scroll" id="log" style="max-height:210px;font-size:12px;line-height:1.35;color:var(--muted)"></div>
            </div>
          </div>
        </div>
      </div>

      <!-- MARKET (CENTER) -->
      <div class="card" id="marketCard">
        <div class="hd">
          <div>
            <div class="ttl">Market</div>
            <div class="sub">Buy and sell at the current station</div>
          </div>
          <div class="right">
            <span class="pill" id="ppTag">—</span>
          </div>
        </div>
        <div class="body">
          <div class="stack">
            <div class="mini">
              <div class="row" style="margin-bottom:10px">
                <div style="flex:1">
                  <div class="k">Commodity</div>
                  <select class="select" id="commoditySelect"></select>
                </div>
                <div style="width:220px">
                  <div class="k">Quick quantity</div>
                  <div class="qtyRow">
                    <button class="btn small ghost" id="q1">1</button>
                    <button class="btn small ghost" id="q10">10</button>
                  </div>
                </div>
              </div>

              <div class="tradeStrip">
                <div class="bigPrice">
                  <div class="priceBox buy">
                    <div class="lab">Buy price (each)</div>
                    <div class="amt mono" id="buyPrice">$0.00</div>
                  </div>
                  <div class="priceBox sell">
                    <div class="lab">Sell price (each)</div>
                    <div class="amt mono" id="sellPrice">$0.00</div>
                  </div>
                </div>

                <div>
                  <div class="k">Trade quantity</div>
                  <div class="stepper" style="margin-top:6px">
                    <button class="stepBtn" id="qtyMinus">−</button>
                    <input class="input mono" id="qtyInput" value="1" inputmode="numeric" />
                    <button class="stepBtn" id="qtyPlus">+</button>
                  </div>

                  <div style="height:10px"></div>
                  <div class="btnGroup2">
                    <button class="btn primary" id="buyBtn">Buy</button>
                    <button class="btn bad" id="sellBtn">Sell</button>
                  </div>
                  <div style="height:10px"></div>
                  <div class="btnGroup2">
                    <button class="btn primary" id="buyMax">Buy Max</button>
                    <button class="btn bad" id="sellAll">Sell All</button>
                  </div>
                </div>
              </div>

              <div class="bars">
                <div class="barline">
                  <div class="barlabel">Supply</div>
                  <div class="barwrap"><div class="barfill" id="supplyBar"></div></div>
                  <div class="barvalue mono" id="supplyVal">—</div>
                </div>
                <div class="barline">
                  <div class="barlabel">Demand</div>
                  <div class="barwrap"><div class="barfill" id="demandBar"></div></div>
                  <div class="barvalue mono" id="demandVal">—</div>
                </div>
              </div>

              <div class="sep"></div>
              <div class="hint">
                Your trades move the market: buying increases demand and reduces supply; selling does the reverse.
              </div>
            </div>

            <div class="mini">
              <div class="row" style="margin-bottom:10px">
                <div>
                  <div class="ttl" style="font-size:13px">Profit Preview (sell everything here)</div>
                  <div class="sub" style="margin-top:2px">Based on your cost basis; fees included</div>
                </div>
              </div>
              <table class="table">
                <thead>
                  <tr><th>Line</th><th class="rightTxt">Value</th></tr>
                </thead>
                <tbody>
                  <tr><td>Held quantity</td><td class="rightTxt mono" id="heldQty">—</td></tr>
                  <tr><td>Average cost (each)</td><td class="rightTxt mono" id="avgCost">—</td></tr>
                  <tr><td>Gross sell value</td><td class="rightTxt mono" id="sellValue">—</td></tr>
                  <tr><td>Transaction fees</td><td class="rightTxt mono" id="feesEst">—</td></tr>
                  <tr><td><b>Net profit</b></td><td class="rightTxt mono" id="netIfSold">—</td></tr>
                </tbody>
              </table>
              <div class="hint" style="margin-top:10px">
                If you hold 0 units, this section stays blank. Buy first, then compare destinations.
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- RIGHT (DIALS + RULES) -->
      <div class="card" id="rightCard">
        <div class="hd">
          <div>
            <div class="ttl">Tuning</div>
            <div class="sub">Adjust the economy and watch it react</div>
          </div>
          <div class="right">
            <span class="pill" id="livePill">Live</span>
          </div>
        </div>
        <div class="body">
          <div class="stack">
            <div class="mini">
              <div class="ttl" style="font-size:13px;margin-bottom:10px">What this POC proves</div>
              <div class="hint">
                1) Location matters: prices differ by station.<br>
                2) Time matters: prices drift while you travel.<br>
                3) Actions matter: trades change supply and demand.<br>
                4) Scale matters: profit funds upgrades that increase profit/min.
              </div>
            </div>

            <div class="mini">
              <div class="ttl" style="font-size:13px;margin-bottom:10px">Dials</div>
              <div class="dialGrid">
                <div class="dial" data-dial="fee">
                  <div class="knob" id="knobFee"></div>
                  <div class="meta">
                    <div class="name">Transaction Fee</div>
                    <div class="val mono" id="feeLabel">2.0%</div>
                    <input id="feeSlider" type="range" min="0" max="8" step="0.1" value="2" />
                  </div>
                </div>

                <div class="dial" data-dial="drift">
                  <div class="knob" id="knobDrift"></div>
                  <div class="meta">
                    <div class="name">Drift Rate</div>
                    <div class="val mono" id="driftLabel">0.040</div>
                    <input id="driftSlider" type="range" min="0.005" max="0.150" step="0.005" value="0.040" />
                  </div>
                </div>

                <div class="dial" data-dial="elas">
                  <div class="knob" id="knobElas"></div>
                  <div class="meta">
                    <div class="name">Elasticity</div>
                    <div class="val mono" id="elasLabel">0.90</div>
                    <input id="elasSlider" type="range" min="0.2" max="1.6" step="0.05" value="0.90" />
                  </div>
                </div>

                <div class="dial" data-dial="impact">
                  <div class="knob" id="knobImpact"></div>
                  <div class="meta">
                    <div class="name">Player Impact</div>
                    <div class="val mono" id="impactLabel">1.00</div>
                    <input id="impactSlider" type="range" min="0.2" max="2.5" step="0.1" value="1.0" />
                  </div>
                </div>
              </div>

              <div class="sep"></div>
              <div class="hint">
                Drift pulls supply/demand toward baseline. Elasticity controls how strongly price responds. Player Impact scales how much your trades move the market.
              </div>
            </div>

            <div class="mini">
              <div class="row" style="margin-bottom:6px">
                <div class="ttl" style="font-size:13px">Run Summary</div>
              </div>
              <div class="row">
                <div>
                  <div class="k">Last trade profit</div>
                  <div class="v mono" id="lastProfit">—</div>
                </div>
                <div style="text-align:right">
                  <div class="k">Lifetime profit</div>
                  <div class="v mono" id="lifeProfit">—</div>
                </div>
              </div>
              <div class="sep"></div>
              <div class="row">
                <div>
                  <div class="k">Profit per minute</div>
                  <div class="v mono" id="lifePPM">—</div>
                </div>
                <div style="text-align:right">
                  <div class="k">Trips</div>
                  <div class="v mono" id="tripCount">0</div>
                </div>
              </div>
            </div>

          </div>
        </div>
      </div>

      <!-- LEDGER (BOTTOM WIDE) -->
      <div class="card" id="ledgerCard">
        <div class="hd">
          <div>
            <div class="ttl">Cargo Ledger</div>
            <div class="sub">Holdings, cost basis, and estimated sell value at this station</div>
          </div>
          <div class="right">
            <span class="pill" id="chartPill">Local</span>
          </div>
        </div>
        <div class="body scroll">
          <table class="table" id="cargoTable">
            <thead>
              <tr>
                <th>Commodity</th>
                <th class="rightTxt">Qty</th>
                <th class="rightTxt">Avg Cost</th>
                <th class="rightTxt">Sell (ea)</th>
                <th class="rightTxt">Net after fee (ea)</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
          <div style="height:12px"></div>
          <div class="mini">
            <div class="ttl" style="font-size:13px;margin-bottom:8px">Route Intelligence (selected commodity)</div>
            <div class="row">
              <div>
                <div class="k">Selected commodity</div>
                <div class="v" id="selName">—</div>
              </div>
              <div style="text-align:right">
                <div class="k">Held</div>
                <div class="v mono" id="selHeld">0</div>
              </div>
            </div>
            <div class="sep"></div>
            <div class="row">
              <div>
                <div class="k">Best sell destination</div>
                <div class="v" id="bestDest">—</div>
              </div>
              <div style="text-align:right">
                <div class="k">Distance</div>
                <div class="v mono" id="bestDist">—</div>
              </div>
            </div>
            <div class="row">
              <div>
                <div class="k">Estimated net margin</div>
                <div class="v mono" id="bestMargin">—</div>
              </div>
              <div style="text-align:right">
                <div class="k">Estimated profit/min</div>
                <div class="v mono" id="bestPPM">—</div>
              </div>
            </div>
            <div class="hint" id="bestExplain" style="margin-top:8px"></div>
          </div>
        </div>
      </div>

      <!-- CHART (BOTTOM RIGHT) -->
      <div class="card" id="chartCard">
        <div class="hd">
          <div>
            <div class="ttl">Price Chart</div>
            <div class="sub">Last 120 ticks at this station (selected commodity)</div>
          </div>
          <div class="right">
            <span class="pill mono" id="chartScale">—</span>
          </div>
        </div>
        <div class="body">
          <canvas id="chart" width="392" height="260"></canvas>
          <div class="legend">
            <div class="legLeft">
              <span class="ldot blue"></span><span class="tiny">Buy</span>
              <span class="ldot gold"></span><span class="tiny">Sell</span>
              <span class="ldot good"></span><span class="tiny">You bought</span>
              <span class="ldot bad"></span><span class="tiny">You sold</span>
            </div>
            <div class="tiny mono" id="locMini">—</div>
          </div>
          <div class="hint" style="margin-top:10px">
            You should see your buy/sell markers shift supply/demand and move the line. If you can’t see that, the model is failing.
          </div>
        </div>
      </div>

    </div>
  </div>
</div>

<script>
(() => {
  // ---------- Helpers ----------
  const clamp = (v,min,max)=>Math.max(min,Math.min(max,v));
  const fmt = (n)=> (n>=0? "" : "-") + "$" + Math.abs(n).toFixed(2);
  const fmt0 = (n)=> (n>=0? "" : "-") + "$" + Math.abs(n).toFixed(0);
  const now = ()=>performance.now();
  const el = (id)=>document.getElementById(id);

  // ---------- Starfield ----------
  const starC = el("stars");
  const sctx = starC.getContext("2d");
  const stars = [];
  for(let i=0;i<900;i++){
    stars.push({
      x:Math.random()*1500,
      y:Math.random()*1500,
      r:Math.random()*1.2,
      a:0.25+Math.random()*0.55,
      tw:Math.random()*Math.PI*2,
      sp:0.2+Math.random()*0.7
    });
  }
  function drawStars(){
    sctx.clearRect(0,0,1500,1500);
    sctx.fillStyle="#000";
    sctx.fillRect(0,0,1500,1500);
    for(const st of stars){
      st.tw += 0.01*st.sp;
      const alpha = st.a*(0.65 + 0.35*Math.sin(st.tw));
      sctx.fillStyle = `rgba(220,235,255,${alpha})`;
      sctx.beginPath();
      sctx.arc(st.x, st.y, st.r, 0, Math.PI*2);
      sctx.fill();
    }
  }

  // ---------- Economy Model ----------
  const COMMS = [
    {id:"ore",  name:"Ore",  base: 24, mass:1},
    {id:"fuel", name:"Fuel", base: 18, mass:1},
    {id:"food", name:"Food", base: 12, mass:1},
    {id:"tech", name:"Tech", base: 55, mass:1}
  ];

  const LOCS = [
    {id:"aegis",   name:"Aegis Station",   dist:{aegis:0, cinder:2, verdant:3, helio:5}},
    {id:"cinder",  name:"Cinder Outpost",  dist:{aegis:2, cinder:0, verdant:2, helio:4}},
    {id:"verdant", name:"Verdant Port",    dist:{aegis:3, cinder:2, verdant:0, helio:2}},
    {id:"helio",   name:"Helio Exchange",  dist:{aegis:5, cinder:4, verdant:2, helio:0}},
  ];

  const baseline = {
    aegis:   { ore:{s:65,d:40}, fuel:{s:45,d:60}, food:{s:55,d:55}, tech:{s:35,d:70} },
    cinder:  { ore:{s:78,d:35}, fuel:{s:30,d:80}, food:{s:40,d:65}, tech:{s:25,d:85} },
    verdant: { ore:{s:35,d:70}, fuel:{s:60,d:45}, food:{s:85,d:25}, tech:{s:40,d:60} },
    helio:   { ore:{s:40,d:65}, fuel:{s:55,d:55}, food:{s:35,d:70}, tech:{s:75,d:35} },
  };

  const market = {};
  for(const L of LOCS){
    market[L.id] = {};
    for(const C of COMMS){
      const b = baseline[L.id][C.id];
      market[L.id][C.id] = { supply:b.s, demand:b.d, histBuy:[], histSell:[], histEvents:[] };
    }
  }

  // tunables
  let feeRate = 0.02;
  let driftRate = 0.040;
  let elasticity = 0.90;
  let impact = 1.00;

  function priceAt(locId, commId){
    const C = COMMS.find(x=>x.id===commId);
    const st = market[locId][commId];
    const s = clamp(st.supply, 1, 99);
    const d = clamp(st.demand, 1, 99);
    const ss = s/100, dd = d/100;

    let mult = Math.pow(0.55 + 1.20*dd, elasticity) / Math.pow(0.55 + 1.20*ss, elasticity);
    mult = clamp(mult, 0.55, 2.10);

    const mid = C.base * mult;
    return { buy: mid*1.04, sell: mid*0.96, mid };
  }

  function driftMarkets(){
    for(const L of LOCS){
      for(const C of COMMS){
        const st = market[L.id][C.id];
        const b = baseline[L.id][C.id];
        st.supply += (b.s - st.supply) * driftRate;
        st.demand += (b.d - st.demand) * driftRate;
        st.supply += (Math.random()-0.5) * 0.18;
        st.demand += (Math.random()-0.5) * 0.18;
        st.supply = clamp(st.supply, 1, 99);
        st.demand = clamp(st.demand, 1, 99);
      }
    }
  }

  // ---------- Player ----------
  const player = {
    loc:"aegis",
    traveling:false,
    dest:null,
    travelEndTick:0,
    credits: 500,
    cargoCap: 30,
    engineLvl: 1,
    intelLvl: 1,
    tripCount:0,
    lifeProfit:0,
    lastProfit:0,
    startTime: now(),
    cargo: { ore:{qty:0,avgCost:0}, fuel:{qty:0,avgCost:0}, food:{qty:0,avgCost:0}, tech:{qty:0,avgCost:0} },
  };

  function cargoUsed(){
    let u=0;
    for(const C of COMMS) u += player.cargo[C.id].qty * C.mass;
    return u;
  }
  function cargoValueHere(){
    let v=0;
    for(const C of COMMS){
      const q = player.cargo[C.id].qty;
      if(q<=0) continue;
      v += q * priceAt(player.loc, C.id).sell;
    }
    return v;
  }
  function netWorth(){ return player.credits + cargoValueHere(); }

  // ---------- UI refs ----------
  const locName = el("locName");
  const stateName = el("stateName");
  const creditsEl = el("credits");
  const cargoCapEl = el("cargoCap");
  const netWorthEl = el("netWorth");
  const etaEl = el("eta");
  const feePill = el("feePill");
  const intelPill = el("intelPill");
  const tickLabel = el("tickLabel");
  const locMini = el("locMini");

  const commoditySelect = el("commoditySelect");
  const destSelect = el("destSelect");

  const buyPriceEl = el("buyPrice");
  const sellPriceEl = el("sellPrice");
  const supplyBar = el("supplyBar");
  const demandBar = el("demandBar");
  const supplyVal = el("supplyVal");
  const demandVal = el("demandVal");

  const qtyInput = el("qtyInput");

  const heldQtyEl = el("heldQty");
  const avgCostEl = el("avgCost");
  const sellValueEl = el("sellValue");
  const feesEstEl = el("feesEst");
  const netIfSoldEl = el("netIfSold");
  const ppTag = el("ppTag");

  const bestHint = el("bestHint");
  const bestHint2 = el("bestHint2");
  const selName = el("selName");
  const selHeld = el("selHeld");
  const bestDest = el("bestDest");
  const bestDist = el("bestDist");
  const bestMargin = el("bestMargin");
  const bestPPM = el("bestPPM");
  const bestExplain = el("bestExplain");

  const lastProfitEl = el("lastProfit");
  const lifeProfitEl = el("lifeProfit");
  const lifePPMEl = el("lifePPM");
  const tripCountEl = el("tripCount");

  const logEl = el("log");

  const travelBtn = el("travelBtn");
  const dockBtn = el("dockBtn");

  const upCargoBtn = el("upCargo");
  const upEngineBtn = el("upEngine");
  const upIntelBtn = el("upIntel");

  // dials
  const feeSlider = el("feeSlider");
  const driftSlider = el("driftSlider");
  const elasSlider = el("elasSlider");
  const impactSlider = el("impactSlider");
  const feeLabel = el("feeLabel");
  const driftLabel = el("driftLabel");
  const elasLabel = el("elasLabel");
  const impactLabel = el("impactLabel");

  const knobFee = el("knobFee");
  const knobDrift = el("knobDrift");
  const knobElas = el("knobElas");
  const knobImpact = el("knobImpact");

  // chart
  const chart = el("chart");
  const cctx = chart.getContext("2d");
  const chartScale = el("chartScale");
  const chartPill = el("chartPill");

  // ---------- Populate selects ----------
  for(const C of COMMS){
    const opt = document.createElement("option");
    opt.value = C.id;
    opt.textContent = C.name;
    commoditySelect.appendChild(opt);
  }
  function refreshDestSelect(){
    destSelect.innerHTML = "";
    for(const L of LOCS){
      if(L.id===player.loc) continue;
      const opt = document.createElement("option");
      opt.value = L.id;
      opt.textContent = L.name;
      destSelect.appendChild(opt);
    }
  }
  refreshDestSelect();

  // ---------- Logging ----------
  function pushLog(msg){
    const t = new Date().toLocaleTimeString([], {hour:"2-digit", minute:"2-digit", second:"2-digit"});
    const div = document.createElement("div");
    div.textContent = `[${t}] ${msg}`;
    logEl.prepend(div);
    while(logEl.childNodes.length>80) logEl.removeChild(logEl.lastChild);
  }

  // ---------- Knob visuals ----------
  function setKnob(knobEl, sliderEl){
    const min = parseFloat(sliderEl.min), max = parseFloat(sliderEl.max), val = parseFloat(sliderEl.value);
    const t = (val - min) / (max - min);
    const sweep = 270;
    const start = 225;
    const fillDeg = Math.round(t * sweep);
    knobEl.style.background =
      `radial-gradient(circle at 35% 30%, rgba(255,255,255,.20), rgba(255,255,255,.06) 45%, rgba(0,0,0,.25) 70%),
       conic-gradient(from ${start}deg, rgba(77,163,255,.92) 0deg, rgba(77,163,255,.18) ${fillDeg}deg, rgba(255,255,255,.08) ${fillDeg}deg, rgba(255,255,255,.05) ${sweep}deg, rgba(255,255,255,.05) 360deg)`;
    const rot = (-135 + t*270);
    knobEl.dataset.rot = rot;
  }
  const ptrStyle = document.createElement("style");
  document.head.appendChild(ptrStyle);
  function refreshPointers(){
    const pairs = [
      ["#knobFee", knobFee.dataset.rot||0],
      ["#knobDrift", knobDrift.dataset.rot||0],
      ["#knobElas", knobElas.dataset.rot||0],
      ["#knobImpact", knobImpact.dataset.rot||0],
    ];
    ptrStyle.textContent = pairs.map(([sel,rot]) =>
      `${sel}:after{transform: translate(-50%,-90%) rotate(${rot}deg);}`
    ).join("\\n");
  }
  function updateAllKnobs(){
    setKnob(knobFee, feeSlider);
    setKnob(knobDrift, driftSlider);
    setKnob(knobElas, elasSlider);
    setKnob(knobImpact, impactSlider);
    refreshPointers();
  }

  // ---------- Trades ----------
  function canTrade(){ return !player.traveling; }
  function parseQty(){
    const raw = (qtyInput.value||"").trim();
    const n = Math.floor(parseFloat(raw));
    if(!Number.isFinite(n) || n<=0) return 1;
    return clamp(n, 1, 999999);
  }
  function setQty(n){ qtyInput.value = String(clamp(Math.floor(n),1,999999)); }

  function buy(commId, qty){
    if(!canTrade()) return;
    qty = Math.floor(qty);
    if(qty<=0) return;

    const C = COMMS.find(x=>x.id===commId);
    const st = market[player.loc][commId];
    const p = priceAt(player.loc, commId).buy;

    const capLeft = player.cargoCap - cargoUsed();
    const maxByCap = Math.floor(capLeft / C.mass);
    const maxByMoney = Math.floor(player.credits / (p * (1+feeRate)));
    const max = Math.max(0, Math.min(maxByCap, maxByMoney));
    const q = Math.min(qty, max);
    if(q<=0){ pushLog("Buy blocked: not enough credits or cargo capacity."); return; }

    const cost = q * p;
    const fee = cost * feeRate;
    player.credits -= (cost + fee);

    const led = player.cargo[commId];
    const oldQty = led.qty;
    const newQty = oldQty + q;
    led.avgCost = newQty>0 ? ((led.avgCost*oldQty) + (p*q)) / newQty : 0;
    led.qty = newQty;

    const k = 0.65 * impact;
    st.supply = clamp(st.supply - (q*k), 1, 99);
    st.demand = clamp(st.demand + (q*k*0.85), 1, 99);

    st.histEvents.push({t:tick, type:"buy"});
    pushLog(`Bought ${q} ${C.name} @ ${fmt(p)} each (fee ${fmt(fee)}).`);
    redrawAll();
  }

  function sell(commId, qty){
    if(!canTrade()) return;
    qty = Math.floor(qty);
    if(qty<=0) return;

    const C = COMMS.find(x=>x.id===commId);
    const st = market[player.loc][commId];
    const p = priceAt(player.loc, commId).sell;

    const led = player.cargo[commId];
    const q = Math.min(qty, led.qty);
    if(q<=0){ pushLog(`Sell blocked: you hold 0 ${C.name}.`); return; }

    const gross = q * p;
    const fee = gross * feeRate;
    const net = gross - fee;

    const profit = (p - led.avgCost) * q - fee;

    player.credits += net;
    led.qty -= q;
    if(led.qty<=0){ led.qty=0; led.avgCost=0; }

    const k = 0.65 * impact;
    st.supply = clamp(st.supply + (q*k), 1, 99);
    st.demand = clamp(st.demand - (q*k*0.85), 1, 99);

    st.histEvents.push({t:tick, type:"sell"});
    player.lastProfit = profit;
    player.lifeProfit += profit;

    pushLog(`Sold ${q} ${C.name} @ ${fmt(p)} each (fee ${fmt(fee)}). Profit vs cost: ${fmt(profit)}.`);
    redrawAll();
  }

  // ---------- Travel ----------
  function travelTo(destId){
    if(player.traveling) return;
    if(destId===player.loc) return;

    const cur = LOCS.find(x=>x.id===player.loc);
    const dist = cur.dist[destId] || 3;

    const baseTicksPerDist = 45;
    const speedMult = 1 + (player.engineLvl-1)*0.22;
    const travelTicks = Math.max(18, Math.round((dist * baseTicksPerDist) / speedMult));

    player.traveling = true;
    player.dest = destId;
    player.travelEndTick = tick + travelTicks;

    travelBtn.disabled = true;
    dockBtn.disabled = false;

    pushLog(`Travel started: ${LOCS.find(x=>x.id===player.loc).name} → ${LOCS.find(x=>x.id===destId).name} (ETA ${travelTicks} ticks).`);
    redrawAll();
  }

  function dock(){
    if(!player.traveling) return;
    if(tick < player.travelEndTick) return;

    player.traveling = false;
    player.loc = player.dest;
    player.dest = null;
    player.travelEndTick = 0;
    player.tripCount += 1;

    travelBtn.disabled = false;
    dockBtn.disabled = true;

    refreshDestSelect();
    pushLog(`Arrived and docked at ${LOCS.find(x=>x.id===player.loc).name}.`);
    redrawAll();
  }

  // ---------- Upgrades ----------
  function upgradeCargo(){
    const lvl = Math.round((player.cargoCap - 30)/10) + 1;
    const cost = 220 + (lvl-1)*180;
    if(player.credits < cost){ pushLog(`Cargo upgrade blocked: need ${fmt0(cost)}.`); return; }
    player.credits -= cost;
    player.cargoCap += 10;
    pushLog(`Upgrade purchased: +10 cargo capacity (cost ${fmt0(cost)}).`);
    redrawAll();
  }
  function upgradeEngine(){
    const lvl = player.engineLvl;
    const cost = 260 + (lvl-1)*240;
    if(player.credits < cost){ pushLog(`Engine upgrade blocked: need ${fmt0(cost)}.`); return; }
    player.credits -= cost;
    player.engineLvl += 1;
    pushLog(`Upgrade purchased: Engine L${player.engineLvl} (cost ${fmt0(cost)}).`);
    redrawAll();
  }
  function upgradeIntel(){
    const lvl = player.intelLvl;
    const cost = 280 + (lvl-1)*260;
    if(player.credits < cost){ pushLog(`Intel upgrade blocked: need ${fmt0(cost)}.`); return; }
    player.credits -= cost;
    player.intelLvl = clamp(player.intelLvl+1, 1, 3);
    pushLog(`Upgrade purchased: Intel L${player.intelLvl} (cost ${fmt0(cost)}).`);
    redrawAll();
  }

  // ---------- History ----------
  const HIST_LEN = 120;
  function recordHistory(){
    for(const L of LOCS){
      for(const C of COMMS){
        const st = market[L.id][C.id];
        const p = priceAt(L.id, C.id);
        st.histBuy.push(p.buy);
        st.histSell.push(p.sell);
        if(st.histBuy.length>HIST_LEN) st.histBuy.shift();
        if(st.histSell.length>HIST_LEN) st.histSell.shift();
        st.histEvents = st.histEvents.filter(e => (tick - e.t) <= (HIST_LEN-1));
      }
    }
  }

  // ---------- Best sell hint ----------
  function calcBestSell(commId){
    const held = player.cargo[commId].qty;
    const basis = player.cargo[commId].avgCost;
    const curLoc = player.loc;
    const qty = held>0 ? held : 1;

    const distances = LOCS
      .map(L => ({id:L.id, d:(LOCS.find(x=>x.id===curLoc).dist[L.id]||99)}))
      .filter(x=>x.id!==curLoc)
      .sort((a,b)=>a.d-b.d);

    let candidateIds = [];
    if(player.intelLvl===1) candidateIds = distances.slice(0,1).map(x=>x.id);
    else if(player.intelLvl===2) candidateIds = distances.slice(0, Math.max(1, distances.length-1)).map(x=>x.id);
    else candidateIds = distances.map(x=>x.id);

    let best = null;
    for(const id of candidateIds){
      const dist = LOCS.find(x=>x.id===curLoc).dist[id] || 3;
      const pSell = priceAt(id, commId).sell;
      const gross = qty * pSell;
      const fee = gross * feeRate;
      const net = gross - fee;

      const pBuyHere = priceAt(curLoc, commId).buy;
      const impliedCost = held>0 ? (qty*basis) : (qty*pBuyHere*(1+feeRate));
      const margin = net - impliedCost;

      const baseTicksPerDist = 45;
      const speedMult = 1 + (player.engineLvl-1)*0.22;
      const travelTicks = Math.max(18, Math.round((dist*baseTicksPerDist)/speedMult));
      const minutes = travelTicks / 60;
      const ppm = minutes>0 ? (margin / minutes) : margin;

      if(!best || ppm > best.ppm){
        best = {id, dist, margin, ppm, pSell, travelTicks};
      }
    }
    return best;
  }

  // ---------- Chart ----------
  function drawChart(){
    const commId = commoditySelect.value;
    const st = market[player.loc][commId];
    const W = chart.width, H = chart.height;
    cctx.clearRect(0,0,W,H);

    cctx.fillStyle = "rgba(0,0,0,.16)";
    cctx.fillRect(0,0,W,H);

    cctx.strokeStyle = "rgba(255,255,255,.08)";
    cctx.lineWidth = 1;
    for(let i=0;i<=5;i++){
      const y = Math.round((i/5)*H);
      cctx.beginPath(); cctx.moveTo(0,y); cctx.lineTo(W,y); cctx.stroke();
    }
    for(let i=0;i<=6;i++){
      const x = Math.round((i/6)*W);
      cctx.beginPath(); cctx.moveTo(x,0); cctx.lineTo(x,H); cctx.stroke();
    }

    const buy = st.histBuy, sell = st.histSell;
    if(buy.length<2){ chartScale.textContent = "—"; return; }

    let lo=Infinity, hi=-Infinity;
    for(let i=0;i<buy.length;i++){
      lo = Math.min(lo, buy[i], sell[i]);
      hi = Math.max(hi, buy[i], sell[i]);
    }
    const pad = (hi-lo)*0.12 + 0.5;
    lo -= pad; hi += pad;
    chartScale.textContent = `${fmt(lo)} → ${fmt(hi)}`;

    const yOf = (v)=> H - ((v-lo)/(hi-lo))*H;
    const xOf = (i)=> (i/(HIST_LEN-1))*W;

    function drawLine(arr, color){
      cctx.strokeStyle = color;
      cctx.lineWidth = 2;
      cctx.beginPath();
      for(let i=0;i<arr.length;i++){
        const x=xOf(i), y=yOf(arr[i]);
        if(i===0) cctx.moveTo(x,y); else cctx.lineTo(x,y);
      }
      cctx.stroke();
    }

    drawLine(buy, "rgba(77,163,255,.95)");
    drawLine(sell,"rgba(255,200,87,.92)");

    for(const e of st.histEvents){
      const age = tick - e.t;
      const idx = Math.max(0, Math.min(HIST_LEN-1, (HIST_LEN-1)-age));
      const x = xOf(idx);
      const y = e.type==="buy" ? yOf(buy[idx]) : yOf(sell[idx]);
      cctx.fillStyle = e.type==="buy" ? "rgba(43,228,167,.95)" : "rgba(255,77,109,.95)";
      cctx.beginPath(); cctx.arc(x,y,3.5,0,Math.PI*2); cctx.fill();
    }
  }

  // ---------- Render ----------
  const cargoTbody = el("cargoTable").querySelector("tbody");

  function redrawCargoTable(){
    cargoTbody.innerHTML = "";
    for(const C of COMMS){
      const led = player.cargo[C.id];
      const q = led.qty;
      const pSell = priceAt(player.loc, C.id).sell;
      const netAfterFeeEa = pSell * (1-feeRate);

      const tr = document.createElement("tr");
      tr.innerHTML = `
        <td>${C.name}</td>
        <td class="rightTxt mono">${q}</td>
        <td class="rightTxt mono">${q>0 ? fmt(led.avgCost) : "—"}</td>
        <td class="rightTxt mono">${fmt(pSell)}</td>
        <td class="rightTxt mono">${fmt(netAfterFeeEa)}</td>
      `;
      cargoTbody.appendChild(tr);
    }
  }

  function redrawMarketPanel(){
    const commId = commoditySelect.value;
    const st = market[player.loc][commId];
    const p = priceAt(player.loc, commId);

    buyPriceEl.textContent = fmt(p.buy);
    sellPriceEl.textContent = fmt(p.sell);

    const s = clamp(st.supply,1,99);
    const d = clamp(st.demand,1,99);
    supplyVal.textContent = s.toFixed(0);
    demandVal.textContent = d.toFixed(0);

    supplyBar.style.width = `${s}%`;
    demandBar.style.width = `${d}%`;
    supplyBar.style.background = `linear-gradient(90deg, rgba(77,163,255,.85), rgba(77,163,255,.25))`;
    demandBar.style.background = `linear-gradient(90deg, rgba(255,200,87,.85), rgba(255,200,87,.22))`;

    const held = player.cargo[commId].qty;
    const basis = player.cargo[commId].avgCost;

    const gross = held * p.sell;
    const fees = gross * feeRate;
    const net = gross - fees;
    const basisTotal = held * basis;
    const profit = net - basisTotal;

    heldQtyEl.textContent = String(held);
    avgCostEl.textContent = held>0 ? fmt(basis) : "—";
    sellValueEl.textContent = held>0 ? fmt(gross) : "—";
    feesEstEl.textContent = held>0 ? fmt(fees) : "—";
    netIfSoldEl.textContent = held>0 ? fmt(profit) : "—";

    if(held<=0){
      ppTag.textContent = "No position";
      ppTag.style.borderColor = "rgba(255,255,255,.14)";
    } else if(profit>0){
      ppTag.textContent = "Profitable here";
      ppTag.style.borderColor = "rgba(43,228,167,.40)";
    } else {
      ppTag.textContent = "Unprofitable here";
      ppTag.style.borderColor = "rgba(255,77,109,.40)";
    }
  }

  function redrawTop(){
    locName.textContent = LOCS.find(x=>x.id===player.loc).name;
    stateName.textContent = player.traveling ? "Traveling" : "Docked";
    creditsEl.textContent = fmt(player.credits);
    cargoCapEl.textContent = `${cargoUsed()}/${player.cargoCap}`;
    netWorthEl.textContent = fmt(netWorth());
    feePill.textContent = `Fee ${(feeRate*100).toFixed(1)}%`;
    intelPill.textContent = `Intel L${player.intelLvl}`;
    chartPill.textContent = LOCS.find(x=>x.id===player.loc).name;
    locMini.textContent = LOCS.find(x=>x.id===player.loc).name;

    if(player.traveling){
      etaEl.textContent = `${Math.max(0, player.travelEndTick - tick)} ticks`;
      travelBtn.disabled = true;
      dockBtn.disabled = (tick < player.travelEndTick);
      destSelect.disabled = true;
    } else {
      etaEl.textContent = "Docked";
      travelBtn.disabled = false;
      dockBtn.disabled = true;
      destSelect.disabled = false;
    }

    const cargoLvl = Math.round((player.cargoCap - 30)/10) + 1;
    upCargoBtn.textContent = `+ Cargo (${fmt0(220 + (cargoLvl-1)*180)})`;
    upEngineBtn.textContent = `+ Engine (${fmt0(260 + (player.engineLvl-1)*240)})`;
    upIntelBtn.textContent = `+ Intel (${fmt0(280 + (player.intelLvl-1)*260)})`;

    lastProfitEl.textContent = player.lastProfit===0 ? "—" : fmt(player.lastProfit);
    lifeProfitEl.textContent = fmt(player.lifeProfit);
    tripCountEl.textContent = String(player.tripCount);

    const mins = (now() - player.startTime) / 60000;
    const ppm = mins>0 ? player.lifeProfit / mins : 0;
    lifePPMEl.textContent = fmt(ppm) + "/min";
  }

  function redrawHints(){
    const commId = commoditySelect.value;
    const C = COMMS.find(x=>x.id===commId);
    const held = player.cargo[commId].qty;
    selName.textContent = C.name;
    selHeld.textContent = String(held);

    const best = calcBestSell(commId);
    if(!best){
      bestHint.textContent = "Best sell hint: —";
      bestHint2.textContent = "";
      bestDest.textContent = "—";
      bestDist.textContent = "—";
      bestMargin.textContent = "—";
      bestPPM.textContent = "—";
      bestExplain.textContent = "";
      return;
    }

    const destName = LOCS.find(x=>x.id===best.id).name;
    bestDest.textContent = destName;
    bestDist.textContent = `${best.dist} jumps`;
    bestMargin.textContent = fmt(best.margin);
    bestPPM.textContent = fmt(best.ppm) + "/min";

    bestHint.textContent = `Best sell hint: ${destName} (${best.margin>=0?"+":""}${fmt(best.margin)})`;
    bestHint2.textContent = `ETA ~${best.travelTicks} ticks • Uses Intel L${player.intelLvl} visibility.`;

    bestExplain.textContent =
      `Hint scope: ` +
      (player.intelLvl===1 ? "nearest station only." : player.intelLvl===2 ? "most stations (excludes farthest)." : "all stations.");
  }

  function redrawAll(){
    redrawTop();
    redrawMarketPanel();
    redrawCargoTable();
    redrawHints();
    drawChart();
    updateAllKnobs();
  }

  // ---------- Tick loop ----------
  let tick = 0;
  function step(){
    tick++;
    driftMarkets();
    recordHistory();

    if(player.traveling && tick >= player.travelEndTick){
      dock();
    }

    tickLabel.textContent = String(tick).padStart(5,"0");

    redrawTop();
    redrawMarketPanel();
    redrawHints();
    drawChart();
  }

  // ---------- Controls ----------
  el("qtyMinus").addEventListener("click", ()=>setQty(parseQty()-1));
  el("qtyPlus").addEventListener("click", ()=>setQty(parseQty()+1));
  el("q1").addEventListener("click", ()=>setQty(1));
  el("q10").addEventListener("click", ()=>setQty(10));

  qtyInput.addEventListener("input", ()=>{
    const n = parseQty();
    qtyInput.value = String(n);
  });

  el("buyBtn").addEventListener("click", ()=>buy(commoditySelect.value, parseQty()));
  el("sellBtn").addEventListener("click", ()=>sell(commoditySelect.value, parseQty()));
  el("buyMax").addEventListener("click", ()=>{
    const commId = commoditySelect.value;
    const p = priceAt(player.loc, commId).buy;
    const capLeft = player.cargoCap - cargoUsed();
    const maxByCap = Math.floor(capLeft);
    const maxByMoney = Math.floor(player.credits / (p*(1+feeRate)));
    buy(commId, Math.max(0, Math.min(maxByCap, maxByMoney)));
  });
  el("sellAll").addEventListener("click", ()=>{
    const commId = commoditySelect.value;
    sell(commId, player.cargo[commId].qty);
  });

  commoditySelect.addEventListener("change", ()=>redrawAll());

  travelBtn.addEventListener("click", ()=>travelTo(destSelect.value));
  dockBtn.addEventListener("click", ()=>dock());

  upCargoBtn.addEventListener("click", ()=>upgradeCargo());
  upEngineBtn.addEventListener("click", ()=>upgradeEngine());
  upIntelBtn.addEventListener("click", ()=>upgradeIntel());

  feeSlider.addEventListener("input", ()=>{
    feeRate = parseFloat(feeSlider.value)/100;
    feeLabel.textContent = (feeRate*100).toFixed(1) + "%";
    redrawAll();
  });
  driftSlider.addEventListener("input", ()=>{
    driftRate = parseFloat(driftSlider.value);
    driftLabel.textContent = driftRate.toFixed(3);
    redrawAll();
  });
  elasSlider.addEventListener("input", ()=>{
    elasticity = parseFloat(elasSlider.value);
    elasLabel.textContent = elasticity.toFixed(2);
    redrawAll();
  });
  impactSlider.addEventListener("input", ()=>{
    impact = parseFloat(impactSlider.value);
    impactLabel.textContent = impact.toFixed(2);
    redrawAll();
  });

  // ---------- Init ----------
  function initDialLabels(){
    feeLabel.textContent = (feeRate*100).toFixed(1) + "%";
    driftLabel.textContent = driftRate.toFixed(3);
    elasLabel.textContent = elasticity.toFixed(2);
    impactLabel.textContent = impact.toFixed(2);
  }
  initDialLabels();

  // seed chart history so it isn’t empty
  for(let i=0;i<30;i++){ driftMarkets(); recordHistory(); tick++; }
  tick = 0;

  pushLog("Ready. Step 1: buy a commodity. Step 2: check the best sell hint. Step 3: travel and sell. Step 4: upgrade.");

  drawStars();
  redrawAll();
  setInterval(step, 250);
  setInterval(drawStars, 80);
})();
</script>
</body>
</html>
'></iframe>
</div>