<?php
/**
 * OT Intelligence Center — Overtime analysis, cost modeling, and predictive tools
 * Data: 654 days of daily staffing by 22 skill positions (Telestaff/Cronos export)
 * Salary: Local 48 contract rates with 28% benefits load
 * Budget: FY 2026 Approved City Budget
 */
$shift = getShiftForDate();
?>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>

<style>
.info-badge {
    display:inline-flex; align-items:center; justify-content:center;
    width:18px; height:18px; border-radius:50%; background:var(--ios-bg);
    border:1px solid var(--ios-border); cursor:pointer; flex-shrink:0;
    font-size:11px; font-weight:700; color:var(--accent); font-style:italic;
    font-family:Georgia,serif; transition:all 0.2s;
    position:relative;
}
.info-badge:hover { background:var(--accent); color:#fff; border-color:var(--accent); }
.info-popover {
    display:none; position:fixed; z-index:9999;
    width:380px; max-width:90vw; max-height:70vh; overflow-y:auto;
    padding:16px 18px; background:#fff; border-radius:12px;
    box-shadow:0 12px 40px rgba(0,0,0,0.22); border:1px solid var(--ios-border);
    font-size:12px; color:var(--primary); line-height:1.7; font-style:normal;
    font-family:-apple-system,BlinkMacSystemFont,sans-serif; font-weight:400;
}
.info-popover::before { display:none; }
.info-popover.visible { display:block; }
.info-popover .info-title { font-size:13px; font-weight:700; color:var(--primary); margin-bottom:8px; display:flex; align-items:center; gap:6px; }
.info-popover .info-formula { background:var(--ios-bg); border:1px solid var(--ios-border); border-radius:6px; padding:8px 10px; font-family:monospace; font-size:11px; color:#1B6CB0; margin:8px 0; }
.info-popover .info-action { margin-top:10px; padding:8px 12px; background:rgba(140,198,63,0.08); border:1px solid rgba(140,198,63,0.2); border-radius:8px; font-size:11px; color:#4a7c10; }
.info-popover .info-action strong { color:#3d6b0a; }
.info-date-range { display:inline-block; margin-top:10px; padding:4px 10px; background:#0B224008; border:1px solid #0B224015; border-radius:8px; font-size:10px; font-weight:600; color:#1B6CB0; }
.info-date-range svg { vertical-align:-2px; margin-right:4px; }
#burndownTable th[data-sort]:hover { background:var(--ios-border); }
#burndownTable th[data-sort] .sort-arrow { font-size:10px; color:var(--ios-tertiary); margin-left:2px; }
#burndownTable th[data-sort].sort-asc .sort-arrow::after { content:'▲'; color:var(--accent); }
#burndownTable th[data-sort].sort-desc .sort-arrow::after { content:'▼'; color:var(--accent); }
</style>
<script>
// Smart popover positioning — opens toward available viewport space
document.addEventListener('DOMContentLoaded', function() {
    let activePopover = null;

    document.addEventListener('mouseover', function(e) {
        const badge = e.target.closest('.info-badge');
        if (!badge) return;
        const popover = badge.querySelector('.info-popover');
        if (!popover) return;

        // Show it offscreen first to measure
        popover.style.left = '-9999px';
        popover.style.top = '-9999px';
        popover.classList.add('visible');

        const badgeRect = badge.getBoundingClientRect();
        const popRect = popover.getBoundingClientRect();
        const vw = window.innerWidth;
        const vh = window.innerHeight;
        const pad = 12;

        // Horizontal: prefer right-aligned to badge, flip left if no room
        let left = badgeRect.right - popRect.width;
        if (left < pad) left = badgeRect.left;
        if (left + popRect.width > vw - pad) left = vw - pad - popRect.width;
        if (left < pad) left = pad;

        // Vertical: prefer below badge, flip above if no room
        let top = badgeRect.bottom + 8;
        if (top + popRect.height > vh - pad) {
            top = badgeRect.top - popRect.height - 8;
        }
        if (top < pad) top = pad;

        popover.style.left = left + 'px';
        popover.style.top = top + 'px';

        if (activePopover && activePopover !== popover) {
            activePopover.classList.remove('visible');
        }
        activePopover = popover;
    });

    document.addEventListener('mouseout', function(e) {
        const badge = e.target.closest('.info-badge');
        if (!badge) return;
        const popover = badge.querySelector('.info-popover');
        if (!popover) return;

        // Small delay to allow moving mouse into the popover
        setTimeout(() => {
            if (!badge.matches(':hover') && !popover.matches(':hover')) {
                popover.classList.remove('visible');
                if (activePopover === popover) activePopover = null;
            }
        }, 150);
    });

    // Also close when clicking elsewhere
    document.addEventListener('click', function(e) {
        if (activePopover && !e.target.closest('.info-badge') && !e.target.closest('.info-popover')) {
            activePopover.classList.remove('visible');
            activePopover = null;
        }
    });
});
</script>

<!-- Executive Brief Link -->
<a href="dashboard.php?page=ot-executive" style="display:flex; align-items:center; gap:8px; padding:10px 16px; background:linear-gradient(135deg, #0a0e14, #132d4f); border:1px solid rgba(0,200,255,0.2); border-radius:10px; margin-bottom:16px; text-decoration:none; transition:all 0.2s;" onmouseover="this.style.borderColor='rgba(0,200,255,0.5)';this.style.transform='translateY(-1px)';" onmouseout="this.style.borderColor='rgba(0,200,255,0.2)';this.style.transform='';">
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgba(0,220,255,0.9)" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
    <span style="font-size:13px; font-weight:700; color:#fff;">Executive Brief</span>
    <span style="font-size:11px; color:rgba(180,210,230,0.5);">— Stakeholder-ready one-pager with headline metrics, key findings & recommendations</span>
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,200,255,0.7)" stroke-width="2" style="margin-left:auto;"><polyline points="9 18 15 12 9 6"/></svg>
</a>

<!-- Hero Header -->
<div style="
    position:relative; border-radius:16px; overflow:hidden; margin-bottom:24px;
    background:linear-gradient(135deg, #1a0a0a 0%, #4a1010 30%, #8B0000 60%, #DC2626 100%);
    padding:32px 36px; color:#fff;
">
    <div style="position:absolute; top:-40px; right:-40px; width:200px; height:200px; background:radial-gradient(circle, rgba(255,100,100,0.12) 0%, transparent 70%); border-radius:50%;"></div>
    <div style="position:absolute; bottom:-20px; left:30%; width:160px; height:160px; background:radial-gradient(circle, rgba(255,200,0,0.08) 0%, transparent 70%); border-radius:50%;"></div>

    <div style="position:relative; z-index:2;">
        <div style="display:flex; align-items:center; gap:14px; margin-bottom:12px;">
            <div style="width:52px; height:52px; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.15); border-radius:14px; display:flex; align-items:center; justify-content:center;">
                <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#FCD34D" stroke-width="1.5"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
            </div>
            <div>
                <h1 style="font-size:26px; font-weight:800; margin-bottom:2px;">OT Intelligence Center</h1>
                <p style="font-size:13px; color:rgba(255,255,255,0.5);">Overtime Taskforce — Data-Driven Decision Support</p>
            </div>
        </div>
        <p style="font-size:14px; color:rgba(255,255,255,0.7); line-height:1.6; max-width:700px;">
            Actual overtime payroll data from Power BI, daily staffing patterns from Telestaff, leave records from the payroll system, and contract minimum manning from the Local 48 CBA — cross-referenced with the FY 2026 approved city budget. Every number traces to a source system.
        </p>
    </div>
</div>

<!-- Budget Context Strip -->
<div class="grid" style="grid-template-columns:repeat(5,1fr); gap:12px; margin-bottom:24px;">
    <div class="card" style="text-align:center; padding:16px 10px;">
        <div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--ios-tertiary); margin-bottom:6px;">City Budget</div>
        <div style="font-size:22px; font-weight:800; color:var(--primary);">$2.07B</div>
        <div style="font-size:10px; color:var(--ios-tertiary);">FY 2026 All Funds</div>
    </div>
    <div class="card" style="text-align:center; padding:16px 10px;">
        <div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--ios-tertiary); margin-bottom:6px;">Public Safety</div>
        <div style="font-size:22px; font-weight:800; color:#DC2626;">$348.8M</div>
        <div style="font-size:10px; color:var(--ios-tertiary);">60.7% of General Fund</div>
    </div>
    <div class="card" style="text-align:center; padding:16px 10px;">
        <div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--ios-tertiary); margin-bottom:6px;">Fire Sworn</div>
        <div style="font-size:22px; font-weight:800; color:#1B6CB0;">859</div>
        <div style="font-size:10px; color:var(--ios-tertiary);">26 Stations · 4 Districts</div>
    </div>
    <div class="card" style="text-align:center; padding:16px 10px;">
        <div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--ios-tertiary); margin-bottom:6px;">Personnel Cost</div>
        <div style="font-size:22px; font-weight:800; color:#FF9800;">80.9%</div>
        <div style="font-size:10px; color:var(--ios-tertiary);">of General Fund Budget</div>
    </div>
    <div class="card" style="text-align:center; padding:16px 10px;">
        <div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--ios-tertiary); margin-bottom:6px;">Current Academy</div>
        <div style="font-size:22px; font-weight:800; color:#FF9800;">50</div>
        <div style="font-size:10px; color:var(--ios-tertiary);">In training · Aug 2026 deploy</div>
    </div>
</div>

<!-- Data Source & Date Range Panel -->
<div style="background:var(--ios-bg); border:1px solid var(--ios-border); border-radius:12px; margin-bottom:24px; overflow:hidden;">
    <div style="display:flex; align-items:center; gap:8px; padding:10px 16px; border-bottom:1px solid var(--ios-border);">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
        <span style="font-size:12px; font-weight:700; color:var(--primary);">Data Sources & Date Ranges</span>
        <span style="font-size:10px; color:var(--ios-tertiary); margin-left:auto;">Exported Mar 31, 2026</span>
    </div>
    <div style="display:grid; grid-template-columns:repeat(3, 1fr); gap:0; font-size:11px;">
        <!-- Source 1: Power BI OT -->
        <div style="padding:10px 16px; border-right:1px solid var(--ios-border); border-bottom:1px solid var(--ios-border);">
            <div style="font-weight:700; color:var(--primary); margin-bottom:2px;">Power BI — Internal OT</div>
            <div style="color:var(--ios-secondary);">52,109 OT records across 4 fiscal years</div>
            <div style="display:inline-block; margin-top:4px; padding:2px 8px; background:#DC262612; border:1px solid #DC262625; border-radius:8px; font-size:10px; font-weight:600; color:#DC2626;">FY2023 → FY2026</div>
        </div>
        <!-- Source 2: Telestaff -->
        <div style="padding:10px 16px; border-right:1px solid var(--ios-border); border-bottom:1px solid var(--ios-border);">
            <div style="font-weight:700; color:var(--primary); margin-bottom:2px;">Telestaff — Daily Staffing</div>
            <div style="color:var(--ios-secondary);">654 days, 22 skill positions</div>
            <div style="display:inline-block; margin-top:4px; padding:2px 8px; background:#1B6CB012; border:1px solid #1B6CB025; border-radius:8px; font-size:10px; font-weight:600; color:#1B6CB0;">Aug 2023 → Mar 2026</div>
        </div>
        <!-- Source 3: Leave Records -->
        <div style="padding:10px 16px; border-bottom:1px solid var(--ios-border);">
            <div style="font-weight:700; color:var(--primary); margin-bottom:2px;">Payroll — Leave Records</div>
            <div style="color:var(--ios-secondary);">12,069 sick/vacation/FMLA/injury</div>
            <div style="display:inline-block; margin-top:4px; padding:2px 8px; background:#FF980012; border:1px solid #FF980025; border-radius:8px; font-size:10px; font-weight:600; color:#FF9800;">Jan 2018 → Mar 2026</div>
        </div>
        <!-- Source 4: Contract -->
        <div style="padding:10px 16px; border-right:1px solid var(--ios-border);">
            <div style="font-weight:700; color:var(--primary); margin-bottom:2px;">Local 48 CBA — Art. 37</div>
            <div style="color:var(--ios-secondary);">Minimum manning: 199/shift</div>
            <div style="display:inline-block; margin-top:4px; padding:2px 8px; background:#00A6A012; border:1px solid #00A6A025; border-radius:8px; font-size:10px; font-weight:600; color:#00A6A0;">2023–2026 Contract</div>
        </div>
        <!-- Source 5: Burn-down -->
        <div style="padding:10px 16px; border-right:1px solid var(--ios-border);">
            <div style="font-weight:700; color:var(--primary); margin-bottom:2px;">Burn-Down Analysis</div>
            <div style="color:var(--ios-secondary);">269 members, 6+ mo consecutive OT</div>
            <div style="display:inline-block; margin-top:4px; padding:2px 8px; background:#7C3AED12; border:1px solid #7C3AED25; border-radius:8px; font-size:10px; font-weight:600; color:#7C3AED;">Jun 2024 → Feb 2026</div>
        </div>
        <!-- Source 6: Budget -->
        <div style="padding:10px 16px;">
            <div style="font-weight:700; color:var(--primary); margin-bottom:2px;">FY 2026 City Budget</div>
            <div style="color:var(--ios-secondary);">Approved All Funds: $2.07B</div>
            <div style="display:inline-block; margin-top:4px; padding:2px 8px; background:#8CC63F12; border:1px solid #8CC63F25; border-radius:8px; font-size:10px; font-weight:600; color:#5A8C1A;">FY 2026 Approved</div>
        </div>
    </div>
