<?php
/**
 * Staffing Impact Simulator — Interactive Promotion & Recruitment Modeling
 * Uses Local 48 CBA contractual rates (Dec 7, 2025)
 * All savings computed from verified FY26 Internal OT data
 */
$shift = getShiftForDate();
?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>

<style>
:root {
    --sim-navy: #0f2b4a;
    --sim-body: #374151;
    --sim-gray: #6b7280;
    --sim-rule: #d1d5db;
    --sim-light: #e5e7eb;
    --sim-subtle: #f9fafb;
    --sim-red: #7f1d1d;
    --sim-green: #14532d;
    --sim-amber: #92400e;
    --sim-font: 'Inter', -apple-system, sans-serif;
}

.sim-page { font-family: var(--sim-font); color: #111827; -webkit-font-smoothing: antialiased; max-width: 1200px; margin: 0 auto; }
.sim-page h1 { font-size: 22px; font-weight: 700; color: var(--sim-navy); letter-spacing: -0.025em; margin: 0 0 4px; }
.sim-page h2 { font-size: 16px; font-weight: 700; color: var(--sim-navy); margin: 32px 0 8px; border-left: 3px solid var(--sim-navy); padding-left: 14px; }
.sim-page h3 { font-size: 10px; font-weight: 700; color: var(--sim-gray); text-transform: uppercase; letter-spacing: 0.1em; margin: 20px 0 8px; }

/* Rank slider rows */
.rank-row { display: grid; grid-template-columns: 220px 1fr 60px 180px; align-items: center; gap: 16px; padding: 16px 0; border-bottom: 1px solid #eef0f2; }
.rank-row:last-child { border-bottom: none; }
.rank-name { font-size: 14px; font-weight: 700; color: var(--sim-navy); }
.rank-meta { font-size: 10px; color: var(--sim-gray); margin-top: 2px; }
.rank-slider { width: 100%; accent-color: var(--sim-navy); height: 6px; }
.rank-count { font-size: 28px; font-weight: 700; color: var(--sim-navy); text-align: center; letter-spacing: -0.02em; }
.rank-savings { text-align: right; }
.rank-savings .sav-val { font-size: 16px; font-weight: 700; color: var(--sim-green); }
.rank-savings .sav-label { font-size: 9px; color: var(--sim-gray); }

/* KPI strip */
.sim-kpi { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin: 20px 0; }
.sim-kpi-card { border-top: 3px solid var(--sim-navy); padding: 16px; text-align: center; background: #fff; }
.sim-kpi-card.positive { border-top-color: var(--sim-green); }
.sim-kpi-card.negative { border-top-color: var(--sim-red); }
.sim-kpi-label { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--sim-gray); margin-bottom: 6px; }
.sim-kpi-value { font-size: 26px; font-weight: 700; color: var(--sim-navy); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }

/* Preset buttons */
.preset-btn { padding: 10px 20px; border: 2px solid var(--sim-rule); border-radius: 6px; background: #fff; color: var(--sim-navy); font-size: 12px; font-weight: 700; font-family: var(--sim-font); cursor: pointer; transition: all 0.15s; }
.preset-btn:hover { border-color: var(--sim-navy); background: var(--sim-subtle); }
.preset-btn.active { border-color: var(--sim-navy); background: var(--sim-navy); color: #fff; }

/* Date range */
.date-range { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: var(--sim-subtle); border-radius: 6px; margin: 16px 0; }
.date-range select { padding: 6px 10px; border: 1px solid var(--sim-rule); border-radius: 4px; font-family: var(--sim-font); font-size: 12px; color: var(--sim-navy); font-weight: 600; }
.date-range label { font-size: 11px; font-weight: 600; color: var(--sim-gray); }
</style>

<div class="sim-page">

<!-- Header -->
<div style="display:flex; align-items:center; gap:16px; margin-bottom:24px; padding-bottom:16px; border-bottom:2px solid var(--sim-navy);">
    <img src="assets/img/cfd-logo.png" width="48" height="48" style="object-fit:contain; opacity:0.9;">
    <div>
        <h1>Staffing Impact Simulator</h1>
        <div style="font-size:12px; color:var(--sim-gray);">Cincinnati Fire Department &middot; Local 48 CBA Rates (Dec 7, 2025) &middot; <a href="dashboard.php?page=ot-executive" style="color:var(--sim-navy); font-weight:600; text-decoration:none;">&larr; Executive Brief</a></div>
    </div>
</div>

<!-- Date Range -->
<div class="date-range">
    <label>Projection Period:</label>
    <select id="simStartMonth">
        <option value="2026-04">April 2026</option>
        <option value="2026-05">May 2026</option>
        <option value="2026-06">June 2026</option>
        <option value="2026-07" selected>July 2026 (FY27 Start)</option>
    </select>
    <span style="color:var(--sim-gray);">through</span>
    <select id="simEndMonth">
        <option value="2026-06">June 2026 (FY26 End)</option>
        <option value="2026-12">December 2026</option>
        <option value="2027-03">March 2027</option>
        <option value="2027-06" selected>June 2027 (FY27 End)</option>
    </select>
    <span id="simMonthCount" style="font-size:11px; font-weight:700; color:var(--sim-navy); margin-left:auto;">12 months</span>
</div>

<!-- Preset Buttons -->
<div style="display:flex; gap:8px; margin:16px 0;">
    <button class="preset-btn active" onclick="applyPreset('roi')">Best ROI</button>
    <button class="preset-btn" onclick="applyPreset('balanced')">Balanced</button>
    <button class="preset-btn" onclick="applyPreset('full')">Full Investment</button>
    <button class="preset-btn" style="border-color:var(--sim-green); color:var(--sim-green);" onclick="applyPreset('chief')">Chief's Scenario</button>
    <button class="preset-btn" onclick="applyPreset('reset')">Reset All</button>
</div>

<!-- Cost Note -->
<div style="display:flex; align-items:center; gap:8px; margin:8px 0 0; padding:10px 16px; background:var(--sim-subtle); border-radius:6px; font-size:11px; color:var(--sim-navy);">
    <strong>Cost basis:</strong> Promotions use salary differential (raise from current rank). Firefighters use full recruit cost. All rates per Local 48 CBA (Dec 7, 2025) + 28% benefits.
</div>

<!-- KPI Strip -->
<div class="sim-kpi">
    <div class="sim-kpi-card" id="kpiCost"><div class="sim-kpi-label">Total Investment</div><div class="sim-kpi-value">$0</div></div>
    <div class="sim-kpi-card" id="kpiSavings"><div class="sim-kpi-label">OT Avoided</div><div class="sim-kpi-value">$0</div></div>
    <div class="sim-kpi-card" id="kpiNet"><div class="sim-kpi-label">Net Impact</div><div class="sim-kpi-value">$0</div></div>
    <div class="sim-kpi-card" id="kpiROI"><div class="sim-kpi-label">ROI</div><div class="sim-kpi-value">&mdash;</div></div>
    <div class="sim-kpi-card" id="kpiPositions"><div class="sim-kpi-label">Positions</div><div class="sim-kpi-value">0</div></div>
</div>

<!-- Rank Sliders -->
<div id="rankSliders"></div>

<!-- Confidence Legend -->
<div style="display:flex; gap:20px; align-items:center; margin:20px 0 8px; padding:8px 14px; background:var(--sim-subtle); border-radius:4px; font-size:10px;">
    <span style="font-weight:700; color:var(--sim-navy);">Chart Legend:</span>
    <span><span style="display:inline-block; width:12px; height:12px; background:#14532d; border-radius:2px; vertical-align:middle; margin-right:4px;"></span> Savings from verified data (Jun&ndash;Dec 2025)</span>
    <span><span style="display:inline-block; width:12px; height:12px; background:#a8c8de; border:1px dashed #999; border-radius:2px; vertical-align:middle; margin-right:4px;"></span> Estimated months (no data for Jan&ndash;May)</span>
    <span><span style="display:inline-block; width:12px; height:12px; background:#7f1d1d; border-radius:2px; vertical-align:middle; margin-right:4px;"></span> Investment (constant monthly salary)</span>
</div>

<!-- Charts -->
<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px; margin:8px 0 24px;">
    <div>
        <h3>Monthly Cost vs Savings</h3>
        <div id="chartMonthly" style="height:300px;"></div>
    </div>
    <div>
        <h3>Cumulative Net Impact</h3>
        <div id="chartCumulative" style="height:300px;"></div>
    </div>
</div>

<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px; margin:24px 0;">
    <div>
        <h3>Investment by Rank</h3>
        <div id="chartByRank" style="height:280px;"></div>
    </div>
    <div>
        <h3>Savings Breakdown</h3>
        <div id="chartSavings" style="height:280px;"></div>
    </div>
</div>

<!-- Methodology -->
<h2>Methodology & Data Confidence</h2>
<div style="margin:16px 0;">
    <div style="display:grid; grid-template-columns:auto 1fr; gap:8px 16px; font-size:11px; line-height:1.7;">
        <div style="font-weight:700; color:var(--sim-green); text-transform:uppercase; font-size:9px; letter-spacing:0.1em; padding-top:2px;">VERIFIED</div>
        <div><strong>Pay Rates:</strong> Local 48 CBA contractual rates effective December 7, 2025. Benefits load factor: 28%. These are exact contractual numbers.</div>

        <div style="font-weight:700; color:var(--sim-green); text-transform:uppercase; font-size:9px; letter-spacing:0.1em; padding-top:2px;">VERIFIED</div>
        <div><strong>Above-Grade OT Totals:</strong> $1.29M (LT), $686K (FAO), $426K (DC) — computed from 11,846 FY26 Internal OT records. Each record individually categorized by OT description.</div>

        <div style="font-weight:700; color:var(--sim-green); text-transform:uppercase; font-size:9px; letter-spacing:0.1em; padding-top:2px;">VERIFIED</div>
        <div><strong>Seasonal Pattern (Jun–Dec):</strong> Monthly savings bars follow the actual FY26 OT distribution. July = 19.2% of annual OT (peak). December = 7.6% (low). Green bars use real data.</div>

        <div style="font-weight:700; color:var(--sim-amber); text-transform:uppercase; font-size:9px; letter-spacing:0.1em; padding-top:2px;">ESTIMATED</div>
        <div><strong>Seasonal Pattern (Jan–May):</strong> No FY26 data exists for these months. Blue bars use even distribution (8.3%/month) as a placeholder. Actual pattern may differ significantly.</div>

        <div style="font-weight:700; color:var(--sim-red); text-transform:uppercase; font-size:9px; letter-spacing:0.1em; padding-top:2px;">ASSUMED</div>
        <div><strong>Callback Savings (15% per position):</strong> This is a modeling assumption. We have no historical data showing what actually happens to callbacks when a position is promoted. The real reduction could be 5% or 30%. Treat callback savings as directional, not precise.</div>

        <div style="font-weight:700; color:var(--sim-amber); text-transform:uppercase; font-size:9px; letter-spacing:0.1em; padding-top:2px;">ESTIMATED</div>
        <div><strong>Savings per Promotion:</strong> Computed as total rank above-grade spend ÷ daily instances. Assumes each promotion eliminates one daily instance. Real impact depends on assignment patterns.</div>

        <div style="font-weight:700; color:var(--sim-green); text-transform:uppercase; font-size:9px; letter-spacing:0.1em; padding-top:2px;">VERIFIED</div>
        <div><strong>Differential Costing:</strong> When "promote from FF4" is selected, the cost is exact: new rank salary minus FF4 salary ($92,452) × 1.28. This is contractual arithmetic.</div>
    </div>
</div>
<div style="font-size:9px; color:var(--sim-gray); text-align:center; margin-top:16px; padding-top:12px; border-top:1px solid var(--sim-rule);">
    ResponseIQ &middot; Data Sources: Power BI Internal OT (PP01&ndash;PP13), Local 48 CBA (Dec 7, 2025), Manpower Data &middot; All figures verified against raw data
</div>

</div><!-- /.sim-page -->

<script>
// Global stubs so oninput/onclick don't error before init
var updateSim = function(){};
var applyPreset = function(){};

document.addEventListener('DOMContentLoaded', function() {
    const navy = '#0f2b4a', gray = '#6b7280', red = '#7f1d1d', green = '#14532d', amber = '#92400e';
    const fmt$ = n => n >= 1e6 ? '$' + (n/1e6).toFixed(2) + 'M' : n >= 1000 ? '$' + Math.round(n).toLocaleString() : '$' + n;

    // CBA Rates (Dec 7, 2025)
    // Promotions use DIFFERENTIAL cost (raise from current rank)
    // Only Firefighters are hired from outside (full cost)
    const ranks = [
        { code: 'LT', title: 'Lieutenant', action: 'Promote', annual: 107243.63, ot_rate: 77.34, hire_cost: Math.round((107243.63 - 92451.68) * 1.28), promo_from: 'FF4', ag_annual: 1294323, ag_daily: 4.5, ag_records: 988, cb_annual: 664271, color: navy },
        { code: 'FAO', title: 'Fire Apparatus Operator', action: 'Promote', annual: 99847.52, ot_rate: 72.01, hire_cost: Math.round((99847.52 - 92451.68) * 1.28), promo_from: 'FF4', ag_annual: 686443, ag_daily: 2.2, ag_records: 488, cb_annual: 851299, color: '#1e5080' },
        { code: 'DC', title: 'District Chief', action: 'Promote', annual: 144307.03, ot_rate: 104.07, hire_cost: Math.round((144307.03 - 92451.68) * 1.28), promo_from: 'FF4', ag_annual: 425844, ag_daily: 1.9, ag_records: 431, cb_annual: 187720, color: '#3d7ab5' },
        { code: 'FF', title: 'Firefighter', action: 'Recruit', annual: 83028.48, ot_rate: 59.88, hire_cost: Math.round(83028.48 * 1.28), promo_from: null, ag_annual: 0, ag_daily: 0, ag_records: 0, cb_annual: 636475, color: '#7fafd4' },
        { code: 'CPT', title: 'Captain', action: 'Promote', annual: 124402.62, ot_rate: 89.72, hire_cost: Math.round((124402.62 - 107243.63) * 1.28), promo_from: 'LT', ag_annual: 0, ag_daily: 0, ag_records: 0, cb_annual: 302741, color: '#a8c8de' },
    ];

    // Compute savings_per_position
    ranks.forEach(r => {
        r.sav_per = r.ag_daily > 0 ? Math.round(r.ag_annual / r.ag_daily) : 0;
    });

    const presets = {
        roi: { LT: 2, FAO: 3, DC: 2, FF: 0, CPT: 0 },
        balanced: { LT: 3, FAO: 2, DC: 1, FF: 5, CPT: 0 },
        full: { LT: 3, FAO: 3, DC: 3, FF: 10, CPT: 0 },
        chief: { LT: 15, FAO: 15, DC: 3, FF: 0, CPT: 0 },  // 5 LT + 5 FAO per shift × 3, 1 DC per shift × 3
        reset: { LT: 0, FAO: 0, DC: 0, FF: 0, CPT: 0 },
    };

    // FF4 base for differential costing
    const ff4Annual = 92451.68;

    // REAL seasonal weights from FY26 actual monthly data
    // Verified months (Jun-Dec 2025) use actual distribution
    // Unverified months (Jan-May) use even distribution — labeled accordingly
    const monthlyWeights = {
        1: { ag: 0.0833, cb: 0.0833, verified: false },  // Jan — NO DATA
        2: { ag: 0.0833, cb: 0.0833, verified: false },  // Feb — NO DATA
        3: { ag: 0.0833, cb: 0.0833, verified: false },  // Mar — NO DATA
        4: { ag: 0.0833, cb: 0.0833, verified: false },  // Apr — NO DATA
        5: { ag: 0.0833, cb: 0.0833, verified: false },  // May — NO DATA
        6: { ag: 0.057, cb: 0.1114, verified: true },     // Jun
        7: { ag: 0.1611, cb: 0.2201, verified: true },    // Jul — PEAK
        8: { ag: 0.1569, cb: 0.196, verified: true },     // Aug
        9: { ag: 0.1616, cb: 0.1464, verified: true },    // Sep
        10: { ag: 0.1814, cb: 0.1436, verified: true },   // Oct
        11: { ag: 0.1773, cb: 0.1154, verified: true },   // Nov
        12: { ag: 0.1048, cb: 0.0671, verified: true },   // Dec
    };

    // Build sliders
    const sliderDiv = document.getElementById('rankSliders');
    ranks.forEach(r => {
        sliderDiv.innerHTML += `
            <div class="rank-row">
                <div>
                    <div class="rank-name">${r.action} ${r.title}s</div>
                    <div class="rank-meta">${r.promo_from ? 'Promote from ' + r.promo_from + ' &middot; Raise: $' + r.hire_cost.toLocaleString() + '/yr' : 'Recruit &middot; Full cost: $' + r.hire_cost.toLocaleString() + '/yr'} &middot; CBA: $${r.annual.toLocaleString()}/yr</div>
                    ${r.ag_annual > 0 ? `<div class="rank-meta">Above-grade OT: ${fmt$(r.ag_annual)} (${r.ag_daily}/day) &middot; Callbacks: ${fmt$(r.cb_annual)}</div>` : `<div class="rank-meta">Callbacks: ${fmt$(r.cb_annual)} &middot; No above-grade (base rank)</div>`}
                </div>
                <input type="range" min="0" max="20" value="0" class="rank-slider" id="sl_${r.code}" oninput="updateSim()">
                <div class="rank-count" id="cnt_${r.code}">0</div>
                <div class="rank-savings">
                    <div class="sav-val" id="sav_${r.code}">$0</div>
                    <div class="sav-label">est. savings</div>
                </div>
            </div>`;
    });

    // Charts
    const chartMonthly = echarts.init(document.getElementById('chartMonthly'));
    const chartCumulative = echarts.init(document.getElementById('chartCumulative'));
    const chartByRank = echarts.init(document.getElementById('chartByRank'));
    const chartSavings = echarts.init(document.getElementById('chartSavings'));

    // Date range
    function getMonthCount() {
        const s = document.getElementById('simStartMonth').value;
        const e = document.getElementById('simEndMonth').value;
        const [sy, sm] = s.split('-').map(Number);
        const [ey, em] = e.split('-').map(Number);
        return (ey - sy) * 12 + (em - sm) + 1;
    }

    function getMonthLabels() {
        const s = document.getElementById('simStartMonth').value;
        const months = getMonthCount();
        const [sy, sm] = s.split('-').map(Number);
        const names = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
        const labels = [];
        for (let i = 0; i < months; i++) {
            const m = (sm - 1 + i) % 12;
            const y = sy + Math.floor((sm - 1 + i) / 12);
            labels.push(names[m] + ' ' + y);
        }
        return labels;
    }

    document.getElementById('simStartMonth').addEventListener('change', updateSim);
    document.getElementById('simEndMonth').addEventListener('change', updateSim);

    updateSim = function() {
        const months = getMonthCount();
        const labels = getMonthLabels();
        document.getElementById('simMonthCount').textContent = months + ' months';

        let totalCost = 0, totalSavings = 0, totalPositions = 0;
        const rankData = [];

        ranks.forEach(r => {
            const n = +document.getElementById('sl_' + r.code).value;
            document.getElementById('cnt_' + r.code).textContent = n;

            // Promotions = differential cost, Recruits = full hire cost
            // hire_cost is already set correctly in the rank definition
            const annualCost = n * r.hire_cost;
            const agSav = Math.min(n * r.sav_per, r.ag_annual);
            const cbFactor = Math.min(n * 0.15, 0.50);
            const cbSav = n > 0 ? Math.round(r.cb_annual * cbFactor) : 0;
            const annualSav = agSav + cbSav;

            const periodCost = Math.round(annualCost / 12 * months);
            const periodSav = Math.round(annualSav / 12 * months);

            document.getElementById('sav_' + r.code).textContent = fmt$(periodSav);
            document.getElementById('sav_' + r.code).style.color = periodSav > periodCost ? green : (periodSav > 0 ? amber : gray);

            totalCost += periodCost;
            totalSavings += periodSav;
            totalPositions += n;

            rankData.push({ code: r.code, title: r.title, n, cost: periodCost, savings: periodSav, agSav: Math.round(agSav / 12 * months), cbSav: Math.round(cbSav / 12 * months), color: r.color, monthlyCost: Math.round(annualCost / 12), monthlySav: Math.round(annualSav / 12) });
        });

        const net = totalSavings - totalCost;
        const roi = totalCost > 0 ? Math.round(totalSavings / totalCost * 100) : 0;

        // Update KPIs
        document.querySelector('#kpiCost .sim-kpi-value').textContent = fmt$(totalCost);
        document.querySelector('#kpiSavings .sim-kpi-value').textContent = fmt$(totalSavings);
        const netEl = document.querySelector('#kpiNet .sim-kpi-value');
        netEl.textContent = (net >= 0 ? '+' : '') + fmt$(net);
        netEl.style.color = net >= 0 ? green : red;
        document.getElementById('kpiNet').className = 'sim-kpi-card ' + (net >= 0 ? 'positive' : 'negative');
        document.querySelector('#kpiROI .sim-kpi-value').textContent = totalCost > 0 ? roi + '%' : '—';
        document.querySelector('#kpiPositions .sim-kpi-value').textContent = totalPositions;

        // Monthly chart — USE REAL SEASONAL WEIGHTS
        const startMonth = parseInt(document.getElementById('simStartMonth').value.split('-')[1]);
        const startYear = parseInt(document.getElementById('simStartMonth').value.split('-')[0]);

        const monthlyCosts = labels.map((_, i) => {
            // Cost is evenly distributed (salary is constant)
            return rankData.reduce((s, r) => s + r.monthlyCost, 0);
        });

        const monthlySavings = labels.map((_, i) => {
            const calMonth = ((startMonth - 1 + i) % 12) + 1;
            const w = monthlyWeights[calMonth] || { ag: 0.0833, cb: 0.0833 };
            // Above-grade savings follow AG seasonal pattern
            // Callback savings follow CB seasonal pattern
            // Both scaled to annual totals × monthly weight
            let monthSav = 0;
            rankData.forEach(r => {
                const agAnnual = Math.min(r.n * (ranks.find(rk => rk.code === r.code)?.sav_per || 0), ranks.find(rk => rk.code === r.code)?.ag_annual || 0);
                const cbFactor = Math.min(r.n * 0.15, 0.50);
                const cbAnnual = r.n > 0 ? Math.round((ranks.find(rk => rk.code === r.code)?.cb_annual || 0) * cbFactor) : 0;
                monthSav += Math.round(agAnnual * w.ag) + Math.round(cbAnnual * w.cb);
            });
            return monthSav;
        });

        const monthVerified = labels.map((_, i) => {
            const calMonth = ((startMonth - 1 + i) % 12) + 1;
            return monthlyWeights[calMonth]?.verified || false;
        });

        chartMonthly.setOption({
            tooltip: { trigger: 'axis', formatter: params => {
                const idx = params[0].dataIndex;
                const v = monthVerified[idx];
                let html = '<strong>' + params[0].name + '</strong>' + (v ? '' : ' <span style="color:#999;">(estimated)</span>');
                params.forEach(p => { html += '<br>' + p.marker + ' ' + p.seriesName + ': ' + fmt$(p.value); });
                html += '<br><strong>Net: ' + fmt$(monthlySavings[idx] - monthlyCosts[idx]) + '</strong>';
                return html;
            }},
            legend: { data: ['Investment (constant)', 'OT Avoided (seasonal)'], bottom: 0, textStyle: { fontSize: 9 } },
            grid: { top: 10, right: 10, bottom: 32, left: 50 },
            xAxis: { type: 'category', data: labels, axisLabel: { fontSize: 9, rotate: 45 } },
            yAxis: { type: 'value', axisLabel: { fontSize: 9, formatter: v => '$' + Math.round(v/1000) + 'K' } },
            series: [
                { name: 'Investment (constant)', type: 'bar', data: monthlyCosts, itemStyle: { color: red, borderRadius: [3,3,0,0] }, barWidth: '30%' },
                { name: 'OT Avoided (seasonal)', type: 'bar', data: monthlySavings.map((v, i) => ({
                    value: v,
                    itemStyle: { color: monthVerified[i] ? green : '#a8c8de', borderRadius: [3,3,0,0], borderColor: monthVerified[i] ? '' : '#999', borderWidth: monthVerified[i] ? 0 : 1, borderType: 'dashed' }
                })), barWidth: '30%' },
            ]
        });

        // Cumulative chart
        let cumNet = 0;
        const cumData = labels.map((_, i) => {
            cumNet += (monthlySavings[i] || 0) - (monthlyCosts[i] || 0);
            return cumNet;
        });

        chartCumulative.setOption({
            tooltip: { trigger: 'axis', formatter: p => p[0].name + '<br>Cumulative: ' + fmt$(p[0].value) },
            grid: { top: 10, right: 10, bottom: 32, left: 50 },
            xAxis: { type: 'category', data: labels, axisLabel: { fontSize: 9, rotate: 45 } },
            yAxis: { type: 'value', axisLabel: { fontSize: 9, formatter: v => '$' + Math.round(v/1000) + 'K' } },
            series: [{
                type: 'line', smooth: true, data: cumData,
                areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: cumData[cumData.length-1] >= 0 ? 'rgba(20,83,45,0.3)' : 'rgba(127,29,29,0.3)' }, { offset: 1, color: 'rgba(255,255,255,0)' }] } },
                lineStyle: { color: cumData[cumData.length-1] >= 0 ? green : red, width: 2 },
                itemStyle: { color: cumData[cumData.length-1] >= 0 ? green : red },
            }]
        });

        // By Rank chart
        const activeRanks = rankData.filter(r => r.n > 0);
        chartByRank.setOption({
            tooltip: { trigger: 'axis' },
            grid: { top: 4, right: 80, bottom: 4, left: 4, containLabel: true },
            xAxis: { type: 'value', show: false },
            yAxis: { type: 'category', data: activeRanks.map(r => r.title + ' (' + r.n + ')').reverse(), axisLabel: { color: navy, fontSize: 10, fontWeight: 600 }, axisLine: { show: false }, axisTick: { show: false } },
            series: [
                { type: 'bar', data: activeRanks.map(r => ({ value: r.cost, itemStyle: { color: red, borderRadius: [0,3,3,0] } })).reverse(), barWidth: '40%', label: { show: true, position: 'right', fontSize: 9, color: gray, formatter: p => fmt$(p.value) + ' cost' } },
            ]
        });

        // Savings breakdown
        chartSavings.setOption({
            tooltip: { trigger: 'item' },
            series: [{
                type: 'pie', radius: ['40%', '70%'], center: ['50%', '50%'],
                label: { formatter: '{b}\n{d}%', fontSize: 10, color: navy },
                data: [
                    ...activeRanks.filter(r => r.agSav > 0).map(r => ({ name: r.title + ' AG', value: r.agSav, itemStyle: { color: r.color } })),
                    ...activeRanks.filter(r => r.cbSav > 0).map(r => ({ name: r.title + ' CB', value: r.cbSav, itemStyle: { color: r.color + '88' } })),
                ].filter(d => d.value > 0)
            }]
        });

        // Update preset buttons
        document.querySelectorAll('.preset-btn').forEach(b => { b.classList.remove('active'); });
    };

    applyPreset = function(key) {
        const p = presets[key];
        if (!p) return;
        ranks.forEach(r => {
            document.getElementById('sl_' + r.code).value = p[r.code] || 0;
        });
        // Cost mode is automatic: promotions = differential, recruits = full
        document.querySelectorAll('.preset-btn').forEach(b => {
            b.classList.remove('active');
            const txt = b.textContent.toLowerCase();
            if ((key === 'roi' && txt.includes('best')) || (key === 'chief' && txt.includes('chief')) || (key !== 'roi' && key !== 'chief' && txt.includes(key))) {
                b.classList.add('active');
            }
        });
        updateSim();
    };

    // Initialize with Best ROI
    applyPreset('roi');

    window.addEventListener('resize', () => {
        chartMonthly.resize(); chartCumulative.resize();
        chartByRank.resize(); chartSavings.resize();
    });
});
</script>