</div>

<!-- Global Date Range Control -->
<div class="card" style="margin-bottom:20px; padding:14px 20px; display:flex; align-items:center; gap:16px; flex-wrap:wrap; border:2px solid var(--accent); background:rgba(27,108,176,0.02);">
    <div style="display:flex; align-items:center; gap:6px;">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
        <span style="font-size:13px; font-weight:700; color:var(--primary);">Analysis Period</span>
    </div>
    <div style="display:flex; align-items:center; gap:8px;">
        <select id="globalStartMonth" onchange="globalRangeChanged()" style="padding:6px 12px; border:1px solid var(--ios-border); border-radius:8px; font-size:12px; font-weight:600; color:var(--primary); background:#fff;"></select>
        <span style="color:var(--ios-tertiary); font-size:13px;">to</span>
        <select id="globalEndMonth" onchange="globalRangeChanged()" style="padding:6px 12px; border:1px solid var(--ios-border); border-radius:8px; font-size:12px; font-weight:600; color:var(--primary); background:#fff;"></select>
    </div>
    <div style="display:flex; gap:6px;">
        <button onclick="setGlobalRange('fy26')" style="padding:5px 14px; border:2px solid var(--accent); border-radius:8px; font-size:11px; font-weight:700; color:var(--accent); background:rgba(27,108,176,0.06); cursor:pointer;">FY 2026</button>
        <button onclick="setGlobalRange('fy25')" style="padding:5px 14px; border:1px solid var(--ios-border); border-radius:8px; font-size:11px; font-weight:600; color:var(--primary); background:#fff; cursor:pointer;">FY 2025</button>
        <button onclick="setGlobalRange('1y')" style="padding:5px 14px; border:1px solid var(--ios-border); border-radius:8px; font-size:11px; font-weight:600; color:var(--primary); background:#fff; cursor:pointer;">1 Year</button>
        <button onclick="setGlobalRange('2y')" style="padding:5px 14px; border:1px solid var(--ios-border); border-radius:8px; font-size:11px; font-weight:600; color:var(--primary); background:#fff; cursor:pointer;">2 Years</button>
        <button onclick="setGlobalRange('all')" style="padding:5px 14px; border:1px solid var(--ios-border); border-radius:8px; font-size:11px; font-weight:600; color:var(--primary); background:#fff; cursor:pointer;">All Data</button>
    </div>
    <div id="globalRangeLabel" style="font-size:11px; color:var(--ios-tertiary); margin-left:auto;"></div>
</div>

<!-- SECTION 1: Key Findings -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Key Findings</h2>
    <a href="https://app.powerbi.com/groups/d5658f26-663a-426d-b6d5-326c8b15484d/reports/7b9fb1ca-7c9b-427e-9deb-99128b41176d/2d0e6aacef08def068eb?experience=power-bi" target="_blank" style="margin-left:auto; display:inline-flex; align-items:center; gap:5px; padding:4px 12px; background:rgba(242,200,17,0.08); border:1px solid rgba(242,200,17,0.25); border-radius:8px; font-size:11px; font-weight:600; color:#D4A300; text-decoration:none;">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><rect x="2" y="6" width="4" height="16" rx="1" fill="#F2C811"/><rect x="8" y="3" width="4" height="19" rx="1" fill="#E8B800"/><rect x="14" y="8" width="4" height="14" rx="1" fill="#D4A300"/></svg>
        View in Power BI →
    </a>
    <span class="info-badge">i
        <div class="info-popover">
            <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> How These Numbers Are Calculated</div>
            <p><strong>Baseline</strong> is derived from the Local 48 CBA Article 37 minimum manning requirements multiplied by the actual CFD apparatus roster — not a statistical estimate.</p>
            <div class="info-formula">27 Engines x 4 = 108 (Art. 37.1)<br>12 Ladders x 4 = 48 (Art. 37.1)<br>2 Heavy Rescues x 4 = 8 (Art. 37.6)<br>13 Medics x 2 = 26 (Art. 37.4)<br>4 District Chiefs x 1 = 4 (Art. 37.5)<br>───────────────<br>194 apparatus + 5 additional = 199 personnel/shift x 24 hrs = 4,776 hrs/day</div>
            <p><strong>Daily OT Delta</strong> = Average daily staffing minus contract baseline. <strong>Annualized</strong> = Daily delta x 365.</p>
            <div class="info-action"><strong>What this means:</strong> Every hour above 4,656/day represents staffing beyond what Article 37 requires. This includes overtime callbacks, training backfill, special assignments, and administrative positions not captured in the minimum. The delta is the department's structural above-minimum cost.</div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Power BI OT + Telestaff + Article 37 · <strong>Jun 2024–Feb 2026 (OT) · Aug 2023–Mar 2026 (staffing)</strong></div>
        </div>
    </span>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Source: Power BI [OT Spend] measure · Breakdowns from Internal OT - Main table</p>

<div class="grid" style="grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:32px;">
    <div class="card" style="padding:20px; border-left:4px solid #DC2626;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">FY26 OT Spend</div>
        <div style="font-size:28px; font-weight:800; color:#DC2626;">$8.85M</div>
        <div id="statOTSpend" style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">Power BI [OT Spend] measure</div>
    </div>
    <div class="card" style="padding:20px; border-left:4px solid #FF9800;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">FY26 Internal OT</div>
        <div id="statOTHours" style="font-size:28px; font-weight:800; color:#FF9800;">—</div>
        <div style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">breakdown detail (13 pay periods)</div>
    </div>
    <div class="card" style="padding:20px; border-left:4px solid #1B6CB0;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">Leave Records</div>
        <div id="statLeave" style="font-size:28px; font-weight:800; color:#1B6CB0;">—</div>
        <div style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">sick + vacation + FMLA + injury</div>
    </div>
    <div class="card" style="padding:20px; border-left:4px solid #00A6A0;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">Contract Minimum</div>
        <div id="statBaseline" style="font-size:28px; font-weight:800; color:#00A6A0;">4,656</div>
        <div style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">Art. 37 · 199 personnel/shift</div>
    </div>
</div>

<!-- SECTION 2: Monthly Staffing Trend -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Monthly Staffing Trend</h2>
    <span class="info-badge">i
        <div class="info-popover">
            <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Monthly Staffing Trend</div>
            <p>Each bar = the <strong>average daily staffing hours</strong> for that month across all 22 skill positions. The green dashed line = the P5 baseline (minimum manning floor).</p>
            <div class="info-formula">Bar height = SUM(all position hours in month) / days in month<br>Green line = P5 baseline (3,730 hrs/day)</div>
            <p><strong>Bar color:</strong> Red = >10% above baseline. Orange = 5-10% above. Blue = within 5%.</p>
            <p>Months with fewer than 15 days are excluded to avoid partial-month distortion.</p>
            <div class="info-action"><strong>What to watch:</strong> If bars are trending upward, the staffing gap is widening. A flat trend near baseline would indicate adequate staffing. The Feb-Mar 2026 uptick (4,981-5,474/day) warrants investigation — retirements? Leave burn? Increased call volume?</div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Telestaff daily staffing export · <strong>Aug 2023 → Mar 2026 (654 days)</strong></div>
        </div>
    </span>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Total daily hours by month — baseline (green) shows minimum manning floor, delta above = estimated overtime</p>

<div class="card" style="margin-bottom:24px;">
    <div id="chartMonthlyTrend" style="height:360px;"></div>
</div>

<!-- SECTION 3: Position Breakdown -->
<div class="grid grid-2" style="margin-bottom:24px;">
    <div>
        <div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
            <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Where the Hours Go</h2>
            <span class="info-badge">i
                <div class="info-popover">
                    <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Total Staffing Hours by Position</div>
                    <p>Each block's size = that position's share of <strong>total staffing hours</strong> across all 654 days. This shows where the department's labor hours concentrate.</p>
                    <div class="info-formula">Block size = Position total hrs / Grand total hrs<br>Color: Blue=Firefighter, Red=Driver/FAO, Orange=Officer/LT, Purple=DC, Teal=Specialty</div>
                    <p>Click any block to drill into its daily trend in the Position Deep Dive chart below.</p>
                    <div class="info-action"><strong>Key insight:</strong> Fire Fighter EMT alone accounts for ~34% of all staffing hours. Combined with FF Medic (17%), firefighter-level positions consume half of all labor. This is where volume-based OT reductions will have the biggest dollar impact.
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Telestaff daily staffing export · <strong>Aug 2023 → Mar 2026 (654 days)</strong></div></div>
                </div>
            </span>
        </div>
        <p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Click any position for daily detail</p>
        <div class="card">
            <div id="chartPositionTreemap" style="height:400px;"></div>
        </div>
    </div>
    <div>
        <div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
            <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Staffing Above Contract Minimum</h2>
            <span class="info-badge">i
                <div class="info-popover">
                    <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Estimated Overtime by Position</div>
                    <p>For each position, the P5 baseline represents the minimum normal staffing. Every hour <strong>above</strong> that baseline on any given day is counted as above-minimum staffing — a proxy for overtime-driven hours.</p>
                    <div class="info-formula">Above-baseline = SUM(max(0, daily_hours - P5_baseline)) over 654 days<br>Only non-zero days counted for baseline calculation</div>
                    <p>This is NOT exact overtime — it's a structural estimate. Positions with high bars have the largest gap between minimum manning and actual staffing.</p>
                    <div class="info-action"><strong>Action priority:</strong> The top 5 positions here (FF EMT, FF Medic, Officer Medic, Driver, Driver Medic) drive ~63% of the above-baseline hours. Targeted hiring or cross-training in these roles yields the highest OT reduction per dollar invested.
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Telestaff + Article 37 baseline · <strong>Aug 2023 → Mar 2026 (654 days)</strong></div></div>
                </div>
            </span>
        </div>
        <p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Telestaff hours above Art. 37 minimum — see Actual OT by Position below for real $ from Power BI</p>
        <div class="card">
            <div id="chartOTByPosition" style="height:400px;"></div>
        </div>
    </div>
</div>

<!-- SECTION 4: Power BI — OT by Reason & Leave Breakdown -->
<div class="grid grid-2" style="margin-bottom:24px;">
    <div>
        <div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
            <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Why OT Happens</h2>
            <span class="info-badge">i
                <div class="info-popover">
                    <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> OT by Reason Code (FY26)</div>
                    <p>Each OT record has an OT description/reason code. This shows why overtime was generated — full tour callbacks, acting above grade, peak demand units, training, etc.</p>
                    <div class="info-action"><strong>Key insight:</strong> "Full Tour OT" (minimum staffing callbacks) and "Above Grade" (acting in higher rank) together account for ~60% of FY26 OT. These are structural — driven by vacancies and promotion pipeline gaps.</div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Power BI Internal OT · <strong>FY2026 (11,846 records)</strong></div>
                </div>
            </span>
        </div>
        <p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">FY26 OT by reason — what's driving the $8.16M</p>
        <div class="card">
            <div id="chartOTByReason" style="height:320px;"></div>
        </div>
    </div>
    <div>
        <div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
            <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Leave Type Breakdown</h2>
            <span class="info-badge">i
                <div class="info-popover">
                    <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> What Drives OT Callbacks?</div>
                    <p>Every leave record represents a shift that needed to be backfilled. The distribution shows which leave types generate the most callbacks — and therefore the most overtime.</p>
                    <div class="info-action"><strong>Key finding:</strong> Vacation and Sick leave together dominate. Vacation is largely predictable and schedulable — better advance planning could reduce the OT it generates. Sick leave is less predictable but patterns (day of week, season) can be used to anticipate it.
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Payroll leave records · <strong>Jan 2018 → Mar 2026 (12,069 records)</strong></div></div>
                </div>
            </span>
        </div>
        <p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Payroll leave records — each absence potentially triggers an OT callback</p>
        <div class="card">
            <div id="chartLeaveBreakdown" style="height:320px;"></div>
        </div>
    </div>
</div>

<!-- OT by Skill Position (actual $) -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Actual OT Spend by Position</h2>
    <span class="info-badge">i
        <div class="info-popover">
            <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Real OT Dollars by Position</div>
            <p>Actual overtime pay from the Manpower Data system — not estimated from salary tables. Each bar represents the total OT dollars paid for that skill position.</p>
            <div class="info-action"><strong>Hiring priority:</strong> The positions with the highest OT spend are where hiring yields the greatest dollar-for-dollar return. FF EMT, FF Medic, and Driver Medic together account for nearly half of all OT spend.</div>
        </div>
    </span>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Real payroll dollars from Power BI Internal OT — 52,109 records across FY23–FY26</p>

<div class="card" style="margin-bottom:24px;">
    <div id="chartActualOTBySkill" style="height:400px;"></div>
</div>

<!-- OT Monthly Trend (actual $) -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">OT Spend Monthly Trend</h2>
    <span class="info-badge">i
        <div class="info-popover">
            <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Monthly OT Cost Trajectory</div>
            <p>Actual OT dollars paid per month from Manpower Data. The trend line shows whether the OT problem is stable, improving, or worsening.</p>
            <div class="info-action"><strong>Budget context:</strong> Track this against the OT budget allocation to project when/if the department will exceed its annual OT budget.</div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Power BI Manpower Data · <strong>FY23–FY26 (52,109 records)</strong></div>
        </div>
    </span>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Actual monthly OT dollars from Power BI Manpower Data</p>

<div class="card" style="margin-bottom:32px;">
    <div id="chartOTMonthlySpend" style="height:320px;"></div>
</div>

<!-- SECTION 5: Burn-Down Analysis -->
<div style="position:relative; border-radius:16px; overflow:hidden; margin-bottom:24px; background:linear-gradient(135deg, #1a0a0a 0%, #3b1010 50%, #8B0000 100%); padding:28px 32px; color:#fff;">
    <div style="position:absolute; top:-30px; right:-30px; width:150px; height:150px; background:radial-gradient(circle, rgba(252,211,77,0.1) 0%, transparent 70%); border-radius:50%;"></div>
    <div style="position:relative; z-index:2;">
        <div style="display:flex; align-items:center; gap:12px; margin-bottom:8px;">
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#FCD34D" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
            <h2 style="font-size:20px; font-weight:800;">Burn-Down Analysis</h2>
            <span class="info-badge" style="background:rgba(255,255,255,0.15); border-color:rgba(255,255,255,0.2); color:#FCD34D;">i
                <div class="info-popover">
                    <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#DC2626" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> "Burning Time" — Corrected Methodology</div>
                    <p>Members systematically consuming accumulated sick, vacation, and comp time before retirement. Each leave hour = a position that needed OT backfill at ~$87.50/hr TRUE rate.</p>
                    <div class="info-formula">Burn-down = members with 200+ leave hours consumed<br>Source: Payroll leave records (12,069 records, Jan 2018–Mar 2026)<br>Cost: Leave pay (paid while absent) + Est. OT backfill ($87.50/hr)<br>Accelerating = recent 6mo burn rate > 130% of earlier rate</div>
                    <div class="info-action"><strong>Why this matters:</strong> These members are draining their leave banks to zero, then retiring. Every day they're absent costs double — their leave pay PLUS someone else's overtime. With leave balance data, we could predict exact retirement dates and budget accordingly.
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Power BI Manpower Data · <strong>Jun 2024 → Feb 2026</strong></div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Payroll leave records · <strong>Jan 2018 → Mar 2026 (12,069 records)</strong></div></div>
                </div>
            </span>
        </div>
        <p style="font-size:13px; color:rgba(255,255,255,0.6);">Members consuming the most accumulated leave — their absence drives OT callbacks</p>
    </div>
</div>

<!-- Date Range Selector -->
<div class="card" style="margin-bottom:16px; padding:14px 20px; display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
    <div style="display:flex; align-items:center; gap:6px;">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
        <span style="font-size:12px; font-weight:700; color:var(--primary);">Date Range:</span>
    </div>
    <div style="display:flex; align-items:center; gap:8px;">
        <select id="bdStartMonth" onchange="updateBurndown()" style="padding:6px 10px; border:1px solid var(--ios-border); border-radius:8px; font-size:12px; font-weight:600; color:var(--primary); background:#fff;"></select>
        <span style="color:var(--ios-tertiary); font-size:13px;">to</span>
        <select id="bdEndMonth" onchange="updateBurndown()" style="padding:6px 10px; border:1px solid var(--ios-border); border-radius:8px; font-size:12px; font-weight:600; color:var(--primary); background:#fff;"></select>
    </div>
    <div style="display:flex; gap:6px; margin-left:auto;">
        <button onclick="setBdRange('1y')" style="padding:4px 12px; border:1px solid var(--ios-border); border-radius:6px; font-size:11px; font-weight:600; color:var(--primary); background:#fff; cursor:pointer;">1 Year</button>
        <button onclick="setBdRange('2y')" style="padding:4px 12px; border:1px solid var(--ios-border); border-radius:6px; font-size:11px; font-weight:600; color:var(--primary); background:#fff; cursor:pointer;">2 Years</button>
        <button onclick="setBdRange('all')" style="padding:4px 12px; border:1px solid var(--ios-border); border-radius:6px; font-size:11px; font-weight:600; color:var(--primary); background:#fff; cursor:pointer;">All Data</button>
        <button onclick="setBdRange('fy26')" style="padding:4px 12px; border:1px solid var(--accent); border-radius:6px; font-size:11px; font-weight:700; color:var(--accent); background:rgba(27,108,176,0.05); cursor:pointer;">FY 2026</button>
    </div>
    <div id="bdRangeLabel" style="font-size:11px; color:var(--ios-tertiary);"></div>
</div>

<div class="grid" style="grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:24px;">
    <div class="card" style="padding:20px; border-left:4px solid #DC2626;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">Top 30 Burners</div>
        <div id="bdCount" style="font-size:28px; font-weight:800; color:#DC2626;">—</div>
        <div style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">highest leave consumption</div>
    </div>
    <div class="card" style="padding:20px; border-left:4px solid #FF9800;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">Total Predictable Cost</div>
        <div id="bdCost" style="font-size:28px; font-weight:800; color:#FF9800;">—</div>
        <div id="bdPct" style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">leave pay + OT backfill</div>
    </div>
    <div class="card" style="padding:20px; border-left:4px solid #1B6CB0;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">OT Backfill Est.</div>
        <div id="bdOT" style="font-size:28px; font-weight:800; color:#1B6CB0;">—</div>
        <div style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">cost to cover their absence</div>
    </div>
    <div class="card" style="padding:20px; border-left:4px solid #7C3AED;">
        <div style="font-size:11px; font-weight:600; color:var(--ios-tertiary); text-transform:uppercase; margin-bottom:8px;">Accelerating</div>
        <div id="bdAccel" style="font-size:28px; font-weight:800; color:#7C3AED;">—</div>
        <div style="font-size:11px; color:var(--ios-secondary); margin-top:4px;">burning faster recently</div>
    </div>
</div>

<!-- Burn-Down Member Table -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Burn-Down Members — By Name</h2>
    <span class="info-badge">i
        <div class="info-popover">
            <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#DC2626" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Individual Leave Consumption Detail</div>
            <p>Each row = one member, ranked by total leave hours consumed. "OT Backfill" = estimated cost to cover their absence at $87.50/hr weighted avg TRUE OT rate. "Rate" = average leave hours consumed per month. Red flag = burn rate accelerating recently.</p>
            <div class="info-action"><strong>If we had leave balance data:</strong> We could add a "Remaining" column and project the exact month each person exhausts their bank and retires. That would make every dollar in this table predictable months in advance.</div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Payroll leave records · <strong>Jan 2018 → Mar 2026 (12,069 records)</strong></div>
        </div>
    </span>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Ranked by total leave consumed — each absence triggers OT callback at ~$87.50/hr</p>

<div class="card" style="margin-bottom:24px; overflow:hidden;">
    <div style="overflow-x:auto;">
        <table id="burndownTable" style="width:100%; border-collapse:collapse; font-size:12px;">
            <thead>
                <tr style="background:var(--ios-bg); border-bottom:2px solid var(--ios-border);">
                    <th data-sort="idx" style="padding:10px 12px; text-align:left; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;"># <span class="sort-arrow"></span></th>
                    <th data-sort="name" style="padding:10px 12px; text-align:left; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Name <span class="sort-arrow"></span></th>
                    <th data-sort="job" style="padding:10px 12px; text-align:left; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Position <span class="sort-arrow"></span></th>
                    <th data-sort="total_hrs" style="padding:10px 12px; text-align:right; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Leave Hrs <span class="sort-arrow"></span></th>
                    <th data-sort="total_cost" style="padding:10px 12px; text-align:right; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Leave Pay <span class="sort-arrow"></span></th>
                    <th data-sort="ot_backfill_est" style="padding:10px 12px; text-align:right; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">OT Backfill Est. <span class="sort-arrow"></span></th>
                    <th data-sort="total_combined" style="padding:10px 12px; text-align:right; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Total Cost <span class="sort-arrow"></span></th>
                    <th data-sort="monthly_rate" style="padding:10px 12px; text-align:right; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Rate/Mo <span class="sort-arrow"></span></th>
                    <th data-sort="top_leave" style="padding:10px 12px; text-align:left; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Top Leave Type <span class="sort-arrow"></span></th>
                    <th data-sort="accelerating" style="padding:10px 12px; text-align:center; font-weight:700; color:var(--primary); cursor:pointer; user-select:none; white-space:nowrap;">Status <span class="sort-arrow"></span></th>
                </tr>
            </thead>
            <tbody id="burndownBody"></tbody>
        </table>
    </div>
</div>

<!-- Burn-down chart -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Leave Consumption — Top 20 Members</h2>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Total leave hours consumed per member — their absence = OT for someone else</p>
<div class="card" style="margin-bottom:32px;">
    <div id="chartBurnMembers" style="height:420px;"></div>
</div>

<!-- SECTION 6: Break-Even Calculator -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Hire vs. Overtime Calculator</h2>
    <span class="info-badge">i
        <div class="info-popover">
            <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Break-Even Analysis Methodology</div>
            <p>Each rank has a <strong>break-even threshold</strong> — the number of OT hours per year at which hiring a new full-time position becomes cheaper than paying overtime.</p>
            <div class="info-formula">Total Hire Cost = Base Salary x 1.28 (28% benefits)<br>TRUE OT Rate = Hourly OT Rate x 1.28<br>Break-Even = Total Hire Cost / TRUE OT Rate<br>Hire Equiv. = Annual OT Cost / Total Hire Cost</div>
            <p><strong>Red "Hire Equiv."</strong> = the OT spend for that rank already exceeds what it would cost to hire that many positions. A value of "5.2x" means the department is spending enough in OT to fund 5.2 full-time hires at that rank.</p>
            <p><strong>Source:</strong> Salary table from Local 48 CBA. Benefits load factor (28%) from the March 25 Taskforce kickoff materials.</p>
            <div class="info-action"><strong>Decision framework:</strong> If a rank shows >1.0x hire equivalents, every unfilled vacancy at that rank costs more in OT than it would to hire. The Taskforce should prioritize hiring where the hire equivalent multiplier is highest — that's the best ROI per recruit.</div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Power BI OT + Local 48 CBA · <strong>Jun 2024–Feb 2026 (OT) · 2023–2026 CBA</strong></div>
        </div>
    </span>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">When does hiring beat overtime? Based on TRUE cost (base salary + 28% benefits load) from Local 48 CBA</p>

<div class="card" style="margin-bottom:24px; overflow:hidden;">
    <div style="overflow-x:auto;">
        <table id="breakevenTable" style="width:100%; border-collapse:collapse; font-size:13px;">
            <thead>
                <tr style="background:var(--ios-bg); border-bottom:2px solid var(--ios-border);">
                    <th style="padding:12px 14px; text-align:left; font-weight:700; color:var(--primary);">Rank</th>
                    <th style="padding:12px 14px; text-align:right; font-weight:700; color:var(--primary);">Base Salary</th>
                    <th style="padding:12px 14px; text-align:right; font-weight:700; color:var(--primary);">Total Hire Cost</th>
                    <th style="padding:12px 14px; text-align:right; font-weight:700; color:var(--primary);">TRUE OT Rate</th>
                    <th style="padding:12px 14px; text-align:right; font-weight:700; color:var(--primary);">Break-Even</th>
                    <th style="padding:12px 14px; text-align:right; font-weight:700; color:var(--primary);">Est. Annual OT Hrs</th>
                    <th style="padding:12px 14px; text-align:right; font-weight:700; color:var(--primary);">Hire Equiv.</th>
                </tr>
            </thead>
            <tbody id="breakevenBody"></tbody>
        </table>
    </div>
</div>

<!-- SECTION 5: Data-Driven Recommendations -->
<h2 style="font-size:18px; font-weight:700; color:var(--primary); margin-bottom:4px;">Data-Driven Recommendations</h2>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Actionable findings derived from the data above — each recommendation includes the math behind it</p>

<div id="recommendationsContainer" style="margin-bottom:32px;">
    <!-- Populated by JS from API data -->
</div>

<!-- SECTION 6: Position Deep Dive -->
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
    <h2 style="font-size:18px; font-weight:700; color:var(--primary);">Position Deep Dive</h2>
    <span class="info-badge">i
        <div class="info-popover">
            <div class="info-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1B6CB0" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> Daily Staffing Detail</div>
            <p>Shows every day's staffing hours for one position across 654 days. The green dashed line = that position's P5 baseline. Days above the line = above-minimum staffing (OT proxy). Days below = staffing dip.</p>
            <p>Use the slider at the bottom to zoom into specific date ranges. The badge shows the position's mapped contract rank, baseline, average, and min/max range.</p>
            <div class="info-action"><strong>What to look for:</strong> Sustained periods above baseline indicate structural understaffing — not one-off spikes. Seasonal patterns (summer, holidays) reveal when leave-driven OT is worst. A rising trend over time suggests the gap is growing and hiring hasn't kept pace with attrition.</div>
                    <div class="info-date-range"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Telestaff daily staffing export · <strong>Aug 2023 → Mar 2026 (654 days)</strong></div>
        </div>
    </span>
</div>
<p style="font-size:13px; color:var(--ios-secondary); margin-bottom:16px;">Select a position to see its daily staffing trend with baseline overlay</p>

<div class="card" style="margin-bottom:24px;">
    <div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
        <select id="positionSelect" onchange="loadPositionDetail(this.value)" style="padding:8px 14px; border:1px solid var(--ios-border); border-radius:8px; font-size:13px; background:#fff; color:var(--primary); font-weight:500; min-width:240px;">
            <option value="">Select a position...</option>
        </select>
        <span id="positionRankBadge" style="display:none; padding:4px 12px; border-radius:20px; font-size:11px; font-weight:700; background:var(--ios-bg); border:1px solid var(--ios-border); color:var(--primary);"></span>
    </div>
    <div id="chartPositionDetail" style="height:300px;"></div>
</div>

<!-- SECTION 6: Methodology -->
<div class="card" style="margin-bottom:24px; background:var(--ios-bg);">
    <div class="card-header"><h2 class="card-title" style="font-size:15px;">Methodology & Data Notes</h2></div>
    <div style="font-size:12px; color:var(--ios-secondary); line-height:1.8;">
        <p><strong>Primary — Power BI [OT Spend] Measure:</strong> The headline FY26 OT spend of $8.85M comes from Power BI's [OT Spend] DAX measure, evaluated via API for the Jul 2025–Mar 2026 date range. This matches the Power BI Overtime Analysis report ($8.74M as of Mar 24). The detailed breakdowns (by position, rank, OT reason, pay period) come from the Internal OT - Main table: 52,109 records across FY23–FY26, with FY26 = 11,846 records totaling $8.16M. The $685K difference between the measure ($8.85M) and the table ($8.16M) represents additional Manpower Data OT captured by the measure but not in the Internal OT table.</p>
        <p style="margin-top:8px;"><strong>Secondary — Telestaff Daily Staffing:</strong> 654 consecutive days (Aug 12, 2023 → Mar 13, 2026) of total daily hours by 22 skill positions. Used for trend analysis, seasonal patterns, and day-of-week analysis. Total column verified against sum of positions.</p>
        <p style="margin-top:8px;"><strong>Leave Records:</strong> 12,069 payroll leave records from the PS_C_PAYCHK_FISCAL table, filtered to sick, vacation, FMLA, and injury earning codes. Each record represents an absence that potentially triggers an OT callback.</p>
        <p style="margin-top:8px;"><strong>Contract Minimum Manning:</strong> Local 48 CBA Article 37 applied to the actual apparatus roster: 27 Engines (x4), 12 Ladders (x4), 2 Heavy Rescues (x4), 13 Medics (x2), 4 District Chiefs (x1) = 194 apparatus minimum + 5 additional required positions = 199 personnel/shift = 4,776 hrs/day. Does not include Hazmat, Bomb, ARFF, Boat, or administrative positions.</p>
        <p style="margin-top:8px;"><strong>Salary Rates:</strong> Local 48 CBA salary table. TRUE OT rate = hourly OT rate × 1.28 (28% benefits load). Used in the Hire vs. Overtime Calculator.</p>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', async function() {
    const navy = '#0B2240', blue = '#1B6CB0', teal = '#00A6A0', green = '#8CC63F', red = '#DC2626', orange = '#FF9800', gold = '#FCD34D';

    // Load summary data + Power BI data
    const [summary, pbi] = await Promise.all([
        window.portalAPI('overtime.php?action=summary'),
        window.portalAPI('overtime.php?action=powerbi'),
    ]);
    if (!summary) return;

    // Populate stat cards with REAL Power BI data
    if (pbi && pbi.manpower_ot) {
        const fmt$ = n => n >= 1000000 ? '$' + (n/1000000).toFixed(2) + 'M' : '$' + Math.round(n).toLocaleString();
        document.getElementById('statOTSpend').textContent = fmt$(pbi.manpower_ot.total_pay);
        document.getElementById('statOTHours').textContent = pbi.manpower_ot.total_hours.toLocaleString();
        document.getElementById('statLeave').textContent = pbi.leave.total_records.toLocaleString();
    } else {
        // Fallback to Telestaff estimates
        document.getElementById('statOTSpend').textContent = '—';
        document.getElementById('statOTHours').textContent = summary.ot_delta_daily.toLocaleString() + '/day';
        document.getElementById('statLeave').textContent = '—';
    }

    // Load monthly data
    const monthlyData = await window.portalAPI('overtime.php?action=monthly');
    if (!monthlyData) return;

    // ─── CHART 1: Monthly Staffing Trend ───
    const months = monthlyData.monthly.filter(m => m.days >= 15); // exclude partial months
    const monthLabels = months.map(m => m.month);
    const monthAvgs = months.map(m => m.daily_avg);

    // Compute baseline avg across all months
    const baselineAvg = summary.baseline_daily;

    const trendChart = echarts.init(document.getElementById('chartMonthlyTrend'));
    trendChart.setOption({
        tooltip: {
            trigger: 'axis',
            formatter: params => {
                let tip = '<strong>' + params[0].axisValue + '</strong><br>';
                params.forEach(p => {
                    if (p.value !== undefined) tip += p.marker + ' ' + p.seriesName + ': <strong>' + p.value.toLocaleString() + '</strong> hrs/day<br>';
                });
                const delta = params[0].value - baselineAvg;
                tip += '<span style="color:#FF9800">Δ Above baseline: <strong>' + delta.toLocaleString() + '</strong> hrs/day</span>';
                return tip;
            }
        },
        legend: { data: ['Daily Avg Staffing', 'Minimum Manning (P5)'], right: 16, top: 0, textStyle: { color: '#5A6A7E', fontSize: 11 } },
        grid: { top: 32, right: 20, bottom: 30, left: 60 },
        xAxis: { type: 'category', data: monthLabels, axisLabel: { color: '#5A6A7E', fontSize: 10, rotate: 45 } },
        yAxis: { type: 'value', name: 'Hours / Day', nameTextStyle: { color: '#999' }, axisLabel: { color: '#5A6A7E' }, splitLine: { lineStyle: { color: '#F0F2F5' } }, min: 3500 },
        series: [{
            name: 'Daily Avg Staffing',
            type: 'bar',
            data: monthAvgs.map(v => ({
                value: v,
                itemStyle: { color: v > baselineAvg * 1.1 ? red : v > baselineAvg * 1.05 ? orange : blue, borderRadius: [4,4,0,0] }
            })),
            barWidth: '60%',
        }, {
            name: 'Minimum Manning (P5)',
            type: 'line',
            data: monthLabels.map(() => baselineAvg),
            lineStyle: { color: green, width: 2, type: 'dashed' },
            symbol: 'none',
            itemStyle: { color: green },
        }]
    });

    // ─── CHART 2: Position Treemap ───
    const treemapData = summary.positions.map(p => ({
        name: p.position,
        value: p.total_hours,
        rank: p.rank,
        pct: p.pct_of_total,
        itemStyle: {
            color: p.rank === 'FF1' ? '#2563EB' : p.rank === 'FAO' ? '#DC2626' : p.rank === 'LT' ? '#FF9800' : p.rank === 'DC' ? '#7C3AED' : '#00A6A0'
        }
    }));

    const treemapChart = echarts.init(document.getElementById('chartPositionTreemap'));
    treemapChart.setOption({
        tooltip: {
            formatter: p => `<strong>${p.name}</strong><br>Hours: ${p.value.toLocaleString()}<br>Share: ${p.data.pct}%<br>Rank: ${p.data.rank}`
        },
        series: [{
            type: 'treemap',
            data: treemapData,
            roam: false,
            nodeClick: false,
            breadcrumb: { show: false },
            label: { show: true, formatter: p => p.name + '\n' + p.data.pct + '%', fontSize: 10, color: '#fff', fontWeight: 600 },
            itemStyle: { borderColor: '#fff', borderWidth: 2, gapWidth: 2 },
            levels: [{ itemStyle: { borderWidth: 2, borderColor: '#fff', gapWidth: 3 } }],
        }]
    });

    treemapChart.on('click', params => {
        document.getElementById('positionSelect').value = params.name;
        loadPositionDetail(params.name);
        document.getElementById('positionSelect').scrollIntoView({ behavior: 'smooth', block: 'center' });
    });

    // ─── CHART 3: OT by Position (horizontal bar) ───
    const otPositions = summary.positions
        .filter(p => p.above_baseline > 0)
        .sort((a, b) => a.above_baseline - b.above_baseline)
        .slice(-15);

    const otChart = echarts.init(document.getElementById('chartOTByPosition'));
    otChart.setOption({
        tooltip: {
            trigger: 'axis',
            formatter: params => {
                const p = params[0];
                const d = otPositions[p.dataIndex];
                return `<strong>${d.position}</strong><br>Above baseline: <strong>${d.above_baseline.toLocaleString()}</strong> hrs<br>Est. OT cost: <strong>$${d.ot_cost_est.toLocaleString()}</strong><br>Rank: ${d.rank}`;
            }
        },
        grid: { top: 8, right: 80, bottom: 8, left: 8, containLabel: true },
        xAxis: { type: 'value', show: false },
        yAxis: {
            type: 'category',
            data: otPositions.map(p => p.position),
            axisLabel: { color: navy, fontSize: 11, fontWeight: 600 },
            axisLine: { show: false }, axisTick: { show: false }
        },
        series: [{
            type: 'bar',
            data: otPositions.map(p => ({
                value: p.above_baseline,
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0,0,1,0,[
                        { offset: 0, color: p.rank === 'FF1' ? blue+'60' : p.rank === 'FAO' ? red+'60' : p.rank === 'LT' ? orange+'60' : teal+'60' },
                        { offset: 1, color: p.rank === 'FF1' ? blue : p.rank === 'FAO' ? red : p.rank === 'LT' ? orange : teal }
                    ]),
                    borderRadius: [0,6,6,0]
                }
            })),
            barWidth: '60%',
            label: { show: true, position: 'right', color: '#5A6A7E', fontSize: 11, formatter: p => p.value.toLocaleString() },
        }]
    });

    // ─── Break-Even Table ───
    const beData = await window.portalAPI('overtime.php?action=breakeven');
    if (beData && beData.breakeven) {
        const tbody = document.getElementById('breakevenBody');
        beData.breakeven.forEach(r => {
            if (r.annual_ot_hours === 0 && r.rank === 'FF-R') return; // skip recruit (no data)
            const overBE = r.annual_ot_hours > r.breakeven_hours;
            const tr = document.createElement('tr');
            tr.style.borderBottom = '1px solid var(--ios-border)';
            tr.innerHTML = `
                <td style="padding:10px 14px; font-weight:600; color:var(--primary);">${r.title}<br><span style="font-size:10px; color:var(--ios-tertiary); font-weight:400;">${r.rank}</span></td>
                <td style="padding:10px 14px; text-align:right; color:var(--primary);">$${r.base_salary.toLocaleString()}</td>
                <td style="padding:10px 14px; text-align:right; color:var(--primary);">$${r.hire_cost.toLocaleString()}</td>
                <td style="padding:10px 14px; text-align:right; color:var(--primary);">$${r.true_ot_rate.toFixed(2)}/hr</td>
                <td style="padding:10px 14px; text-align:right; font-weight:600; color:${navy};">${r.breakeven_hours.toLocaleString()} hrs</td>
                <td style="padding:10px 14px; text-align:right; font-weight:700; color:${overBE ? red : green};">${r.annual_ot_hours.toLocaleString()} hrs</td>
                <td style="padding:10px 14px; text-align:right;">
                    <span style="display:inline-block; padding:3px 10px; border-radius:12px; font-size:12px; font-weight:700; ${overBE ? 'background:rgba(220,38,38,0.1); color:#DC2626;' : 'background:rgba(140,198,63,0.1); color:#8CC63F;'}">${r.hire_equivalents}x</span>
                </td>
            `;
            tbody.appendChild(tr);
        });
    }

    // ─── Populate position selector ───
    const select = document.getElementById('positionSelect');
    summary.positions.forEach(p => {
        const opt = document.createElement('option');
        opt.value = p.position;
        opt.textContent = `${p.position} (${p.pct_of_total}%)`;
        select.appendChild(opt);
    });

    // ─── Build Data-Driven Recommendations ───
    const recContainer = document.getElementById('recommendationsContainer');
    if (recContainer && pbi && pbi.manpower_ot) {
        const recs = [];
        const pbiOT = pbi.manpower_ot;
        const fmt$ = n => n >= 1000000 ? '$' + (n/1000000).toFixed(2) + 'M' : '$' + Math.round(n).toLocaleString();

        // REC 1: Where the $13.19M is going — top rank
        const topRank = pbiOT.by_rank[0]; // sorted by pay descending
        const topSkills = pbiOT.by_skill.slice(0, 5);
        const top5Total = topSkills.reduce((s, d) => s + d.pay, 0);
        const top5Pct = Math.round(top5Total / pbiOT.total_pay * 100);
        recs.push({
            priority: 'CRITICAL',
            color: '#DC2626',
            icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>',
            title: `${fmt$(pbiOT.total_pay)} in actual overtime — ${topRank.rank} rank leads at ${fmt$(topRank.pay)}`,
            body: `Power BI Manpower Data shows <strong>${pbiOT.total_hours.toLocaleString()} actual OT hours</strong> costing <strong>${fmt$(pbiOT.total_pay)}</strong> across ${pbiOT.total_records.toLocaleString()} OT shift records. The top 5 positions account for <strong>${top5Pct}% of all OT spend</strong>.`,
            math: `Source: Power BI Manpower Data (OT records only)\nTotal OT records: ${pbiOT.total_records.toLocaleString()}\nTotal OT hours: ${pbiOT.total_hours.toLocaleString()}\nTotal OT pay: ${fmt$(pbiOT.total_pay)}\n\nTop 5 positions by OT cost:\n${topSkills.map(d => '  ' + d.skill + ': ' + d.hours.toLocaleString() + ' hrs = ' + fmt$(d.pay)).join('\n')}\n\nTop 5 total: ${fmt$(top5Total)} (${top5Pct}% of all OT)`,
            action: `Each new hire at the ${topRank.rank} rank (cost: ~$${Math.round(topRank.pay / Math.max(topRank.hours / 2000, 1)).toLocaleString()}/yr fully loaded) directly offsets this OT. The Dec 2025 recruit class (46 members, deploying July 2026) will enter as Firefighters — the Taskforce should ensure they are assigned to fill the highest-OT companies upon graduation, and advocate for a follow-on class in FY2027.`,
            chartHeight: 170,
            chartInit: (el) => {
                const c = echarts.init(el);
                const d = topSkills.slice(0, 5).reverse();
                c.setOption({
                    grid: { top: 4, right: 80, bottom: 4, left: 4, containLabel: true },
                    xAxis: { type: 'value', show: false },
                    yAxis: { type: 'category', data: d.map(x => x.skill), axisLabel: { color: '#0B2240', fontSize: 10, fontWeight: 600 }, axisLine: { show: false }, axisTick: { show: false } },
                    series: [{ type: 'bar', data: d.map(x => ({ value: Math.round(x.pay), itemStyle: { color: new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:'#DC262640'},{offset:1,color:'#DC2626'}]), borderRadius: [0,5,5,0] } })), barWidth: '55%', label: { show: true, position: 'right', color: '#5A6A7E', fontSize: 10, formatter: p => '$' + (p.value >= 1e6 ? (p.value/1e6).toFixed(1)+'M' : Math.round(p.value/1e3)+'K') } }]
                });
                return c;
            }
        });

        // REC 2: Dec 2025 recruit class — 5 months until relief
        const monthsUntilRelief = 5;
        const pbiMonths = pbiOT.by_month;
        const avgMonthlyOT = pbiMonths.length ? Math.round(pbiOT.total_pay / pbiMonths.length) : 0;
        const projGapCost = avgMonthlyOT * monthsUntilRelief;
        recs.push({
            priority: 'CRITICAL',
            color: '#DC2626',
            icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
            title: 'December 2025 Recruit Class — 5 Months Until Relief',
            body: `The 46-member recruit class started <strong>December 2025</strong> and is currently in the academy. With an 8-month training program, these recruits <strong>will not deploy to companies until July 2026</strong>. At the current OT rate of ~${fmt$(avgMonthlyOT)}/month, the department will spend an estimated <strong>${fmt$(projGapCost)}</strong> in OT between now and when these recruits hit the street.`,
            math: `Recruit class start: December 2025\nAcademy duration: 8 months\nProjected deployment: July 2026\nMonths remaining: ~${monthsUntilRelief}\n\nAvg monthly OT spend (from Power BI): ${fmt$(avgMonthlyOT)}\nProjected OT cost until Jul 2026: ${fmt$(avgMonthlyOT)} x ${monthsUntilRelief} = ~${fmt$(projGapCost)}\n\nMonthly OT trend:\n${pbiMonths.slice(-6).map(m => '  ' + m.month + ': ' + fmt$(m.pay)).join('\n')}`,
            action: `The department will spend approximately ${fmt$(projGapCost)} in OT before seeing any relief from the recruit class. The Taskforce should: (1) Plan for sustained OT through July 2026 — unavoidable, (2) Identify interim measures (voluntary OT caps, mutual aid, temporary schedule adjustments), (3) Begin planning the NEXT recruit class NOW — if a second class starts late 2026, deploy wouldn't be until mid-2027, creating a 12-month gap. (4) Track attrition — every retirement before July widens the gap the 46 recruits need to close.`,
            chartHeight: 100,
            chartInit: (el) => {
                const c = echarts.init(el);
                const mLabels = ['Dec','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug'];
                const mData = mLabels.map((m,i) => i <= 3 ? 1 : 0); // filled vs remaining
                c.setOption({
                    grid: { top: 10, right: 10, bottom: 24, left: 10 },
                    xAxis: { type: 'category', data: mLabels, axisLabel: { color: '#5A6A7E', fontSize: 9 }, axisLine: { lineStyle: { color: '#E5E7EB' } }, axisTick: { show: false } },
                    yAxis: { show: false },
                    series: [{
                        type: 'bar', data: mLabels.map((m,i) => ({
                            value: 1,
                            itemStyle: { color: i <= 3 ? '#DC2626' : i === 8 ? '#8CC63F' : '#E5E7EB', borderRadius: [3,3,0,0] }
                        })), barWidth: '65%',
                    }],
                    graphic: [{ type: 'text', left: 'center', top: 2, style: { text: '← In Academy →                    ← Deploy Aug →', fontSize: 9, fill: '#9CA3AF' } }]
                });
                return c;
            }
        });

        // REC 3: Medic positions — use actual Power BI OT data
        const medicSkills = pbiOT.by_skill.filter(d =>
            d.skill.includes('Medic') || d.skill === 'Paramedic'
        );
        const medicOTTotal = medicSkills.reduce((s, d) => s + d.pay, 0);
        const medicOTPct = Math.round(medicOTTotal / pbiOT.total_pay * 100);
        const medicOTHours = medicSkills.reduce((s, d) => s + d.hours, 0);
        recs.push({
            priority: 'HIGH',
            color: '#FF9800',
            icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>',
            title: 'Expand the paramedic certification pipeline',
            body: `Medic-certified positions account for <strong>${fmt$(medicOTTotal)} (${medicOTPct}%) of all OT spend</strong> — ${medicOTHours.toLocaleString()} hours across ${medicSkills.length} position types. Every position requiring a medic certification has a smaller callback pool, driving disproportionate overtime when vacancies or leave occur.`,
            math: `Source: Power BI Manpower Data (actual OT records)\n\nMedic-position OT breakdown:\n${medicSkills.map(d => '  ' + d.skill + ': ' + d.hours.toLocaleString() + ' hrs = ' + fmt$(d.pay)).join('\n')}\n\nTotal medic OT: ${medicOTHours.toLocaleString()} hrs = ${fmt$(medicOTTotal)}\nTotal all OT: ${pbiOT.total_hours.toLocaleString()} hrs = ${fmt$(pbiOT.total_pay)}\nMedic share: ${medicOTPct}%`,
            action: `Recommend: (1) Fund additional paramedic academy cohorts in FY2027, (2) Offer incentive pay for existing FF-EMTs to obtain medic certification, (3) The Dec 2025 recruits deploy as FF-EMT level in August — medic certification requires additional post-academy training, meaning the medic OT gap will persist even after the class graduates. (4) Every new medic certification reduces OT across multiple positions simultaneously — it's the highest-leverage training investment.`,
            chartHeight: 150,
            chartInit: (el) => {
                const c = echarts.init(el);
                c.setOption({
                    series: [{ type: 'pie', radius: ['40%','70%'], center: ['50%','50%'], label: { formatter: '{b}\n{d}%', fontSize: 10, color: '#0B2240' },
                        data: [
                            { name: 'Medic Positions', value: Math.round(medicOTTotal), itemStyle: { color: '#00A6A0' } },
                            { name: 'Non-Medic', value: Math.round(pbiOT.total_pay - medicOTTotal), itemStyle: { color: '#E5E7EB' } }
                        ]
                    }]
                });
                return c;
            }
        });

        // REC 4: Driver/FAO — use actual Power BI OT data
        const driverSkills = pbiOT.by_skill.filter(d =>
            d.skill.includes('Driver') || d.skill === 'Driver'
        );
        const driverOTTotal = driverSkills.reduce((s, d) => s + d.pay, 0);
        const driverOTPct = Math.round(driverOTTotal / pbiOT.total_pay * 100);
        const driverOTHours = driverSkills.reduce((s, d) => s + d.hours, 0);
        recs.push({
            priority: 'HIGH',
            color: '#FF9800',
            icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>',
            title: 'Address the Fire Apparatus Operator (FAO) certification gap',
            body: `Driver-class positions account for <strong>${fmt$(driverOTTotal)} (${driverOTPct}%) of all OT spend</strong> — ${driverOTHours.toLocaleString()} hours. FAO certification is required to operate apparatus. When a certified driver takes leave, only another FAO can backfill, often at overtime rates.`,
            math: `Source: Power BI Manpower Data (actual OT records)\n\nDriver-position OT breakdown:\n${driverSkills.map(d => '  ' + d.skill + ': ' + d.hours.toLocaleString() + ' hrs = ' + fmt$(d.pay)).join('\n')}\n\nTotal driver OT: ${driverOTHours.toLocaleString()} hrs = ${fmt$(driverOTTotal)}\nDriver share of all OT: ${driverOTPct}%`,
            action: `Recommend: (1) Increase FAO certification classes for existing personnel NOW — this is the only lever before July 2026, (2) Cross-train Firefighters for FAO certification as a career development track, (3) The Dec 2025 recruits deploy as Firefighters in July 2026, NOT as FAOs — the driver gap will persist into 2027 unless existing members are cross-trained. Budget a dedicated FAO cohort for FY2027.`,
            chartHeight: 150,
            chartInit: (el) => {
                const c = echarts.init(el);
                c.setOption({
                    series: [{ type: 'pie', radius: ['40%','70%'], center: ['50%','50%'], label: { formatter: '{b}\n{d}%', fontSize: 10, color: '#0B2240' },
                        data: [
                            { name: 'Driver/FAO', value: Math.round(driverOTTotal), itemStyle: { color: '#DC2626' } },
                            { name: 'Non-Driver', value: Math.round(pbiOT.total_pay - driverOTTotal), itemStyle: { color: '#E5E7EB' } }
                        ]
                    }]
                });
                return c;
            }
        });

        // REC 5: Leave types driving OT
        const leaveData = pbi.leave.by_type;
        const vacCount = leaveData.find(d => d.type === 'Vacation')?.count || 0;
        const sickCount = leaveData.find(d => d.type === 'Sick With Pay')?.count || 0;
        const totalLeave = leaveData.reduce((s, d) => s + d.count, 0);
        const vacPct = Math.round(vacCount / totalLeave * 100);
        const sickPct = Math.round(sickCount / totalLeave * 100);
        recs.push({
            priority: 'HIGH',
            color: '#FF9800',
            icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
            title: `Vacation (${vacPct}%) and Sick Leave (${sickPct}%) dominate leave records`,
            body: `Of <strong>${totalLeave.toLocaleString()} leave records</strong>, Vacation accounts for <strong>${vacCount.toLocaleString()} (${vacPct}%)</strong> and Sick With Pay for <strong>${sickCount.toLocaleString()} (${sickPct}%)</strong>. Each leave record represents an absence that potentially triggers an OT callback to maintain minimum staffing.`,
            math: `Source: Payroll system leave records\n\nLeave type breakdown:\n${leaveData.slice(0, 10).map(d => '  ' + d.type + ': ' + d.count.toLocaleString() + ' (' + Math.round(d.count / totalLeave * 100) + '%)').join('\n')}\n\nTotal leave records: ${totalLeave.toLocaleString()}`,
            action: `Vacation is predictable and schedulable — better advance planning and staggered vacation approvals could reduce callback OT. Sick leave is less predictable but shows patterns (Tuesdays are highest, Sundays lowest from our Telestaff analysis). Recommend: (1) Implement vacation caps per shift per day to prevent clustering, (2) Analyze sick leave patterns by day-of-week and season to predict callback needs, (3) Review whether FMLA and injury leave (172 combined records) can be better managed through return-to-work programs.`,
            chartHeight: 160,
            chartInit: (el) => {
                const c = echarts.init(el);
                const colors = ['#DC2626','#FF9800','#1B6CB0','#00A6A0','#8CC63F','#7C3AED','#6B7280'];
                c.setOption({
                    series: [{ type: 'pie', radius: ['35%','68%'], center: ['50%','50%'], label: { formatter: '{b}\n{c}', fontSize: 9, color: '#0B2240' },
                        data: leaveData.slice(0, 7).map((d, i) => ({ name: d.type.replace('Sick ','S-').replace('With Pay','WP'), value: d.count, itemStyle: { color: colors[i] } }))
                    }]
                });
                return c;
            }
        });

        // REC 6: OT by Reason — structural vs operational
        if (pbi.fy26 && pbi.fy26.by_ot_reason && pbi.fy26.by_ot_reason.length > 0) {
            const topReasons = pbi.fy26.by_ot_reason.slice(0, 5);
            const topReasonTotal = topReasons.reduce((s, r) => s + r.pay, 0);
            const topReasonPct = Math.round(topReasonTotal / pbi.fy26.total_spend * 100);
            recs.push({
                priority: 'MEDIUM',
                color: '#1B6CB0',
                icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6"/></svg>',
                title: `Top 5 OT reasons account for ${topReasonPct}% of FY26 spend`,
                body: `Full Tour OT (minimum staffing callbacks) and Above Grade assignments (acting in higher rank) together drive the majority of overtime. These are <strong>structural</strong> — they reflect vacancies and promotion pipeline gaps, not one-off events.`,
                math: `Source: Power BI Internal OT - Main (FY26)\n\nTop 5 OT reasons:\n${topReasons.map(r => '  ' + r.reason + ': ' + fmt$(r.pay) + ' (' + r.hours.toLocaleString() + ' hrs)').join('\n')}\n\nTop 5 total: ${fmt$(topReasonTotal)} (${topReasonPct}% of FY26)`,
                action: `"Full Tour OT" = minimum staffing callbacks when someone is absent. "Above Grade" = paying FF to act as LT, or LT to act as CPT, because the higher rank is vacant. Reducing these requires: (1) Filling vacancies through hiring/promotion, (2) Reducing absences that trigger callbacks (see Leave analysis above), (3) Reviewing whether Peak Demand units are cost-justified vs the OT they generate.`,
                chartHeight: 160,
                chartInit: (el) => {
                    const c = echarts.init(el);
                    const d = topReasons.slice(0, 5).reverse();
                    const cols = ['#DC2626','#FF9800','#1B6CB0','#00A6A0','#8CC63F'];
                    c.setOption({
                        grid: { top: 4, right: 70, bottom: 4, left: 4, containLabel: true },
                        xAxis: { type: 'value', show: false },
                        yAxis: { type: 'category', data: d.map(r => r.reason.substring(0, 28)), axisLabel: { color: '#0B2240', fontSize: 9, fontWeight: 600 }, axisLine: { show: false }, axisTick: { show: false } },
                        series: [{ type: 'bar', data: d.map((r, i) => ({ value: Math.round(r.pay), itemStyle: { color: cols[d.length - 1 - i], borderRadius: [0,5,5,0] } })), barWidth: '55%', label: { show: true, position: 'right', color: '#5A6A7E', fontSize: 9, formatter: p => '$' + (p.value >= 1e6 ? (p.value/1e6).toFixed(1)+'M' : Math.round(p.value/1e3)+'K') } }]
                    });
                    return c;
                }
            });
        }

        // REC 7: Ongoing monitoring
        recs.push({
            priority: 'FOUNDATION',
            color: '#00A6A0',
            icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>',
            title: 'Establish ongoing OT monitoring and refresh cadence',
            body: `This dashboard combines Power BI payroll data, Telestaff staffing patterns, and payroll leave records. To maintain accuracy, the data needs regular refresh — particularly as the Dec 2025 recruit class deploys in July and its impact becomes measurable.`,
            math: `Current data sources and freshness:\n  Power BI Manpower OT: ${pbiOT.total_records.toLocaleString()} records (exported ${pbi.exported})\n  Telestaff staffing: 654 days (Aug 2023–Mar 2026)\n  Payroll leave: ${pbi.leave.total_records.toLocaleString()} records\n  Holdover OT: ${pbi.holdover_ot.total_records.toLocaleString()} records ($${Math.round(pbi.holdover_ot.total_spend).toLocaleString()})`,
            action: `Recommend: (1) Monthly Power BI data refresh to track OT trajectory, (2) Weekly Telestaff export for real-time staffing pattern monitoring, (3) Post-July 2026 impact assessment — compare OT before/after recruit class deployment, (4) Automate data pipeline from Power BI to this dashboard via scheduled API refresh. The Office of Performance & Data Analytics (Kelsey Braido, per the March 25 meeting) should own the refresh cadence.`
        });

        // Render recommendations with inline charts
        let html = '';
        recs.forEach((rec, i) => {
            const chartDiv = rec.chartInit ? `<div id="recChart${i}" style="height:${rec.chartHeight || 150}px; margin:12px 0; border:1px solid var(--ios-border); border-radius:10px; overflow:hidden;"></div>` : '';
            html += `
            <div class="card" style="margin-bottom:16px; border-left:4px solid ${rec.color}; position:relative;">
                <div style="display:flex; gap:16px; padding:4px 0;">
                    <div style="width:44px; height:44px; background:${rec.color}12; border-radius:12px; display:flex; align-items:center; justify-content:center; flex-shrink:0; color:${rec.color};">
                        ${rec.icon}
                    </div>
                    <div style="flex:1; min-width:0;">
                        <div style="display:flex; align-items:center; gap:10px; margin-bottom:6px;">
                            <span style="padding:2px 10px; border-radius:10px; font-size:10px; font-weight:800; letter-spacing:0.5px; background:${rec.color}15; color:${rec.color}; border:1px solid ${rec.color}30;">${rec.priority}</span>
                            <h3 style="font-size:15px; font-weight:700; color:var(--primary); margin:0;">${rec.title}</h3>
                        </div>
                        <p style="font-size:13px; color:var(--ios-secondary); line-height:1.6; margin-bottom:10px;">${rec.body}</p>

                        ${chartDiv}

                        <details style="margin-bottom:10px;">
                            <summary style="font-size:12px; font-weight:600; color:var(--accent); cursor:pointer; user-select:none;">Show the math</summary>
                            <pre style="margin-top:8px; padding:12px 14px; background:var(--ios-bg); border:1px solid var(--ios-border); border-radius:8px; font-size:11px; color:#1B6CB0; line-height:1.6; white-space:pre-wrap; font-family:'SF Mono',Monaco,monospace; overflow-x:auto;">${rec.math}</pre>
                        </details>

                        <div style="padding:10px 14px; background:rgba(140,198,63,0.06); border:1px solid rgba(140,198,63,0.15); border-radius:10px;">
                            <div style="font-size:11px; font-weight:700; color:#3d6b0a; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Recommended Action</div>
                            <p style="font-size:12px; color:#4a7c10; line-height:1.6; margin:0;">${rec.action}</p>
                        </div>
                    </div>
                </div>
            </div>`;
        });
        recContainer.innerHTML = html;

        // Initialize inline charts
        const recChartInstances = [];
        recs.forEach((rec, i) => {
            if (rec.chartInit) {
                const el = document.getElementById('recChart' + i);
                if (el) {
                    try {
                        const chart = rec.chartInit(el);
                        if (chart) recChartInstances.push(chart);
                    } catch(e) { console.warn('Rec chart ' + i + ' error:', e); }
                }
            }
        });
        window.addEventListener('resize', () => recChartInstances.forEach(c => c.resize()));
    }

    // ─── BURN-DOWN (Leave Consumption with Date Range) ───
    const bdCharts = [];
    const bd = await window.portalAPI('overtime.php?action=burndown');
    if (bd && bd.members && bd.available_months) {
        const fmt$ = n => n >= 1000000 ? '$' + (n/1000000).toFixed(2) + 'M' : '$' + Math.round(n).toLocaleString();
        const OT_RATE = bd.ot_rate || 87.50;
        const allMonths = bd.available_months;
        const rawMembers = bd.members;

        // Populate month dropdowns
        const startSel = document.getElementById('bdStartMonth');
        const endSel = document.getElementById('bdEndMonth');
        allMonths.forEach(m => {
            const label = new Date(m + '-01').toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
            startSel.add(new Option(label, m));
            endSel.add(new Option(label, m));
        });
        startSel.value = allMonths[0];
        endSel.value = allMonths[allMonths.length - 1];

        // State
        let bdSortKey = 'total_hrs';
        let bdSortDir = 'desc';
        let bdFiltered = [];
        let burnMemberChart = null;

        function computeBurndown() {
            const startM = startSel.value;
            const endM = endSel.value;

            // Filter each member's monthly data to the selected range
            bdFiltered = rawMembers.map(m => {
                const inRange = m.months.filter(mo => mo.m >= startM && mo.m <= endM);
                const hrs = inRange.reduce((s, mo) => s + mo.h, 0);
                const cost = inRange.reduce((s, mo) => s + mo.c, 0);
                const ot = hrs * OT_RATE;
                const monthCount = inRange.length;
                const rate = monthCount > 0 ? +(hrs / monthCount).toFixed(1) : 0;

                // Top leave type in range
                const typeTotals = {};
                inRange.forEach(mo => { typeTotals[mo.t] = (typeTotals[mo.t] || 0) + mo.h; });
                const topType = Object.entries(typeTotals).sort((a, b) => b[1] - a[1])[0];

                // Accelerating? Compare last half vs first half of selected range
                const mid = Math.floor(inRange.length / 2);
                const first = inRange.slice(0, mid);
                const second = inRange.slice(mid);
                const firstRate = first.length ? first.reduce((s, mo) => s + mo.h, 0) / first.length : 0;
                const secondRate = second.length ? second.reduce((s, mo) => s + mo.h, 0) / second.length : 0;
                const accelerating = firstRate > 0 && secondRate > firstRate * 1.3;

                return {
                    ...m, total_hrs: hrs, total_cost: cost, ot_backfill_est: ot,
                    total_combined: cost + ot, monthly_rate: rate,
                    top_leave: topType ? topType[0] : '', accelerating,
                    months_in_range: monthCount,
                };
            }).filter(m => m.total_hrs > 0).sort((a, b) => b.total_hrs - a.total_hrs);

            // Update label
            const rangeLabel = document.getElementById('bdRangeLabel');
            const filteredMonths = allMonths.filter(m => m >= startM && m <= endM);
            rangeLabel.textContent = `${filteredMonths.length} months selected · ${bdFiltered.length} members with leave`;
        }

        function updateStats() {
            const top30 = bdFiltered.slice(0, 30);
            const totalCost = top30.reduce((s, m) => s + m.total_combined, 0);
            const totalOT = top30.reduce((s, m) => s + m.ot_backfill_est, 0);
            const accelCount = top30.filter(m => m.accelerating).length;

            document.getElementById('bdCount').textContent = bdFiltered.length.toLocaleString();
            document.getElementById('bdCost').textContent = fmt$(totalCost);
            document.getElementById('bdOT').textContent = fmt$(totalOT);
            document.getElementById('bdAccel').textContent = accelCount + ' of ' + Math.min(30, bdFiltered.length);
        }

        function renderBurndownTable() {
            const data = bdFiltered.slice(0, 30);
            const sorted = [...data].sort((a, b) => {
                let va = a[bdSortKey], vb = b[bdSortKey];
                if (typeof va === 'string') { va = va.toLowerCase(); vb = (vb||'').toLowerCase(); }
                if (typeof va === 'boolean') { va = va ? 1 : 0; vb = vb ? 1 : 0; }
                if (va < vb) return bdSortDir === 'asc' ? -1 : 1;
                if (va > vb) return bdSortDir === 'asc' ? 1 : -1;
                return 0;
            });

            const tbody = document.getElementById('burndownBody');
            tbody.innerHTML = '';
            sorted.forEach((m, i) => {
                const accelBadge = m.accelerating
                    ? '<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:9px;font-weight:700;background:#DC262615;color:#DC2626;border:1px solid #DC262630;">ACCELERATING</span>'
                    : '<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:9px;font-weight:600;background:var(--ios-bg);color:var(--ios-tertiary);border:1px solid var(--ios-border);">Steady</span>';
                const tr = document.createElement('tr');
                tr.style.borderBottom = '1px solid var(--ios-border)';
                tr.innerHTML = `
                    <td style="padding:8px 12px; color:var(--ios-tertiary); font-weight:600;">${i + 1}</td>
                    <td style="padding:8px 12px; font-weight:700; color:var(--primary);">${m.name}</td>
                    <td style="padding:8px 12px; color:var(--ios-secondary); font-size:11px;">${m.job}</td>
                    <td style="padding:8px 12px; text-align:right; font-weight:600; color:var(--primary);">${m.total_hrs.toLocaleString()}</td>
                    <td style="padding:8px 12px; text-align:right; color:var(--ios-secondary);">$${Math.round(m.total_cost).toLocaleString()}</td>
                    <td style="padding:8px 12px; text-align:right; font-weight:600; color:#DC2626;">$${Math.round(m.ot_backfill_est).toLocaleString()}</td>
                    <td style="padding:8px 12px; text-align:right; font-weight:700; color:#FF9800;">$${Math.round(m.total_combined).toLocaleString()}</td>
                    <td style="padding:8px 12px; text-align:right; color:var(--primary);">${m.monthly_rate}/mo</td>
                    <td style="padding:8px 12px; font-size:11px; color:var(--ios-secondary);">${m.top_leave}</td>
                    <td style="padding:8px 12px; text-align:center;">${accelBadge}</td>
                `;
                tbody.appendChild(tr);
            });

            document.querySelectorAll('#burndownTable th[data-sort]').forEach(th => {
                th.classList.remove('sort-asc', 'sort-desc');
                if (th.dataset.sort === bdSortKey) th.classList.add(bdSortDir === 'asc' ? 'sort-asc' : 'sort-desc');
            });
        }

        function renderBurndownChart() {
            const top20 = bdFiltered.slice(0, 20).reverse();
            const el = document.getElementById('chartBurnMembers');
            if (!burnMemberChart) { burnMemberChart = echarts.init(el); bdCharts.push(burnMemberChart); }
            burnMemberChart.setOption({
                tooltip: { trigger: 'axis', formatter: p => {
                    const m = top20[p[0].dataIndex];
                    return `<strong>${m.name}</strong><br>${m.job}<br>Leave: <strong>${m.total_hrs.toLocaleString()} hrs</strong><br>Leave Pay: $${Math.round(m.total_cost).toLocaleString()}<br>Est OT Backfill: <strong>$${Math.round(m.ot_backfill_est).toLocaleString()}</strong><br>Rate: ${m.monthly_rate} hrs/mo${m.accelerating ? '<br><span style="color:#DC2626">ACCELERATING</span>' : ''}`;
                }},
                grid: { top: 8, right: 100, bottom: 8, left: 8, containLabel: true },
                xAxis: { type: 'value', show: false },
                yAxis: { type: 'category', data: top20.map(m => m.name), axisLabel: { color: navy, fontSize: 10, fontWeight: 600 }, axisLine: { show: false }, axisTick: { show: false } },
                series: [{
                    name: 'Leave Pay', type: 'bar', stack: 'cost',
                    data: top20.map(m => ({ value: Math.round(m.total_cost), itemStyle: { color: '#FF9800' } })),
                    barWidth: '55%',
                }, {
                    name: 'OT Backfill', type: 'bar', stack: 'cost',
                    data: top20.map(m => ({ value: Math.round(m.ot_backfill_est), itemStyle: { color: '#DC2626', borderRadius: [0,5,5,0] } })),
                    barWidth: '55%',
                    label: { show: true, position: 'right', color: '#5A6A7E', fontSize: 10, formatter: p => {
                        const total = top20[p.dataIndex].total_combined;
                        return '$' + (total >= 1e6 ? (total/1e6).toFixed(1)+'M' : Math.round(total/1e3)+'K');
                    }},
                }]
            });
        }

        // Sort click handlers
        document.querySelectorAll('#burndownTable th[data-sort]').forEach(th => {
            th.addEventListener('click', () => {
                const key = th.dataset.sort;
                if (bdSortKey === key) bdSortDir = bdSortDir === 'desc' ? 'asc' : 'desc';
                else { bdSortKey = key; bdSortDir = (key === 'name' || key === 'job' || key === 'top_leave') ? 'asc' : 'desc'; }
                renderBurndownTable();
            });
        });

        // Global update function called by dropdowns and preset buttons
        window.updateBurndown = function() {
            computeBurndown();
            updateStats();
            renderBurndownTable();
            renderBurndownChart();
        };

        window.setBdRange = function(preset) {
            const last = allMonths[allMonths.length - 1];
            const lastDate = new Date(last + '-01');
            let startDate;
            if (preset === '1y') { startDate = new Date(lastDate); startDate.setFullYear(startDate.getFullYear() - 1); }
            else if (preset === '2y') { startDate = new Date(lastDate); startDate.setFullYear(startDate.getFullYear() - 2); }
            else if (preset === 'fy26') { startDate = new Date('2025-07-01'); } // FY26 = Jul 2025 - Jun 2026
            else { startDate = new Date(allMonths[0] + '-01'); }
            const startYM = startDate.toISOString().substring(0, 7);
            const closest = allMonths.find(m => m >= startYM) || allMonths[0];
            startSel.value = closest;
            endSel.value = last;
            updateBurndown();
        };

        // Initial render with all data
        updateBurndown();

    }

    // ─── POWER BI CHARTS ───
    const pbiCharts = [];
    if (pbi && pbi.manpower_ot) {
        // OT by Reason (horizontal bar — FY26 data)
        const reasonEl = document.getElementById('chartOTByReason');
        if (reasonEl && pbi.fy26 && pbi.fy26.by_ot_reason) {
            const reasons = pbi.fy26.by_ot_reason.slice(0, 10).reverse();
            const reasonChart = echarts.init(reasonEl);
            const reasonColors = ['#DC2626','#FF9800','#1B6CB0','#00A6A0','#8CC63F','#7C3AED','#0B2240','#6B7280','#FCD34D','#E5E7EB'];
            reasonChart.setOption({
                tooltip: { trigger: 'axis', formatter: p => { const d = reasons[p[0].dataIndex]; return `<strong>${d.reason}</strong><br>Cost: <strong>$${Math.round(d.pay).toLocaleString()}</strong><br>Hours: ${d.hours.toLocaleString()}<br>Records: ${d.count.toLocaleString()}`; } },
                grid: { top: 4, right: 80, bottom: 4, left: 4, containLabel: true },
                xAxis: { type: 'value', show: false },
                yAxis: { type: 'category', data: reasons.map(d => d.reason.substring(0, 30)), axisLabel: { color: navy, fontSize: 9, fontWeight: 600 }, axisLine: { show: false }, axisTick: { show: false } },
                series: [{ type: 'bar', data: reasons.map((d, i) => ({ value: Math.round(d.pay), itemStyle: { color: reasonColors[reasons.length - 1 - i] || blue, borderRadius: [0,5,5,0] } })), barWidth: '55%', label: { show: true, position: 'right', color: '#5A6A7E', fontSize: 9, formatter: p => '$' + (p.value >= 1e6 ? (p.value/1e6).toFixed(1)+'M' : Math.round(p.value/1e3)+'K') } }]
            });
            pbiCharts.push(reasonChart);
        }

        // Leave Breakdown (donut)
        const leaveColors = ['#DC2626','#FF9800','#1B6CB0','#00A6A0','#8CC63F','#7C3AED','#6B7280','#FCD34D'];
        const leaveChart = echarts.init(document.getElementById('chartLeaveBreakdown'));
        leaveChart.setOption({
            tooltip: { trigger: 'item', formatter: p => `<strong>${p.name}</strong><br>${p.value.toLocaleString()} records (${p.percent}%)` },
            series: [{
                type: 'pie', radius: ['45%', '75%'], center: ['50%', '55%'],
                label: { formatter: '{b}\n{c}', fontSize: 10, color: navy },
                data: pbi.leave.by_type.slice(0, 8).map((d, i) => ({
                    name: d.type, value: d.count,
                    itemStyle: { color: leaveColors[i % leaveColors.length] }
                }))
            }]
        });
        pbiCharts.push(leaveChart);

        // Actual OT by Skill (horizontal bar with $)
        const skillData = pbi.manpower_ot.by_skill.filter(d => d.pay > 1000).slice(0, 15).reverse();
        const skillChart = echarts.init(document.getElementById('chartActualOTBySkill'));
        skillChart.setOption({
            tooltip: { trigger: 'axis', formatter: p => { const d = skillData[p[0].dataIndex]; return `<strong>${d.skill}</strong><br>OT Pay: <strong>$${Math.round(d.pay).toLocaleString()}</strong><br>Hours: ${d.hours.toLocaleString()}<br>Records: ${d.count.toLocaleString()}`; } },
            grid: { top: 8, right: 100, bottom: 8, left: 8, containLabel: true },
            xAxis: { type: 'value', show: false },
            yAxis: { type: 'category', data: skillData.map(d => d.skill), axisLabel: { color: navy, fontSize: 11, fontWeight: 600 }, axisLine: { show: false }, axisTick: { show: false } },
            series: [{
                type: 'bar',
                data: skillData.map(d => ({
                    value: Math.round(d.pay),
                    itemStyle: {
                        color: new echarts.graphic.LinearGradient(0,0,1,0,[
                            { offset: 0, color: d.skill.includes('Medic') || d.skill === 'Paramedic' ? teal+'60' : d.skill.includes('Driver') ? red+'60' : d.skill.includes('Officer') || d.skill.includes('Chief') ? orange+'60' : blue+'60' },
                            { offset: 1, color: d.skill.includes('Medic') || d.skill === 'Paramedic' ? teal : d.skill.includes('Driver') ? red : d.skill.includes('Officer') || d.skill.includes('Chief') ? orange : blue }
                        ]),
                        borderRadius: [0,6,6,0]
                    }
                })),
                barWidth: '60%',
                label: { show: true, position: 'right', color: '#5A6A7E', fontSize: 11, formatter: p => '$' + p.value.toLocaleString() },
            }]
        });
        pbiCharts.push(skillChart);

        // OT Monthly Spend Trend
        const otMonths = pbi.manpower_ot.by_month.filter(d => d.month && d.month.length === 7);
        if (otMonths.length > 1) {
            const otMonthChart = echarts.init(document.getElementById('chartOTMonthlySpend'));
            otMonthChart.setOption({
                tooltip: { trigger: 'axis', formatter: p => `<strong>${p[0].axisValue}</strong><br>OT Spend: <strong>$${p[0].value.toLocaleString()}</strong><br>Hours: ${otMonths[p[0].dataIndex].hours.toLocaleString()}` },
                grid: { top: 20, right: 20, bottom: 30, left: 70 },
                xAxis: { type: 'category', data: otMonths.map(d => d.month), axisLabel: { color: '#5A6A7E', fontSize: 10, rotate: 45 } },
                yAxis: { type: 'value', axisLabel: { color: '#5A6A7E', formatter: v => '$' + (v >= 1000000 ? (v/1000000).toFixed(1) + 'M' : Math.round(v/1000) + 'K') }, splitLine: { lineStyle: { color: '#F0F2F5' } } },
                series: [{
                    type: 'bar',
                    data: otMonths.map(d => ({
                        value: Math.round(d.pay),
                        itemStyle: { color: red, borderRadius: [4,4,0,0] }
                    })),
                    barWidth: '60%',
                }, {
                    type: 'line',
                    data: otMonths.map(d => Math.round(d.pay)),
                    smooth: 0.3, symbol: 'circle', symbolSize: 5,
                    lineStyle: { color: navy, width: 2 },
                    itemStyle: { color: navy },
                }]
            });
            pbiCharts.push(otMonthChart);
        }
    }

    // ─── GLOBAL DATE RANGE ───
    // Collect all available months from all sources
    const allGlobalMonths = new Set();
    if (monthlyData && monthlyData.monthly) monthlyData.monthly.forEach(m => allGlobalMonths.add(m.month));
    if (pbi && pbi.manpower_ot && pbi.manpower_ot.by_month) pbi.manpower_ot.by_month.forEach(m => allGlobalMonths.add(m.month));
    const globalMonths = [...allGlobalMonths].sort();

    // Populate dropdowns
    const gStart = document.getElementById('globalStartMonth');
    const gEnd = document.getElementById('globalEndMonth');
    if (gStart && gEnd && globalMonths.length) {
        globalMonths.forEach(m => {
            const label = new Date(m + '-01').toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
            gStart.add(new Option(label, m));
            gEnd.add(new Option(label, m));
        });
        // Default: FY26 = Jul 2025 → latest
        const fy26Default = '2025-07';
        if (globalMonths.includes(fy26Default)) {
            gStart.value = fy26Default;
        } else {
            gStart.value = globalMonths.find(m => m >= fy26Default) || globalMonths[0];
        }
        gEnd.value = globalMonths[globalMonths.length - 1];
        console.log('[OT-Intel] Global months:', globalMonths.length, 'Default start:', gStart.value, 'End:', gEnd.value);
    }

    // Source ranges for "no data" detection
    const tsRange = monthlyData && monthlyData.monthly.length ? { start: monthlyData.monthly[0].month, end: monthlyData.monthly[monthlyData.monthly.length - 1].month } : null;
    const pbiRange = pbi && pbi.manpower_ot && pbi.manpower_ot.by_month.length ? { start: pbi.manpower_ot.by_month[0].month, end: pbi.manpower_ot.by_month[pbi.manpower_ot.by_month.length - 1].month } : null;

    function rangesOverlap(selStart, selEnd, srcRange) {
        if (!srcRange) return false;
        return selStart <= srcRange.end && selEnd >= srcRange.start;
    }

    window.globalRangeChanged = function() {
        const startM = gStart.value;
        const endM = gEnd.value;
        const selectedMonths = globalMonths.filter(m => m >= startM && m <= endM);

        // Update label
        const lbl = document.getElementById('globalRangeLabel');
        if (lbl) lbl.textContent = `${selectedMonths.length} months selected`;

        // ── Key Findings stats (from filtered Telestaff + Power BI monthly) ──
        if (rangesOverlap(startM, endM, tsRange)) {
            const tsFiltered = monthlyData.monthly.filter(m => m.month >= startM && m.month <= endM && m.days >= 15);
            const totalDays = tsFiltered.reduce((s, m) => s + m.days, 0);
            const totalHrs = tsFiltered.reduce((s, m) => s + m.total_hours, 0);
            const dailyAvg = totalDays > 0 ? Math.round(totalHrs / totalDays) : 0;
            const delta = dailyAvg - baselineAvg;
            document.getElementById('statBaseline').textContent = baselineAvg.toLocaleString();
        }

        if (pbi && pbi.manpower_ot && rangesOverlap(startM, endM, pbiRange)) {
            const pbiFiltered = pbi.manpower_ot.by_month.filter(m => m.month >= startM && m.month <= endM);
            const otPay = pbiFiltered.reduce((s, m) => s + m.pay, 0);
            const otHrs = pbiFiltered.reduce((s, m) => s + m.hours, 0);
            const otCount = pbiFiltered.reduce((s, m) => s + m.count, 0);
            const fmt$ = n => n >= 1000000 ? '$' + (n/1000000).toFixed(2) + 'M' : '$' + Math.round(n).toLocaleString();
            document.getElementById('statOTSpend').textContent = fmt$(otPay);
            document.getElementById('statOTHours').textContent = otHrs.toLocaleString();
        } else {
            document.getElementById('statOTSpend').textContent = 'No data';
            document.getElementById('statOTHours').textContent = 'No data';
        }

        // ── Monthly Staffing Trend chart ──
        if (rangesOverlap(startM, endM, tsRange)) {
            const tsFiltered = monthlyData.monthly.filter(m => m.month >= startM && m.month <= endM && m.days >= 15);
            trendChart.setOption({
                xAxis: { data: tsFiltered.map(m => m.month) },
                series: [{
                    data: tsFiltered.map(m => ({
                        value: m.daily_avg,
                        itemStyle: { color: m.daily_avg > baselineAvg * 1.1 ? red : m.daily_avg > baselineAvg * 1.05 ? orange : blue, borderRadius: [4,4,0,0] }
                    }))
                }, {
                    data: tsFiltered.map(() => baselineAvg)
                }]
            });
        }

        // ── OT Monthly Spend chart (Power BI) ──
        const otMonthEl = document.getElementById('chartOTMonthlySpend');
        if (otMonthEl && otMonthEl._ec) {
            if (pbi && rangesOverlap(startM, endM, pbiRange)) {
                const pbiFiltered = pbi.manpower_ot.by_month.filter(m => m.month >= startM && m.month <= endM);
                otMonthEl._ec.setOption({
                    xAxis: { data: pbiFiltered.map(m => m.month) },
                    series: [
                        { data: pbiFiltered.map(m => ({ value: Math.round(m.pay), itemStyle: { color: red, borderRadius: [4,4,0,0] } })) },
                        { data: pbiFiltered.map(m => Math.round(m.pay)) }
                    ]
                });
            }
        }

        // ── Sync Burn-Down date range ──
        const bdStart = document.getElementById('bdStartMonth');
        const bdEnd = document.getElementById('bdEndMonth');
        if (bdStart && bdEnd && window.updateBurndown) {
            // Find closest available month in burn-down dropdowns
            const bdOptions = [...bdStart.options].map(o => o.value);
            const closestStart = bdOptions.find(m => m >= startM) || bdOptions[0];
            const closestEnd = [...bdOptions].reverse().find(m => m <= endM) || bdOptions[bdOptions.length - 1];
            bdStart.value = closestStart;
            bdEnd.value = closestEnd;
            window.updateBurndown();
        }
    };

    window.setGlobalRange = function(preset) {
        const last = globalMonths[globalMonths.length - 1];
        const lastDate = new Date(last + '-01');
        let startYM;
        if (preset === 'fy26') startYM = '2025-07';
        else if (preset === 'fy25') startYM = '2024-07';
        else if (preset === '1y') { const d = new Date(lastDate); d.setFullYear(d.getFullYear() - 1); startYM = d.toISOString().substring(0, 7); }
        else if (preset === '2y') { const d = new Date(lastDate); d.setFullYear(d.getFullYear() - 2); startYM = d.toISOString().substring(0, 7); }
        else startYM = globalMonths[0];

        const closest = globalMonths.find(m => m >= startYM) || globalMonths[0];
        gStart.value = closest;
        if (preset === 'fy25') {
            const fy25End = globalMonths.find(m => m >= '2025-06') || last;
            gEnd.value = fy25End;
        } else {
            gEnd.value = last;
        }
        globalRangeChanged();
    };

    // Store chart refs on elements for global range updates
    const otMonthChartEl = document.getElementById('chartOTMonthlySpend');
    if (otMonthChartEl) {
        // Find the echarts instance
        const ec = echarts.getInstanceByDom(otMonthChartEl);
        if (ec) otMonthChartEl._ec = ec;
    }

    // Initial render with FY26 default
    globalRangeChanged();

    // Resize handler
    [trendChart, treemapChart, otChart, ...pbiCharts, ...bdCharts].forEach(c => window.addEventListener('resize', () => c.resize()));
    window._otCharts = { trendChart, treemapChart, otChart };
});

// Position deep dive
let posDetailChart = null;
async function loadPositionDetail(position) {
    if (!position) return;
    const data = await window.portalAPI('overtime.php?action=position&position=' + encodeURIComponent(position));
    if (!data) return;

    const badge = document.getElementById('positionRankBadge');
    badge.style.display = 'inline-block';
    badge.textContent = `${data.rank} · Baseline: ${data.baseline} hrs · Avg: ${data.avg} hrs · Range: ${data.min}–${data.max}`;

    const el = document.getElementById('chartPositionDetail');
    if (posDetailChart) posDetailChart.dispose();
    posDetailChart = echarts.init(el);

    const dates = data.daily.map(d => d.date).reverse();
    const hours = data.daily.map(d => d.hours).reverse();

    posDetailChart.setOption({
        tooltip: {
            trigger: 'axis',
            formatter: params => {
                const v = params[0].value;
                const delta = v - data.baseline;
                return `<strong>${params[0].axisValue}</strong><br>Hours: <strong>${v}</strong><br>Baseline: ${data.baseline}<br>${delta > 0 ? '<span style="color:#DC2626">+' + delta + ' above</span>' : '<span style="color:#8CC63F">' + delta + ' below</span>'}`;
            }
        },
        grid: { top: 20, right: 20, bottom: 30, left: 50 },
        xAxis: { type: 'category', data: dates, axisLabel: { color: '#5A6A7E', fontSize: 9, rotate: 45, interval: Math.floor(dates.length / 20) } },
        yAxis: { type: 'value', axisLabel: { color: '#5A6A7E' }, splitLine: { lineStyle: { color: '#F0F2F5' } } },
        dataZoom: [{ type: 'inside', start: 70, end: 100 }, { type: 'slider', height: 20, bottom: 5 }],
        series: [{
            type: 'line',
            data: hours.map(v => ({
                value: v,
                itemStyle: { color: v > data.baseline ? '#DC2626' : '#1B6CB0' }
            })),
            smooth: 0.2,
            symbol: 'none',
            lineStyle: { width: 1.5, color: '#1B6CB0' },
            areaStyle: {
                color: new echarts.graphic.LinearGradient(0,0,0,1,[
                    { offset: 0, color: 'rgba(27,108,176,0.15)' },
                    { offset: 1, color: 'transparent' }
                ])
            },
            markLine: {
                silent: true, symbol: 'none',
                lineStyle: { color: '#8CC63F', type: 'dashed', width: 2 },
                label: { position: 'end', formatter: 'Baseline: ' + data.baseline, color: '#8CC63F', fontSize: 10 },
                data: [{ yAxis: data.baseline }]
            }
        }]
    });

    window.addEventListener('resize', () => posDetailChart && posDetailChart.resize());
}
</script>
