<?php
require_once __DIR__ . '/../src/StrategyChangeHistory.php';
require_once __DIR__ . '/../src/StrategyPicker.php';

$userId = $auth->requireAuth();

// ── Validation Workflow, server-side gate, not just a hidden sidebar link ──
// Reports is the established-edge analytics surface; a trader still
// validating a strategy must never reach it, by direct URL or otherwise
// (spec: "Do not merely hide the visual link if the route itself still
// exposes main-workflow reports to validation users. Enforce the workflow
// boundary server-side as well."). Confirmed live before this fix: a
// validating user hitting /reports directly got a full, real render —
// including their own validation trades, since every query in this file
// uses the unconditional main-workflow is_validation filter (correct for
// this page's real purpose, but only once access itself is gated).
// Reports itself is deliberately NOT made workflow-aware, Validation
// Tracker is the intentionally-simpler validation-facing surface; this
// page continues to answer only the main-workflow question, and becomes
// reachable again automatically the moment workflow_state flips to 'main'
// at graduation (ValidationService::graduate()), nothing here needs to
// change for that to happen, since this check re-evaluates on every request.
// 2026-08-17: this exact 4-line block was independently duplicated across
// reports.php/strategy.php/trader-profile.php/journal.php, now the single
// shared ValidationService::gateMainWorkflowOnly() implementation instead.
ValidationService::gateMainWorkflowOnly($userId, $db);

// ── Tier check ───────────────────────────────────────────────────────────────
$tier = 'free';
try {
    $profile = $db->selectById('user_profiles', $userId, 'user_id');
    if ($profile && !empty($profile['tier'])) {
        $tier = $profile['tier'];
    }
} catch (\Throwable $e) {
    error_log("Failed to read user tier: " . $e->getMessage());
}
$isStarterOrAbove = !in_array($tier, ['free']);
$isProOrAbove = in_array($tier, ['pro', 'elite', 'ultimate']);
$isEliteOrUltimate = in_array($tier, ['elite', 'ultimate']);
$displayName = resolveDisplayName($profile ?? null);


// ── Strategy Context (Dashboard is the single source of truth) ──────
// TRIM(name), 2026-07-28 stabilization pass, item 7: some real strategy
// names carry stored trailing whitespace (e.g. "Dealing Range "), which
// rendered as visible double-spacing and dangling punctuation ("Dealing
// Range . Worth watching...") in headers and narrative text throughout
// this page. Display-layer fix only, the stored name is untouched.
// Real bug found and fixed 2026-08-18 (Reports verification pass): this
// query had no is_validation filter at all, so a user's own validation-only
// strategy (is_validation=1, real trades under it use is_validation=1 too)
// could be picked as $allStrategies[0] (most recently created) and become
// the SILENT DEFAULT for the whole page, is_active/deleted_at say nothing
// about workflow, only is_validation does. Confirmed live: a real
// main-workflow account (workflow_state='main') with a leftover 1-trade
// validation strategy ("Asian range liquidity", created after every real
// strategy) had its entire Reports page, hero stats, 4-Stage Summary, every
// section, silently scoped to that one validation trade instead of the
// account's real trading history, with no visible error. Reports itself is
// already gated to main-workflow users only (ValidationService::
// gateMainWorkflowOnly() above), so this is always a hard exclusion here,
// never a workflow_state branch, matching src/StrategyPicker.php's own
// established main-workflow filter.
$allStrategies = $db->raw(
    "SELECT id, TRIM(name) AS name, current_version_id FROM strategies WHERE user_id = :uid AND is_active = 1 AND deleted_at IS NULL AND (is_validation = 0 OR is_validation IS NULL) ORDER BY created_at DESC",
    [':uid' => $userId]
);

// 2026-08-20: carry the active strategy forward automatically, same fix
// as dashboard.php/drafts.php (see src/StrategyPicker.php's
// spRecordActiveStrategy()/spGetActiveStrategyId() docblocks for the full
// account). Reports is gated main-workflow-only above, so the pointer is
// always read/written under workflow_state='main' here.
$strategyId = $_GET['strategy_id'] ?? null;
$selectedStrategy = null;
$selectedStrategyName = null;
$selectedVersionId = null;

if ($strategyId) {
    foreach ($allStrategies as $strat) {
        if ((string)$strat['id'] === (string)$strategyId) {
            $selectedStrategy = $strat['id'];
            $selectedStrategyName = $strat['name'];
            $selectedVersionId = $strat['current_version_id'];
            break;
        }
    }
    if ($selectedStrategy) {
        spRecordActiveStrategy($userId, $selectedStrategy, $db, 'main');
    }
}

if (!$selectedStrategy) {
    $reportsActiveStrategyId = spGetActiveStrategyId($userId, $db, 'main');
    if ($reportsActiveStrategyId) {
        foreach ($allStrategies as $strat) {
            if ((string)$strat['id'] === (string)$reportsActiveStrategyId) {
                $selectedStrategy = $strat['id'];
                $selectedStrategyName = $strat['name'];
                $selectedVersionId = $strat['current_version_id'];
                break;
            }
        }
    }
}

if (!$selectedStrategy && !empty($allStrategies)) {
    $selectedStrategy = $allStrategies[0]['id'];
    $selectedStrategyName = $allStrategies[0]['name'];
    $selectedVersionId = $allStrategies[0]['current_version_id'];
}

// ── 50-trade Strategy Review — deliberately NOT timeframe/account-view
// scoped, computed once here (not inside any $period/$accountView branch
// below) so it renders identically no matter which tab is active. See
// src/StrategyMilestoneReview.php for the full architecture; this is the
// strategy-level "what is happening with the edge I'm actually trading"
// milestone card, a different question from the period-scoped sections
// below it ("how did I perform during this period"). Scoped to ONLY the
// currently-selected/active trading strategy ($selectedStrategy, resolved
// above) per explicit instruction, never one card per strategy the user
// happens to have. Switching the active strategy from Strategy Center
// changes $selectedStrategy on the next render automatically (same
// spGetActiveStrategyId() pointer every other Reports section already
// follows), so this card always tracks whichever strategy is currently
// being traded. Null (no card rendered) if that strategy hasn't reached
// 50 trades yet.
$strategyMilestoneReview = ($selectedStrategy && $selectedVersionId)
    ? StrategyMilestoneReview::build($userId, $db, ['id' => $selectedStrategy, 'name' => $selectedStrategyName, 'current_version_id' => $selectedVersionId], $displayName, false)
    : null;

// Plain strategy name, no "(vN)" (TRADERALIGN STRATEGY BUSINESS RULES
// redesign, 2026-08-11, §13: version numbers are internal bookkeeping,
// never user-facing).
$strategyDisplayName = $selectedStrategyName ?? 'All Strategies';

// Real, recorded change history for the selected strategy (§20/§21/§24) —
// used by the hero meta chip below and by the 4-Stage Summary's "View
// Changes" panel further down this file. Lifetime, not period-scoped —
// "how many times has this strategy evolved" is a fact about the strategy
// itself, not about the currently-viewed reporting window.
$selectedStrategyChangeEvents = $selectedStrategy ? StrategyChangeHistory::fetchEvents($db, $selectedStrategy) : [];
$selectedStrategyChangeCount = count($selectedStrategyChangeEvents);

$strategyVersionData = null;
$declaredSessions = [];
$declaredInstruments = [];
$declaredMaxRiskPct = 1;
$declaredBacktestedWinRate = null;
$declaredBacktestedAvgRr = null;
if ($selectedVersionId) {
    $strategyVersionData = $db->raw(
        "SELECT session_preference, instruments_traded, max_risk_per_trade_pct, backtested_win_rate, backtested_avg_rr FROM strategy_versions WHERE id = :vid",
        [':vid' => $selectedVersionId]
    );
    if (!empty($strategyVersionData)) {
        $declaredSessions = json_decode($strategyVersionData[0]['session_preference'] ?? '[]', true) ?: [];
        $declaredInstruments = json_decode($strategyVersionData[0]['instruments_traded'] ?? '[]', true) ?: [];
        $declaredMaxRiskPct = (float)($strategyVersionData[0]['max_risk_per_trade_pct'] ?? 1);
        $declaredBacktestedWinRate = isset($strategyVersionData[0]['backtested_win_rate']) && $strategyVersionData[0]['backtested_win_rate'] !== null ? (float)$strategyVersionData[0]['backtested_win_rate'] : null;
        $declaredBacktestedAvgRr   = isset($strategyVersionData[0]['backtested_avg_rr']) && $strategyVersionData[0]['backtested_avg_rr'] !== null ? (float)$strategyVersionData[0]['backtested_avg_rr'] : null;
    }
}

// Trading style, tolerant lookup, hidden from the hero meta bar if the
// column doesn't exist on this install yet.
$tradingStyle = null;
if ($selectedStrategy) {
    try {
        $styleRow = $db->raw("SELECT trading_style FROM strategies WHERE id = :sid", [':sid' => $selectedStrategy]);
        $tradingStyle = $styleRow[0]['trading_style'] ?? null;
    } catch (\Throwable $e) { /* column may not exist on this install */ }
}

$baseParams = [':uid' => $userId];
$svidCondition = '';
if ($selectedVersionId) {
    $svidCondition = ' AND strategy_version_id = :svid';
    $baseParams[':svid'] = $selectedVersionId;
}

$period = $_GET['period'] ?? 'monthly';
$allowed = ['weekly', 'monthly', 'quarterly', 'annual'];
if (!in_array($period, $allowed)) $period = 'monthly';

$firstTradeRow = $db->raw("SELECT MIN(closed_at) AS first_closed FROM trades WHERE user_id=:uid AND (is_validation = 0 OR is_validation IS NULL) AND status='closed' AND deleted_at IS NULL" . $svidCondition, $baseParams);
$firstClosedAt = $firstTradeRow[0]['first_closed'] ?? null;
$daysSinceFirstTrade = $firstClosedAt ? (int)(new DateTime())->diff(new DateTime($firstClosedAt))->days : 0;

if ($selectedVersionId) {
    $atRow = $db->raw("SELECT COUNT(*) AS total_closed_trades FROM trades WHERE user_id=:uid AND (is_validation = 0 OR is_validation IS NULL) AND status='closed' AND deleted_at IS NULL" . $svidCondition, $baseParams);
    $totalClosedTrades = (int)($atRow[0]['total_closed_trades'] ?? 0);
} else {
    // mv_account_performance is grouped by (user_id, account_id,
    // phase_record_id), one row per account/phase, not one row per user
    // (see dashboard.php's no-strategy-selected branch for the full
    // writeup). SUM() across all of the user's rows instead of grabbing a
    // single one via LIMIT 1, which silently under-reported for any user
    // with more than one account/phase.
    $atRow = $db->raw("SELECT SUM(total_closed_trades) AS total_closed_trades FROM mv_account_performance WHERE user_id=:uid", [':uid'=>$userId]);
    $totalClosedTrades = (int)($atRow[0]['total_closed_trades'] ?? 0);
}

// Item 4 (2026-07-28 refinement pass) unlocked Annual. Item 3 of the
// 2026-07-28 (second) refinement pass finishes the job: Quarterly had
// quietly picked up its own hard AND-gate (30 days AND 10 closed trades) —
// stricter than Monthly's OR-gate and never explicitly requested, and was
// flagged as a real regression ("Annual correctly lost its lock. Quarterly
// now incorrectly gained one."). Rather than just loosening Quarterly's
// threshold to match Monthly's (which would still leave four periods on
// three different rules), all four periods are now consistently unlocked —
// the same reasoning already applied to Annual: a hard redirect-away lock
// is a gatekeeping mechanism, and this product's own Time Progress / Data
// Completeness indicators (see below) are the correct replacement for "is
// there enough here to look at", they inform, a lock just blocks. A period
// with zero trades yet still renders (empty states throughout already
// handle that), so there's no real content risk to unlocking early access.
$periodUnlocked = [
    'weekly'    => true,
    'monthly'   => true,
    'quarterly' => true,
    'annual'    => true,
];

$periodTooltips = [];

if (!$periodUnlocked[$period]) {
    foreach (['annual', 'quarterly', 'monthly', 'weekly'] as $candidate) {
        if ($periodUnlocked[$candidate]) { $period = $candidate; break; }
    }
}

// Wave 5d (Single Source of Truth migration): $start/$end/$label are now
// sourced from the shared getReportingPeriodBounds() (src/RenderHelpers.php)
// rather than this file's own inline copy, TraderProfileService::
// periodBounds() was an independent, byte-for-byte re-derivation of the
// exact same weekly/monthly/quarterly/annual calendar math, exactly the
// "second caller" this file's own prior comment anticipated. $depth/
// $chartGrouping are Reports-page-specific rendering concerns (not part of
// "what are this period's boundaries"), so they stay computed locally here.
$now = new DateTime();
$bounds = getReportingPeriodBounds($period, $now);
$start = $bounds['start']; $end = $bounds['end']; $label = $bounds['label'];
switch ($period) {
    case 'weekly':    $depth = 1; $chartGrouping = 'daily';     break;
    case 'monthly':   $depth = 2; $chartGrouping = 'weekly';    break;
    case 'quarterly': $depth = 3; $chartGrouping = 'monthly';   break;
    case 'annual':    $depth = 4; $chartGrouping = 'quarterly'; break;
}
$startStr = $bounds['start_str'];
$endStr   = $bounds['end_str'];
// Every analytical module, 4-Stage Engine, Where the Edge Is, Session/
// Instrument Performance, Strategy/Cross-Strategy Analytics, Portfolio
// Analytics, Risk/Behavioral Analytics, must consume $start/$end/$startStr/
// $endStr from here rather than computing its own date logic. This is what
// makes every section automatically respect the selected reporting period,
// and what will let a future prev/next navigation control the whole page
// by changing only these boundaries.

// Prior period of the same length/kind, immediately before $start. This is
// the first place this project needs a real prev-period comparison (the
// 4-Stage Summary's Top Summary Metrics deltas); no shared period-bounds
// function existed anywhere in the codebase despite CLAUDE.md's earlier
// notes describing one (checked via grep, reports_get_current_period_bounds()/
// getReportingPeriodBounds() do not exist in this codebase; that note was
// stale). Built directly alongside the $start/$end switch above rather than
// as a separate shared function, since there is still only this one caller —
// if a second caller ever needs it, extract then.
switch ($period) {
    case 'weekly':
        $prevStart = (clone $start)->modify('-7 days');
        $prevEnd   = (clone $end)->modify('-7 days');
        break;
    case 'monthly':
        $prevStart = (clone $start)->modify('first day of last month')->setTime(0, 0, 0);
        $prevEnd   = (clone $prevStart)->modify('last day of this month')->setTime(23, 59, 59);
        break;
    case 'quarterly':
        $prevStart = (clone $start)->modify('-3 months');
        $prevEnd   = (clone $start)->modify('-1 second');
        break;
    case 'annual':
        $prevStart = (clone $start)->modify('-1 year');
        $prevEnd   = (clone $start)->modify('-1 second');
        break;
}
$prevStartStr = $prevStart->format('Y-m-d H:i:s');
$prevEndStr   = $prevEnd->format('Y-m-d H:i:s');

// $accountView moved up from further down this file (was originally read
// just before the Prop/Live/Comparison tab logic) so the Time Progress
// block below can scope its "when did this trader actually start trading"
// lookup to the active tab. See the original definition site below for
// everything else that depends on it (Prop/Live account lists, boundary
// analysis, etc.), this is purely an early read of the same GET param.
// 2026-07-28 Trader Profile session: 'comparison' removed from the allow-list
// (superseded by the new, fully separate Trader Profile module, see
// public/trader-profile.php). Any '?account_view=comparison' in an old
// bookmark/link now normalizes to 'prop' here, same as any other invalid
// value, so it's unreachable through the UI or a direct URL. The many
// "$accountView !== 'comparison'" guards further down this file are now
// permanently true and were deliberately left in place rather than
// surgically unwrapped one-by-one across a 5,000+ line file, same
// "leave orphaned-but-harmless code in place" precedent already used
// elsewhere in this file (see CLAUDE.md). $comparisonProp/$comparisonLive
// below are now dead (never computed) for the same reason.
$accountView = $_GET['account_view'] ?? 'prop';
if (!in_array($accountView, ['prop', 'live'], true)) $accountView = 'prop';

// ── Time Progress, how far through the trader's OWN effective reporting
// window today falls (2026-07-27 finishing pass, item 6; renamed and
// corrected 2026-07-28 second refinement pass, item 2; architecture
// corrected again 2026-07-28 final refinement pass, item 2, see CLAUDE.md's
// "Effective Reporting-Period Start" architectural note for the full
// rationale).
//
// This was originally pure calendar math (elapsed seconds / total seconds
// in the CALENDAR period), which produced a nonsensical reading for any
// trader who opened their account or started this account-type mid-period:
// e.g. a trader whose first trade was July 1 would see Annual Time Progress
// already at ~50% on July 1 itself, as if half a year of inactivity before
// they even existed counted as "elapsed" against them.
//
// Fixed: the effective period start is max(calendar period start, this
// trader's first-ever trade in the active account view). If their first
// trade already predates the calendar period start, nothing changes
// (effective start = calendar start, identical to the old behavior). If
// their first trade falls inside the period, progress is measured from
// that trade forward, the trader's OWN reporting window, not the calendar
// before they started. If they have no trades at all yet in this account
// view, progress is naturally 0% (no window has opened yet to measure
// progress through).
//
// Scoped to the active account_view (prop vs live) rather than globally,
// since a trader's prop-firm and personal-account trading histories can
// start on genuinely different dates and this bar always describes the
// currently-active tab. Comparison has no single account type to scope
// against, so it uses the trader's overall first trade across both types.
$avTypeCondForFirstTrade = match ($accountView) {
    'prop' => "AND ta.account_type = 'prop_firm'",
    'live' => "AND ta.account_type = 'personal'",
    default => '',
};
$firstTradeRow = $db->raw(
    "SELECT MIN(t.captured_at) AS first_trade_at
     FROM trades t LEFT JOIN trading_accounts ta ON ta.id = t.account_id
     WHERE t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL)
       AND t.deleted_at IS NULL AND t.captured_at <= :end_str
       {$avTypeCondForFirstTrade}",
    [':uid' => $userId, ':end_str' => $endStr]
);
$firstTradeAt = $firstTradeRow[0]['first_trade_at'] ?? null;

if ($firstTradeAt === null) {
    // No trades at all yet in this account view (as of this period), no
    // reporting window has opened, so there is nothing to be "through".
    $effectivePeriodStart = null;
    $periodTimeProgressPct = 0;
} else {
    $firstTradeDt = new DateTime($firstTradeAt);
    $effectivePeriodStart = $firstTradeDt > $start ? $firstTradeDt : $start;
    $periodTotalSeconds = max(1, $end->getTimestamp() - $effectivePeriodStart->getTimestamp());
    $periodElapsedSeconds = min($periodTotalSeconds, max(0, $now->getTimestamp() - $effectivePeriodStart->getTimestamp()));
    $periodTimeProgressPct = max(0, min(100, (int)round(($periodElapsedSeconds / $periodTotalSeconds) * 100)));
}
// True only when the trader's own trading window started after the
// calendar period began, used to caption the bar honestly ("since your
// first trade this {period}, not since {period} start") rather than
// silently changing what "Time Progress" means without telling the trader.
$periodStartedMidway = $effectivePeriodStart !== null && $effectivePeriodStart > $start;

// Item 4 (2026-07-28 refinement pass): below this threshold, Annual shows a
// dismissible, non-blocking informational banner rather than the old hard
// lock. Threshold picked at 25% (~91 days / roughly one calendar quarter
// into the year) rather than accepted as a given, reasoning: it's the
// point at which at least one full quarter's worth of seasonal/behavioral
// variation has had a chance to appear, and it lines up with this app's own
// "Quarterly" tab becoming a same-length comparison point. This is a
// judgment call, not independently confirmed with Kevin, worth a quick
// sanity check before treating as final if he wants a different cutoff.
$annualEarlyStageThresholdPct = 25;
$showAnnualEarlyStageBanner = ($period === 'annual' && $periodTimeProgressPct < $annualEarlyStageThresholdPct);

// ════════════════════════════════════════════════════════════════════
// Account Analytics (Prop Firms / Live Accounts / Comparison)
// ─────────────────────────────────────────────────────────────────────
// Independent of the strategy-period reporting above. Feeds the new
// Account Overview / Challenge Progress / Lifecycle Journey / Boundary
// Analytics cards further down the page. Read-only, no writes here.
// ════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/../api/analytics/engine.php';
require_once __DIR__ . '/../api/analytics/progressive_insights_engine.php';
require_once __DIR__ . '/../src/TradeGrading.php';
$analyticsEngine = new AnalyticsEngine($pdo);

// Small tolerant field lookup, engine.php's exact array keys may differ
// slightly from what's assumed below. If a card shows "—" or 0 where you
// expect real data, add the correct key to the relevant arrVal() call.
function arrVal($arr, $keys, $default = null) {
    if (!is_array($arr)) return $default;
    foreach ((array)$keys as $k) {
        if (array_key_exists($k, $arr) && $arr[$k] !== null) return $arr[$k];
    }
    return $default;
}

// Safely turn an arrVal() result (or any value) into a display string.
// Some engine.php fields (e.g. "phase") can come back as a nested array
// instead of a scalar; casting an array with (string) triggers "Array to
// string conversion" and prints the literal word "Array". This unwraps
// common sub-keys first and falls back gracefully instead.
function toDisplayString($val, $fallback = '-') {
    if ($val === null || $val === '') return $fallback;
    if (is_array($val)) {
        foreach (['name', 'label', 'phase', 'current_stage', 'status', 'value'] as $k) {
            if (isset($val[$k]) && !is_array($val[$k])) return (string)$val[$k];
        }
        return $fallback;
    }
    return (string)$val;
}

// Render an associative array as label/value rows. Used for stage payloads
// (e.g. Four-Stage Trade Comparison) whose exact keys depend on what the
// analytics engine returns, tolerant of missing/nested/unexpected fields
// rather than erroring on them.
//
// Kept as a separate implementation from journal.php's renderKeyValueRows()
// (different HTML/CSS, this one is inline-styled to work inside this page's
// narrow grid columns, journal's uses the .field-grid design system shared
// with its Trade Details view) but the two MUST stay behaviorally in sync on
// which keys get special-cased, since both render the exact same Stage 1-4
// payloads from AnalyticsEngine, a real bug (found in Round 3 audit #10)
// was this version silently rendering 'note'/'consistency_note' as plain
// rows instead of the distinguished treatment journal.php gives them, so
// identical trade data read differently depending on which page showed it.
function renderKeyValueRows($data, array $skipKeys = []) {
    if (!is_array($data) || empty($data)) return '';
    if (isset($data['note'])) return '<p style="font-size:.68rem;color:var(--foreground-dim);font-style:italic">' . htmlspecialchars($data['note']) . '</p>';
    $html = '';
    foreach ($data as $k => $v) {
        if (in_array($k, $skipKeys, true) || $k === 'consistency_note') continue;
        if (is_array($v)) {
            if (empty($v)) continue;
            $v = json_encode($v);
        }
        if ($v === null || $v === '') continue;
        $label = htmlspecialchars(ucwords(str_replace('_', ' ', (string)$k)));
        $val   = htmlspecialchars((string)$v);
        $html .= '<div style="display:flex;justify-content:space-between;gap:.75rem;font-size:.75rem;padding:.25rem 0;border-bottom:1px solid var(--border)"><span style="color:var(--foreground-dim)">' . $label . '</span><span style="font-family:var(--font-sans);color:var(--foreground);text-align:right">' . $val . '</span></div>';
    }
    if (!empty($data['consistency_note'])) {
        $html .= '<div class="insight-text" style="margin-top:.5rem">' . htmlspecialchars($data['consistency_note']) . '</div>';
    }
    return $html;
}

function pickSelectedAccount($accounts, $requestedId) {
    if ($requestedId) {
        foreach ($accounts as $a) { if ((string)$a['id'] === (string)$requestedId) return $a; }
    }
    return $accounts[0] ?? null;
}

// Split into an all-time call and a period-scoped call, merged together.
// getAccountMetrics()'s $filters param already supports dateFrom/dateTo
// (buildFilterClause() filters trades on captured_at), no engine change
// needed, and getChallengeMetrics()/getLifecycleJourney() below still call
// getAccountMetrics($accountId) with no filters at all, so their behavior
// is untouched.
//
// Account-level facts (current_balance, starting_balance) plus the figures
// that are intrinsically anchored to the account's real historical
// equity-curve peak (equity, peak_equity, current_drawdown_pct,
// max_drawdown_pct, "how far below my all-time peak am I right now")
// are taken from the ALL-TIME call. Scoping these to one period would
// produce incoherent output, e.g. "Peak Equity" resetting every period
// instead of reflecting the account's actual historical peak, or "Current
// Drawdown" measured against a period-local peak instead of the real one.
//
// Everything else, Win Rate, Total Trades, Net P&L, Return %, Profit
// Factor, Expectancy, Streaks, etc., is a trade-performance metric with
// no such anchoring problem ("this month's win rate" is a perfectly
// coherent idea) and is taken from the PERIOD-scoped call, so it actually
// changes with the Weekly/Monthly/Quarterly/Annual toggle like the rest of
// the page's trade-derived sections (Trade Summary / Classification /
// 4-Stage).
function getAccountMetricsForPeriod($engine, $accountId, $startStr, $endStr) {
    $allTime = $engine->getAccountMetrics($accountId);
    if (isset($allTime['error'])) return $allTime;

    $period = $engine->getAccountMetrics($accountId, ['dateFrom' => $startStr, 'dateTo' => $endStr]);
    if (isset($period['error'])) return $allTime;

    $merged = $period;
    foreach (['current_balance', 'starting_balance', 'equity', 'peak_equity', 'current_drawdown_pct', 'max_drawdown_pct'] as $anchoredKey) {
        $merged[$anchoredKey] = $allTime[$anchoredKey];
    }
    // current_drawdown_pct/max_drawdown_pct above are all-time-anchored, so
    // praise calibrated to "how many trades support a zero-drawdown claim"
    // needs the ALL-TIME trade count too, not the period-scoped total_trades
    // this merged array otherwise carries, one disciplined trade this week
    // on a 300-trade account deserves very different praise than one
    // disciplined trade on a brand-new account.
    $merged['all_time_total_trades'] = (int)($allTime['total_trades'] ?? $allTime['trades'] ?? 0);
    return $merged;
}

// Zero drawdown is only a "message" when it's actually zero, otherwise the
// real negative number renders as before. The register of praise scales with
// how many all-time trades back up the claim: "no drawdown yet" on a single
// trade is a different (and much smaller) statement than zero drawdown held
// across a hundred-plus trades.
function drawdownPraise(float $ddAbsPct, int $allTimeTrades): ?string {
    if ($ddAbsPct > 0.005) return null;
    if ($allTimeTrades <= 0) return null;
    if ($allTimeTrades < 5)   return 'No drawdown yet, clean start.';
    if ($allTimeTrades < 30)  return "No drawdown across {$allTimeTrades} trades so far, good early discipline.";
    if ($allTimeTrades < 100) return "Zero drawdown held across {$allTimeTrades} trades, strong, consistent risk control.";
    return "Zero drawdown sustained across {$allTimeTrades} trades, exceptional, career-level risk discipline.";
}

// $accountView is now read early, see the Time Progress block above, which
// needs it before this point in the file.

// has_breached_at / has_passed_at, TRUE if ANY of this account's challenge_accounts
// attempts (there can be more than one per trading_accounts row) has that
// timestamp set, matching the exact OR-across-attempts semantics
// $propFirmAgg's own canonical SQL uses (LEFT JOIN + COUNT DISTINCT ta.id).
// Added so the Boundary Analysis block below can determine breach/pass
// status using the SAME definition as Portfolio Summary, guaranteeing
// identical counts by construction rather than by two independently-
// written queries happening to agree (see the 2026-07-27 Boundary
// Analysis parity fix note further down this file).
$propAccounts = $db->raw(
    "SELECT ta.id, ta.label, ta.account_type, ta.starting_balance, ta.current_balance, ta.lifecycle_state, ta.peak_equity, ta.created_at, ta.current_outcome,
        (SELECT ca2.challenge_type FROM challenge_accounts ca2 WHERE ca2.account_id = ta.id ORDER BY ca2.attempt_number DESC, ca2.created_at DESC LIMIT 1) AS challenge_type,
        (SELECT MAX(CASE WHEN ca3.breached_at IS NOT NULL THEN 1 ELSE 0 END) FROM challenge_accounts ca3 WHERE ca3.account_id = ta.id) AS has_breached_at,
        (SELECT MAX(CASE WHEN ca4.passed_at IS NOT NULL THEN 1 ELSE 0 END) FROM challenge_accounts ca4 WHERE ca4.account_id = ta.id) AS has_passed_at
     FROM trading_accounts ta
     WHERE ta.user_id = :uid AND ta.account_type = 'prop_firm' AND ta.deleted_at IS NULL
     ORDER BY ta.created_at DESC",
    [':uid' => $userId]
);
// closed_trade_count/alltime_pnl, added 2026-07-27 for the leaderboard
// refinement pass: "meaningful trading activity" (closed_trade_count > 0)
// is the shared signal every leaderboard section (Top Performing, Needs
// Attention, Longest-Surviving, the new Closed Account Retrospective card)
// now uses to decide whether a dormant/never-traded account belongs in
// any of them. alltime_pnl is the account's real, all-time trading result
//, used only by the Closed Account Retrospective card below (a closed
// account's own history is a fixed historical fact, not something that
// should re-scope to whichever reporting period tab happens to be open).
$liveAccounts = $db->raw(
    "SELECT ta.id, ta.label, ta.account_type, ta.starting_balance, ta.current_balance, ta.lifecycle_state, ta.peak_equity, ta.created_at, ta.closed_at, ta.net_withdrawals, ta.current_outcome, ta.boundary_event,
        (SELECT COUNT(*) FROM trades t WHERE t.account_id = ta.id AND t.status = 'closed' AND t.deleted_at IS NULL AND (t.is_validation = 0 OR t.is_validation IS NULL)) AS closed_trade_count,
        (SELECT COALESCE(SUM(COALESCE(t2.broker_pnl, t2.projected_pnl, 0)), 0) FROM trades t2 WHERE t2.account_id = ta.id AND t2.status = 'closed' AND t2.deleted_at IS NULL AND (t2.is_validation = 0 OR t2.is_validation IS NULL)) AS alltime_pnl
     FROM trading_accounts ta WHERE ta.user_id = :uid AND ta.account_type = 'personal' AND ta.deleted_at IS NULL
     ORDER BY ta.created_at DESC",
    [':uid' => $userId]
);

// Account-scope selector (4-Stage Summary), resolved this early so the
// SAME value can scope both the early $trueFourStageSummary computation
// below (which also backs the Commentary/Recommendations built before the
// section renders) and the section's own render-time queries further down.
// Validated against this user's own accounts of the active account_view
// type; an id that doesn't match (foreign, stale, or comparison view, which
// has no selector) falls back to '' (the aggregate "All" scope) rather than
// silently filtering by an unvalidated id.
$scopedAccountId = $_GET['account_id'] ?? '';
if ($scopedAccountId !== '') {
    $scopedAccountsList = $accountView === 'prop' ? $propAccounts : ($accountView === 'live' ? $liveAccounts : []);
    $scopedMatch = false;
    foreach ($scopedAccountsList as $__sa) { if ((string)$__sa['id'] === (string)$scopedAccountId) { $scopedMatch = true; break; } }
    if (!$scopedMatch) $scopedAccountId = '';
}

// account_id -> label lookup, used anywhere a highlighted trade needs to
// name which account it belongs to (a trader with many accounts has no way
// to know which one "a disciplined win" refers to from the instrument alone).
$accountLabelsById = [];
foreach (array_merge($propAccounts, $liveAccounts) as $__acct) {
    $accountLabelsById[(string)$__acct['id']] = $__acct['label'];
}
function accountLabelFor($accountLabelsById, $accountId) {
    if (!$accountId) return 'Unknown account';
    return $accountLabelsById[(string)$accountId] ?? 'Unknown account';
}

$accountIdParam       = $_GET['account_id'] ?? null;
$selectedPropAccount  = pickSelectedAccount($propAccounts, $accountIdParam);
$selectedLiveAccount  = pickSelectedAccount($liveAccounts, $accountIdParam);

$activeAccount = null;
if ($accountView === 'prop') $activeAccount = $selectedPropAccount;
elseif ($accountView === 'live') $activeAccount = $selectedLiveAccount;

$accountMetrics   = null;
$challengeMetrics = null;
$lifecycleJourney = null;

if ($activeAccount) {
    try { $accountMetrics = getAccountMetricsForPeriod($analyticsEngine, $activeAccount['id'], $startStr, $endStr); }
    catch (\Throwable $e) { error_log('getAccountMetrics failed: ' . $e->getMessage()); }

    try { $lifecycleJourney = $analyticsEngine->getLifecycleJourney($activeAccount['id']); }
    catch (\Throwable $e) { error_log('getLifecycleJourney failed: ' . $e->getMessage()); }

    if ($accountView === 'prop') {
        try { $challengeMetrics = $analyticsEngine->getChallengeMetrics($activeAccount['id']); }
        catch (\Throwable $e) { error_log('getChallengeMetrics failed: ' . $e->getMessage()); }
    }
}

// Comparison tab, one representative account of each type, side by side
$comparisonProp = $comparisonLive = null;
if ($accountView === 'comparison') {
    if ($selectedPropAccount) {
        try { $comparisonProp = getAccountMetricsForPeriod($analyticsEngine, $selectedPropAccount['id'], $startStr, $endStr); }
        catch (\Throwable $e) { error_log('getAccountMetrics (comparison/prop) failed: ' . $e->getMessage()); }
    }
    if ($selectedLiveAccount) {
        try { $comparisonLive = getAccountMetricsForPeriod($analyticsEngine, $selectedLiveAccount['id'], $startStr, $endStr); }
        catch (\Throwable $e) { error_log('getAccountMetrics (comparison/live) failed: ' . $e->getMessage()); }
    }
}

// Boundary Analytics, aggregated across every prop-firm account this user
// has ever had, via getChallengeMetrics() per account rather than raw SQL,
// so it stays consistent with whatever the engine considers "breach" or
// "distance to pass".
//
// FIXED (this pass), this block previously never populated $breachPayloads/
// $passExcesses/$nearMissCount for ANY account, on any real data, due to
// three compounding bugs (confirmed via CLAUDE.md's own long-standing flag
// and verified directly against the live DB before fixing):
//   1. Compared $pa['lifecycle_state'] against 'blown'/'completed', neither
//      value has ever existed in this schema. Real values are 'breached'
//      (not 'blown') and '<stage>_complete' e.g. 'phase_1_complete' (not a
//      bare 'completed'), see this file's Lifecycle Rule.
//   2. Read $excessLoss via arrVal($cm, ['excess_loss', 'excess_pnl']) —
//      arrVal() only checks TOP-LEVEL keys, but getChallengeMetrics() only
//      ever nests this under excess_amounts.excess_loss_after_breach /
//      excess_amounts.excess_profit_after_pass. Neither flat key has ever
//      existed, so this was always null regardless of bug #1.
//   3. Read $ddRemainingPct via arrVal($cm, ['overall_dd_remaining_pct',
//      'daily_dd_remaining_pct']), same class of bug; getChallengeMetrics()
//      has no such flat keys. distance_to_breach.is_near_breach (a real,
//      already-computed boolean) is the correct equivalent.
// Verified live before/after against this user's real prop accounts: old
// code produced total_challenges > 0 but breach_count/pass_count/
// near_miss_count = 0 for every account view; fixed code correctly counts
// real 'breached' and '*_complete' accounts and their real excess amounts.
// ────────────────────────────────────────────────────────────────────
// PARITY FIX (2026-07-27): pass_count/breach_count previously came from
// counting getChallengeMetrics()'s CURRENT lifecycle_state snapshot
// (== 'breached' / contains '_complete'), which under-counted "Passed"
// against Portfolio Summary's $propFirmAgg['passed_accounts'], e.g. 5
// vs 16 on a real account. Root cause: passing a phase is NOT a
// terminal state (unlike breaching), an account that passed phase 1
// and was then promoted to phase_2_active/funded_active/even later
// breached no longer has a lifecycle_state containing '_complete', even
// though challenge_accounts.passed_at (the durable historical fact) is
// still set. Confirmed live: of one real user's 16 passed_at accounts,
// only 3 still showed a '_complete' lifecycle_state, 4 had moved to
// phase_2_active, 7 to funded_active, 2 had since breached.
//
// Fixed by determining breach/pass status from $propAccounts'
// has_breached_at/has_passed_at/lifecycle_state fields, the EXACT same
// columns and OR-logic $propFirmAgg's own canonical SQL uses (see that
// query's comment), rather than re-deriving it from a getChallengeMetrics()
// snapshot. This guarantees identical counts to Portfolio Summary by
// construction. getChallengeMetrics() is still called per account, but
// only to source the richer per-account figures below (excess amounts,
// near-miss distance), never to decide the headline counts.
// ────────────────────────────────────────────────────────────────────
// Performance fix (2026-08-18): this whole block loops getChallengeMetrics()
// (6+ queries each) over every prop-firm account the user has ever had —
// confirmed live to cost ~650-700ms on a 79-account real account. It was
// previously computed unconditionally on EVERY Reports page load, including
// Live Accounts view, even though $boundaryAnalytics is only ever read
// inside the Prop Firm Analytics accordion (accountView === 'prop') further
// down this file, confirmed via a full grep of every $boundaryAnalytics
// read site before adding this gate. Live-view loads paid this full cost
// for a value that was never once rendered. Gating this to the account view
// that actually uses it removes that entire wasted computation from every
// Live Accounts page load, with zero change to what Prop Firms view shows.
$boundaryAnalytics = null;
if ($accountView === 'prop' && !empty($propAccounts)) {
    $passExcesses = []; $breachPayloads = []; $breachSeverityPcts = []; $passExcessPcts = [];
    $nearMissCount = 0; $nearMissDistancePcts = []; $totalChallenges = 0;
    $passCount = 0; $breachCount = 0; $passStillFreshCount = 0;
    $breachReasonTally = [];

    foreach ($propAccounts as $pa) {
        $totalChallenges++;
        $isBreached = ($pa['lifecycle_state'] === 'breached') || !empty($pa['has_breached_at']);
        $isPassed   = !empty($pa['has_passed_at']);
        if ($isBreached) $breachCount++;
        if ($isPassed) $passCount++;

        try { $cm = $analyticsEngine->getChallengeMetrics($pa['id']); }
        catch (\Throwable $e) { continue; }
        if (!$cm || isset($cm['error'])) continue;

        $excessLossAfterBreach  = $cm['excess_amounts']['excess_loss_after_breach']  ?? null;
        $excessProfitAfterPass  = $cm['excess_amounts']['excess_profit_after_pass']  ?? null;
        $isNearBreach = !empty($cm['distance_to_breach']['is_near_breach']);
        $overallLimitAmount = (float)($cm['overall_drawdown']['limit_amount'] ?? 0);
        $targetProfitAmount = (float)($cm['distance_to_pass']['target_profit_amount'] ?? 0);

        // Breach is terminal in this schema (confirmed live: every account
        // with has_breached_at also currently shows lifecycle_state =
        // 'breached', 0 exceptions across a real 24-account sample), so
        // getChallengeMetrics()'s CURRENT excess_loss_after_breach is
        // reliable for the full breach set, not just a snapshot subset.
        if ($isBreached && $excessLossAfterBreach !== null) {
            $breachPayloads[] = (float)$excessLossAfterBreach;
            if ($overallLimitAmount > 0) $breachSeverityPcts[] = ($excessLossAfterBreach / $overallLimitAmount) * 100;
            // Breach reason, trading_accounts.current_outcome is free-text
            // in practice (see CLAUDE.md's Lifecycle Rule); bucket
            // recognized ALL-CAPS/underscore codes as their own reason,
            // free-text as "Trader-described", and NULL as "Not recorded"
            // (legacy accounts breached before resolve_trade_id started
            // writing a reason).
            $reasonRaw = $pa['current_outcome'] ?? null;
            if ($reasonRaw === null || trim($reasonRaw) === '') {
                $reasonKey = 'Not recorded';
            } elseif (preg_match('/^[A-Z0-9_]+$/', trim($reasonRaw))) {
                $reasonKey = ucwords(strtolower(str_replace('_', ' ', trim($reasonRaw))));
            } else {
                $reasonKey = 'Trader-described (other)';
            }
            $breachReasonTally[$reasonKey] = ($breachReasonTally[$reasonKey] ?? 0) + 1;
        }
        // Pass is NOT terminal, only include the dollar excess figure for
        // accounts whose CURRENT snapshot still reflects the pass moment
        // (lifecycle_state still contains '_complete'); for the others,
        // the current target/profit no longer refers to the phase that was
        // actually passed (they've since progressed), so their "current"
        // excess would measure the wrong thing. pass_count itself (above)
        // is NOT limited this way, only this dollar average is.
        if ($isPassed && str_contains((string)($pa['lifecycle_state'] ?? ''), '_complete') && $excessProfitAfterPass !== null) {
            $passExcesses[] = (float)$excessProfitAfterPass;
            $passStillFreshCount++;
            if ($targetProfitAmount > 0) $passExcessPcts[] = ($excessProfitAfterPass / $targetProfitAmount) * 100;
        }
        if (!$isBreached && $isNearBreach) {
            $nearMissCount++;
            $distToBreachPct = $cm['distance_to_breach']['percentage'] ?? null;
            if ($distToBreachPct !== null) $nearMissDistancePcts[] = abs((float)$distToBreachPct);
        }
    }

    arsort($breachReasonTally);

    $boundaryAnalytics = [
        'total_challenges'          => $totalChallenges,
        'avg_excess_at_pass'        => count($passExcesses)   ? round(array_sum($passExcesses)   / count($passExcesses), 2)   : null,
        'avg_breach_payload'        => count($breachPayloads) ? round(array_sum($breachPayloads) / count($breachPayloads), 2) : null,
        // Severity/typical-excess framing, normalized to % of the account's
        // own limit/target so accounts of different sizes are comparable —
        // "breaches overshoot the limit by 12% of the allowed drawdown
        // budget, on average" is a meaningful trader insight; a raw dollar
        // figure across mixed account sizes is not.
        'avg_breach_severity_pct'   => count($breachSeverityPcts) ? round(array_sum($breachSeverityPcts) / count($breachSeverityPcts), 1) : null,
        'avg_pass_excess_pct'       => count($passExcessPcts) ? round(array_sum($passExcessPcts) / count($passExcessPcts), 1) : null,
        'near_miss_count'           => $nearMissCount,
        'avg_near_miss_distance_pct'=> count($nearMissDistancePcts) ? round(array_sum($nearMissDistancePcts) / count($nearMissDistancePcts), 1) : null,
        'pass_count'                => $passCount,
        'breach_count'              => $breachCount,
        // How many of the pass_count accounts the dollar/pct excess-at-pass
        // figures above are actually based on (the rest have since
        // progressed to a new phase or funded status, so no reliable
        // pass-moment snapshot remains), surfaced so the UI can caption
        // this honestly instead of implying the average covers every pass.
        'pass_excess_sample_count'  => $passStillFreshCount,
        'breach_reason_tally'       => $breachReasonTally,
    ];
}
$startDateOnly = $start->format('Y-m-d');
$endDateOnly   = $end->format('Y-m-d');
$periodParams  = array_merge($baseParams, [':s' => $startStr, ':e' => $endStr]);

// ════════════════════════════════════════════════════════════════════
// Shared P&L expression, SINGLE SOURCE OF TRUTH for every aggregate on
// this page. Prop-firm accounts must be measured against challenge_pnl
// (the firm's internal challenge accounting), not broker_pnl (the raw
// fill P&L), which can legitimately differ. Personal/live accounts keep
// using broker_pnl. Every query below joins trading_accounts (aliased
// ta) specifically so this expression can tell the two apart; trades is
// aliased t throughout to avoid ambiguity with the columns trades and
// trading_accounts both have (id, user_id, closed_at, deleted_at).
//
// Fixed: the inner CASE used to go straight to challenge_pnl (or NULL) for
// prop-firm trades, with only the outer COALESCE's single projected_pnl
// step as a fallback, so a prop-firm trade with challenge_pnl NULL but a
// real broker_pnl (never backfilled, see CLAUDE.md) silently fell through
// to projected_pnl instead of its real broker_pnl. Now matches the two
// places that already had this right: the mv_* views' COALESCE(
// challenge_pnl, broker_pnl, projected_pnl, 0) and dashboard.php's
// dashEffectivePnl() PHP helper, both keep the account_type branch
// rather than letting challenge_pnl cascade into the personal-account
// path (it should never apply there).
// ════════════════════════════════════════════════════════════════════
// ════════════════════════════════════════════════════════════════════
// Risk breach tolerance, a trade's stored risk_pct can legitimately land
// marginally above a strategy's declared max_risk_per_trade_pct because
// the calculator rounds lot size UP to the broker's minimum tradeable
// step (0.01 lots in this data) rather than trading the exact fractional
// lot size the declared risk % implies. Without a tolerance, every trade
// affected by that rounding reads as a "Risk Breach" (Warnings badge,
// Strategy Comparison, Instrument Performance's over-risk count) even
// though the trader did nothing wrong.
//
// 0.10 percentage points, chosen from the real distribution of these
// deviations across the DB (63 trades with risk_pct marginally above
// their strategy's declared max): deviations from genuine single-lot-step
// rounding topped out at 0.08pp (e.g. a 0.01-lot position where the
// calculator wanted 0.0093 lots), while every deviation caused by the
// platform's 0.01 minimum lot floor forcing a MEANINGFULLY oversized
// position (e.g. wanting 0.0067 lots, forced to 0.01, a real 49% larger
// position) started at 0.12pp and ran as high as 0.49pp. There is a clean
// empirical gap between 0.08 and 0.12 with zero trades in it, 0.10 sits
// in that gap, so it absorbs every confirmed rounding artifact without
// also silently absorbing the genuinely oversized positions that should
// still surface as a real breach.
// ════════════════════════════════════════════════════════════════════
const RISK_BREACH_TOLERANCE_PCT = 0.10;

$pnlExpr = "CASE WHEN ta.account_type = 'prop_firm' THEN COALESCE(t.challenge_pnl, t.broker_pnl, t.projected_pnl, 0) ELSE COALESCE(t.broker_pnl, t.projected_pnl, 0) END";
$tradesAccountsJoin = "trades t LEFT JOIN trading_accounts ta ON ta.id = t.account_id";
// Same strategy-version filter as $svidCondition, but qualified with the
// t. alias for queries that now join trading_accounts.
$svidConditionT = $svidCondition !== '' ? ' AND t.strategy_version_id = :svid' : '';

// ════════════════════════════════════════════════════════════════════
// Account-view type filter, SINGLE SOURCE OF TRUTH for scoping every
// trade-derived stat on this page to the active Prop Firms / Live
// Accounts / Comparison tab. Fixes a real data-leak: before this, the
// Trade Set hero, Strategy Performance table, Instrument P&L chart,
// Session/Instrument/Classification breakdowns, expectancy/profit
// factor/drawdown/Sharpe-Sortino, and the 4-Stage Performance Audit's
// gating ($totalTrades) all queried the user's trades platform-wide —
// with zero regard for which account-view tab was selected. Switching
// to "Live Accounts" with zero personal-account trades still showed
// whatever the user's prop-firm trades summed to, instead of the
// correct empty state. $avTypeFilter is for queries already joined to
// trading_accounts as `ta` (via $tradesAccountsJoin or an explicit
// join); $avAccountIdFilter is for queries against a bare `trades`
// table with no such join. 'comparison' intentionally resolves to ''
// (no filter), its content is a placeholder pending redesign, so nothing
// currently renders that would need this scoping.
// ════════════════════════════════════════════════════════════════════
$avTypeFilter = match ($accountView) {
    'prop' => "AND ta.account_type = 'prop_firm'",
    'live' => "AND ta.account_type = 'personal'",
    default => '',
};
$avAccountIdFilter = match ($accountView) {
    'prop' => "AND account_id IN (SELECT id FROM trading_accounts WHERE account_type = 'prop_firm')",
    'live' => "AND account_id IN (SELECT id FROM trading_accounts WHERE account_type = 'personal')",
    default => '',
};
// Every $svidConditionT usage below is inside a $tradesAccountsJoin (or
// equivalently-aliased ta/t) query, so folding $avTypeFilter in here
// scopes all of them (Trade Set stats, Session/Instrument Performance,
// Strategy Performance table + chart, expectancy, drawdown, Sharpe/
// Sortino, all-time P&L) in one place instead of at each call site.
$svidConditionT .= $avTypeFilter !== '' ? ' ' . $avTypeFilter : '';

// Best Trade only ever comes from a winning trade; Worst Trade only ever
// comes from a losing trade, a win can never populate Worst Trade and a
// loss can never populate Best Trade. Previously this was an unconditional
// MAX/MIN over every trade's P&L regardless of sign, so a single winning
// trade (or any account with zero losses) showed its one win as BOTH the
// best AND the worst trade. NULL (not 0) when no trade in that category
// exists yet, 0 would be indistinguishable from a real breakeven/near-zero
// trade, so it's handled as its own "no wins yet"/"no losses yet" state.
$s = $db->raw(
    "SELECT COUNT(*) AS total_trades,
        SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins,
        SUM(CASE WHEN $pnlExpr < 0 THEN 1 ELSE 0 END) AS losses,
        SUM(CASE WHEN $pnlExpr = 0 THEN 1 ELSE 0 END) AS breakevens,
        COALESCE(SUM($pnlExpr), 0) AS total_pnl,
        AVG(t.risk_pct) AS avg_risk,
        MAX(CASE WHEN $pnlExpr > 0 THEN $pnlExpr END) AS best_trade,
        MIN(CASE WHEN $pnlExpr < 0 THEN $pnlExpr END) AS worst_trade
     FROM $tradesAccountsJoin
     WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL
     AND t.closed_at>=:s AND t.closed_at<=:e" . $svidConditionT,
    $periodParams
);
$st = $s[0] ?? [];
$totalTrades     = (int)($st['total_trades'] ?? 0);

// ── Data Completeness, how much usable evidence exists this period, as
// distinct from Time Progress (pure calendar math, computed earlier) as two
// numbers can be (2026-07-28 second refinement pass, item 2). This depends
// ONLY on $totalTrades (already period + account-view scoped by the query
// above), never on today's date. A trader with 500 trades by January 2nd
// has 1% Time Progress and a high Data Completeness; a trader with 5 trades
// in a completed December has 100% Time Progress and a low Data
// Completeness. Both are correct simultaneously; neither substitutes for
// the other.
//
// Modeled as a diminishing-returns curve (1 - e^-n/15) rather than a linear
// ratio against some fixed "target" trade count: the marginal statistical
// value of the 2nd trade is much higher than the 51st, so completeness
// should climb fast early and flatten, never actually reaching 100% (an
// infinite sample never truly exists, so the curve approaches but never
// touches it, capped for display only). The /15 divisor is a judgment
// call, not independently confirmed with Kevin: chosen so it crosses ~50%
// around 10 trades and ~80% around 24 trades, sitting between this
// codebase's own already-established n-aware tier boundaries (10/20/100 —
// see EdgeHealthEngine::getPeriodFactors()) rather than inventing new ones.
// Tier labels below reuse that exact same vocabulary for consistency.
$periodDataCompletenessPct = $totalTrades > 0 ? (int)round((1 - exp(-$totalTrades / 15)) * 100) : 0;
$periodDataCompletenessTier = match (true) {
    $totalTrades === 0 => 'no_trades',
    $totalTrades === 1 => 'single_trade',
    $totalTrades < 10  => 'very_small_sample',
    $totalTrades < 20  => 'small_sample',
    $totalTrades < 100 => 'developing',
    default            => 'confident',
};
$periodDataCompletenessLabel = match ($periodDataCompletenessTier) {
    'no_trades'         => 'No trades yet',
    'single_trade'      => 'One trade, not yet a pattern',
    'very_small_sample' => 'Very small sample',
    'small_sample'      => 'Small sample',
    'developing'        => 'Developing sample',
    'confident'         => 'Strong sample',
};
$totalWins       = (int)($st['wins'] ?? 0);
$totalLosses     = (int)($st['losses'] ?? 0);
$totalBreakevens = (int)($st['breakevens'] ?? 0);
$totalPnl        = round((float)($st['total_pnl'] ?? 0), 2);
$totalWinRate    = $totalTrades > 0 ? round(($totalWins / $totalTrades) * 100, 1) : 0;
$averageRisk     = round((float)($st['avg_risk'] ?? 0), 2);
$bestTrade       = isset($st['best_trade'])  && $st['best_trade']  !== null ? round((float)$st['best_trade'],  2) : null;
$worstTrade      = isset($st['worst_trade']) && $st['worst_trade'] !== null ? round((float)$st['worst_trade'], 2) : null;

// Prior-period comparison, same scope (account view, strategy filter) as
// $st above, just against $prevStartStr/$prevEndStr instead of the current
// period. Feeds the 4-Stage Summary's Top Summary Metrics deltas. Reuses
// $pnlExpr/$tradesAccountsJoin/$svidConditionT so "prior period" is defined
// identically to "this period", only the date range differs.
$prevPeriodParams = array_merge($baseParams, [':s' => $prevStartStr, ':e' => $prevEndStr]);
$sPrev = $db->raw(
    "SELECT COUNT(*) AS total_trades,
        SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins,
        SUM(CASE WHEN $pnlExpr < 0 THEN 1 ELSE 0 END) AS losses,
        COALESCE(SUM($pnlExpr), 0) AS total_pnl
     FROM $tradesAccountsJoin
     WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL
     AND t.closed_at>=:s AND t.closed_at<=:e" . $svidConditionT,
    $prevPeriodParams
);
$stPrev = $sPrev[0] ?? [];
$prevTotalTrades = (int)($stPrev['total_trades'] ?? 0);
$prevTotalWins    = (int)($stPrev['wins'] ?? 0);
$prevTotalPnl     = round((float)($stPrev['total_pnl'] ?? 0), 2);
$prevTotalWinRate = $prevTotalTrades > 0 ? round(($prevTotalWins / $prevTotalTrades) * 100, 1) : null;

// Deltas, null (not 0) when there's no prior-period data to compare
// against, so the UI can say "no prior period to compare" rather than a
// misleading "+0.0" that implies a real, flat comparison.
$deltaWinRate = ($prevTotalTrades > 0) ? round($totalWinRate - $prevTotalWinRate, 1) : null;
$deltaTotalPnl = ($prevTotalTrades > 0) ? round($totalPnl - $prevTotalPnl, 2) : null;
// Ending balance is an all-time-anchored account fact (current_balance,
// right now) per this file's established "account facts stay all-time"
// convention, since every period tab here represents "current" (no
// prev/next navigation exists yet), current_balance already IS the true
// end-of-selected-period balance. The prior period's ending balance is
// reconstructed by unwinding this period's own realized P&L from today's
// balance (current_balance - $totalPnl), exact so long as there were no
// deposits/withdrawals inside the period, which isn't tracked per-period
// here; documented approximation, not a bug.
$deltaEndingBalance = $deltaTotalPnl; // balance moves by exactly the period's own net P&L

$topInst = $db->raw("SELECT instrument, COUNT(*) AS count FROM trades WHERE user_id=:uid AND (is_validation = 0 OR is_validation IS NULL) AND status='closed' AND deleted_at IS NULL AND closed_at>=:s AND closed_at<=:e".$svidCondition." ".$avAccountIdFilter." GROUP BY instrument ORDER BY count DESC LIMIT 1", $periodParams);
$topInstrumentName = $topInst[0]['instrument'] ?? '-';

$classificationRows = $db->raw("SELECT trade_classification, COUNT(*) AS count FROM trades WHERE user_id=:uid AND (is_validation = 0 OR is_validation IS NULL) AND status='closed' AND deleted_at IS NULL AND closed_at>=:s AND closed_at<=:e".$svidCondition." ".$avAccountIdFilter." GROUP BY trade_classification ORDER BY count DESC", $periodParams);

// ════════════════════════════════════════════════════════════════════
// Session Performance
// ════════════════════════════════════════════════════════════════════
$sessionRows = [];
$sessionData = $db->raw(
    "SELECT t.session AS session, COUNT(*) AS trades,
        SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins,
        SUM($pnlExpr) AS total_pnl
    FROM $tradesAccountsJoin
    WHERE t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL
    AND t.closed_at >= :s AND t.closed_at <= :e
    AND t.session IN ('Sydney', 'Tokyo', 'London', 'New York')
    " . $svidConditionT . "
    GROUP BY t.session
    ORDER BY FIELD(t.session, 'Sydney', 'Tokyo', 'London', 'New York')",
    $periodParams
);
foreach ($sessionData as $r) $sessionRows[] = $r;

$declaredSessionsList = !empty($declaredSessions) ? $declaredSessions : [];
$declaredSessionParams = [':uid' => $userId, ':s' => $startStr, ':e' => $endStr];
if ($selectedVersionId) $declaredSessionParams[':svid'] = $selectedVersionId;

$tradesOutsideDeclaredSessions = 0;
$allSessionTradesCount = 0;
$sessionWarning = '';

if (!empty($declaredSessionsList)) {
    $allSessionTradesRow = $db->raw(
        "SELECT COUNT(*) AS cnt FROM trades
        WHERE user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL
        AND closed_at >= :s AND closed_at <= :e
        AND session IS NOT NULL AND session != ''" . $svidCondition . " " . $avAccountIdFilter,
        $declaredSessionParams
    );
    $allSessionTradesCount = (int)($allSessionTradesRow[0]['cnt'] ?? 0);

    $notInPlaceholders = [];
    $notInParams = [':uid' => $userId, ':s' => $startStr, ':e' => $endStr];
    if ($selectedVersionId) $notInParams[':svid'] = $selectedVersionId;

    foreach ($declaredSessionsList as $i => $ds) {
        $ph = ':decl_sess_' . $i;
        $notInPlaceholders[] = $ph;
        $notInParams[$ph] = $ds;
    }

    if (!empty($notInPlaceholders)) {
        $outsideRow = $db->raw(
            "SELECT COUNT(*) AS cnt FROM trades
            WHERE user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL
            AND closed_at >= :s AND closed_at <= :e
            AND session IS NOT NULL AND session != ''
            AND session NOT IN (" . implode(',', $notInPlaceholders) . ")" . $svidCondition . " " . $avAccountIdFilter,
            $notInParams
        );
        $tradesOutsideDeclaredSessions = (int)($outsideRow[0]['cnt'] ?? 0);
    }

    if ($tradesOutsideDeclaredSessions > 0) {
        $sessionWarning = $tradesOutsideDeclaredSessions . ' of your ' . $allSessionTradesCount . ' trades were in sessions you did NOT declare in this strategy. Your declared sessions are: ' . implode(', ', $declaredSessionsList) . '. Trading outside declared sessions makes your edge harder to measure.';
    }
}

// ════════════════════════════════════════════════════════════════════
// Instrument Performance
// ════════════════════════════════════════════════════════════════════
// over_risk_count is now folded into this single aggregate query (was
// previously one extra query PER instrument, with thousands of
// instruments that's thousands of extra round-trips). win_rate is
// computed from already-fetched wins/trades, no query needed for it.
$instrumentPeriodParamsWithRisk = array_merge($periodParams, [':max_risk' => $declaredMaxRiskPct + RISK_BREACH_TOLERANCE_PCT]);
$instrumentDetailRows = [];
$instrumentDetailData = $db->raw(
    "SELECT t.instrument AS instrument,
        COUNT(*) AS trades,
        SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins,
        SUM($pnlExpr) AS total_pnl,
        AVG(t.risk_pct) AS avg_risk_pct,
        COUNT(DISTINCT t.trade_classification) AS distinct_classifications,
        SUM(CASE WHEN t.risk_pct > :max_risk THEN 1 ELSE 0 END) AS over_risk_count
    FROM $tradesAccountsJoin
    WHERE t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL
    AND t.closed_at >= :s AND t.closed_at <= :e
    AND t.instrument IS NOT NULL AND t.instrument != ''
    " . $svidConditionT . "
    GROUP BY t.instrument
    ORDER BY trades DESC",
    $instrumentPeriodParamsWithRisk
);

foreach ($instrumentDetailData as &$instRow) {
    $instRow['win_rate']        = $instRow['trades'] > 0 ? round(($instRow['wins'] / $instRow['trades']) * 100, 1) : 0;
    $instRow['over_risk_count'] = (int)($instRow['over_risk_count'] ?? 0);
}
unset($instRow);
$instrumentDetailRows = $instrumentDetailData;

// ── Top-8 cap for the Instrument Performance display cards ─────────────
// A trader can have hundreds of instruments; Reports never lists them all
// individually. Top 8 by absolute P&L (same ranking as the Instrument
// P&L chart above), with a "+N more" note for the remainder. The full
// $instrumentDetailRows (uncapped) is still used below for Asset Class
// Edge Detection, which is naturally bounded to ~6 fixed categories.
$topInstrumentCards = $instrumentDetailRows;
usort($topInstrumentCards, fn($a, $b) => abs((float)$b['total_pnl']) <=> abs((float)$a['total_pnl']));
$remainingInstrumentCount = max(0, count($topInstrumentCards) - 8);
$topInstrumentCards = array_slice($topInstrumentCards, 0, 8);

// Classification breakdown pills are decorative, per-card detail, only
// fetch them for the 8 instruments that will actually be shown.
foreach ($topInstrumentCards as &$topInst) {
    $classData = $db->raw(
        "SELECT trade_classification, COUNT(*) AS count
        FROM trades
        WHERE user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL
        AND closed_at >= :s AND closed_at <= :e
        AND instrument = :inst" . $svidCondition . " " . $avAccountIdFilter . "
        GROUP BY trade_classification
        ORDER BY count DESC",
        array_merge($periodParams, [':inst' => $topInst['instrument']])
    );
    $topInst['classifications'] = $classData;
}
unset($topInst);

// ── Asset Class Mapping ───────────────────────────────────────────────
// Real trades.instrument values are stored lowercase with no separator
// (confirmed live: 'eurusd', 'xauusd', 'gbpusd', 'ger40', ...) but this map's
// symbols use slashes and uppercase ('EUR/USD'). strtoupper() alone never
// closed that gap, so in_array() never matched a single real instrument for
// any user, $assetClassData was unconditionally empty in production, which
// is why the "Trade Analysis" collapsible (whose only card is Asset Class
// Edge Detection) always rendered empty when expanded. normalizeSymbol()
// strips separators before comparing so real instrument strings actually match.
function normalizeSymbol($s) {
    return strtoupper(str_replace(['/', '-', '_', ' '], '', (string)$s));
}
$assetClassMap = [
    'Forex Majors'  => ['EUR/USD','USD/JPY','GBP/USD','USD/CHF','AUD/USD','USD/CAD','NZD/USD'],
    'Forex Crosses' => ['EUR/GBP','EUR/JPY','EUR/CHF','GBP/JPY','GBP/CHF','AUD/JPY','AUD/NZD','NZD/JPY','CAD/JPY','CHF/JPY','EUR/AUD','EUR/NZD','GBP/AUD','GBP/NZD'],
    'Metals'        => ['XAU/USD','XAG/USD','XPT/USD','XPD/USD','GOLD','SILVER'],
    'Indices'       => ['US30','US100','US500','SPX500','NAS100','UK100','GER40','FRA40','JPN225','AUS200','EU50'],
    'Commodities'   => ['USOIL','UKOIL','XBR/USD','XTI/USD','NGAS','COPPER','CORN','WHEAT','SOYBEAN'],
    'Crypto'        => ['BTC/USD','ETH/USD','XRP/USD','LTC/USD','BCH/USD','ADA/USD','DOT/USD','SOL/USD'],
];

$assetClassData = [];
foreach ($instrumentDetailRows as $instRow) {
    $instrument = $instRow['instrument'];
    foreach ($assetClassMap as $className => $symbols) {
        if (in_array(normalizeSymbol($instrument), array_map('normalizeSymbol', $symbols))) {
            if (!isset($assetClassData[$className])) {
                $assetClassData[$className] = ['trades'=>0,'wins'=>0,'total_pnl'=>0,'instruments'=>[],'total_risk_breaches'=>0,'in_declared'=>false];
            }
            $assetClassData[$className]['trades']             += $instRow['trades'];
            $assetClassData[$className]['wins']               += $instRow['wins'];
            $assetClassData[$className]['total_pnl']          += $instRow['total_pnl'];
            $assetClassData[$className]['instruments'][]       = $instrument;
            $assetClassData[$className]['total_risk_breaches'] += $instRow['over_risk_count'];
            break;
        }
    }
}

$declaredInstrumentsUpper = array_map('strtoupper', $declaredInstruments);
foreach ($assetClassData as $className => &$acData) {
    $overlap = array_intersect(array_map('strtoupper', $acData['instruments']), $declaredInstrumentsUpper);
    $acData['in_declared'] = !empty($overlap);
    $acData['win_rate']    = $acData['trades'] > 0 ? round(($acData['wins'] / $acData['trades']) * 100, 1) : 0;
    $acData['avg_risk']    = 0;
    if (!empty($acData['instruments'])) {
        $instPlaceholders = [];
        $instParams = [':uid' => $userId, ':s' => $startStr, ':e' => $endStr];
        if ($selectedVersionId) $instParams[':svid'] = $selectedVersionId;
        foreach ($acData['instruments'] as $j => $instr) {
            $ph = ':ac_inst_' . $j;
            $instPlaceholders[] = $ph;
            $instParams[$ph] = $instr;
        }
        $acRiskRow = $db->raw(
            "SELECT AVG(risk_pct) AS avg_risk FROM trades
            WHERE user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL
            AND closed_at >= :s AND closed_at <= :e
            AND instrument IN (" . implode(',', $instPlaceholders) . ")" . $svidCondition . " " . $avAccountIdFilter,
            $instParams
        );
        $acData['avg_risk'] = round((float)($acRiskRow[0]['avg_risk'] ?? 0), 2);
    }
}
unset($acData);
uasort($assetClassData, fn($a, $b) => $b['total_pnl'] <=> $a['total_pnl']);

// Instrument chart data, capped at 8, ordered by absolute P&L magnitude so the
// most impactful instruments (biggest wins OR biggest losses) always appear.
// Deliberately NOT repointed at mv_instrument_performance or a shared
// AnalyticsEngine method, this read is genuinely period-scoped (:s/:e,
// bound to the page's selected reporting period, optionally strategy-
// filtered), a question the view structurally cannot answer (no date
// parameterization). See docs/architecture/
// SINGLE_SOURCE_OF_TRUTH_MIGRATION_PLAN.md's Wave 6 entry for the full
// three-way comparison and why this was left as its own implementation.
$instrumentChartRows = [];
$instrumentChartData = $db->raw(
    "SELECT t.instrument AS instrument, COUNT(*) AS trades, SUM($pnlExpr) AS total_pnl
    FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL
    AND t.closed_at>=:s AND t.closed_at<=:e".$svidConditionT."
    GROUP BY t.instrument ORDER BY ABS(SUM($pnlExpr)) DESC LIMIT 8",
    $periodParams
);
foreach ($instrumentChartData as $r) $instrumentChartRows[] = $r;

$sessionDataChart = $db->raw("SELECT t.session AS session, COUNT(*) AS trades, SUM($pnlExpr) AS total_pnl FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL AND t.closed_at>=:s AND t.closed_at<=:e AND t.session IS NOT NULL AND t.session!='' AND t.session NOT LIKE '%Closed%'".$svidConditionT." GROUP BY t.session ORDER BY FIELD(t.session, 'London', 'New York', 'Asia', 'Sydney'), trades DESC", $periodParams);
$sessionChartRows = [];
foreach ($sessionDataChart as $r) $sessionChartRows[] = $r;

$expectancy = $profitFactor = $averageWin = $averageLoss = null;
$overRiskCount = 0;
if ($depth >= 2) {
    $expectancyData = $db->raw("SELECT AVG(CASE WHEN $pnlExpr>0 THEN $pnlExpr END) AS average_win, AVG(CASE WHEN $pnlExpr<0 THEN $pnlExpr END) AS average_loss FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL AND t.closed_at>=:s AND t.closed_at<=:e".$svidConditionT, $periodParams);
    $averageWin     = round((float)($expectancyData[0]['average_win']  ?? 0), 2);
    $averageLoss    = round((float)($expectancyData[0]['average_loss'] ?? 0), 2);
    $expectancy     = round(($totalWinRate / 100) * $averageWin + (1 - $totalWinRate / 100) * $averageLoss, 2);
    $profitFactor   = ($averageLoss != 0 && $averageWin > 0 && $totalLosses > 0) ? round(abs($averageWin * $totalWins / ($averageLoss * $totalLosses)), 2) : null;
    $overRiskCount  = (int)($db->raw("SELECT COUNT(*) AS count FROM trades WHERE user_id=:uid AND (is_validation = 0 OR is_validation IS NULL) AND status='closed' AND deleted_at IS NULL AND closed_at>=:s AND closed_at<=:e AND risk_pct > 1.05".$svidCondition." ".$avAccountIdFilter, $periodParams)[0]['count'] ?? 0);
}

// Strategy Performance table data, grouped by strategy_id only (not
// strategy_id + version_id). TRADERALIGN STRATEGY BUSINESS RULES redesign
// (2026-08-11) §15/§26: a strategy is ONE entity for lifetime/period
// performance purposes, never split into per-version rows just because it
// was edited. Going forward each strategy has exactly one live version
// row, so this GROUP BY is a no-op for new-model strategies; for strategies
// with pre-existing historical version rows (edited before this redesign),
// it correctly rolls their period performance up into one row instead of
// showing the same strategy fragmented across its old eras.
$strategyPerformance = [];
if ($isStarterOrAbove && $totalTrades > 0) {
    $strategyPerformance = $db->raw(
        "SELECT s.id as strategy_id, s.current_version_id AS version_id, TRIM(s.name) AS name, COUNT(t.id) AS trades,
            SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins,
            SUM($pnlExpr) AS total_pnl,
            AVG(t.risk_pct) AS avg_risk,
            MAX(CASE WHEN $pnlExpr > 0 THEN $pnlExpr END) AS best_trade,
            MIN(CASE WHEN $pnlExpr < 0 THEN $pnlExpr END) AS worst_trade
        FROM trades t
        JOIN strategy_versions sv ON sv.id = t.strategy_version_id
        JOIN strategies s ON s.id = sv.strategy_id
        LEFT JOIN trading_accounts ta ON ta.id = t.account_id
        WHERE t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL
        AND t.closed_at >= :s AND t.closed_at <= :e" . $svidConditionT . "
        GROUP BY s.id, s.name, s.current_version_id
        ORDER BY total_pnl DESC",
        $periodParams
    );
}

// Strategy P&L chart data
$strategyChartLabels = []; $strategyChartData = []; $strategyChartBg = [];
if ($isStarterOrAbove && $totalTrades > 0) {
    $rawPerformance = $db->raw(
        "SELECT DATE(t.closed_at) AS day, SUM($pnlExpr) AS pnl
         FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL
         AND t.closed_at>=:s AND t.closed_at<=:e".$svidConditionT."
         GROUP BY DATE(t.closed_at) ORDER BY day ASC",
        $periodParams
    );
    $buckets = [];
    foreach ($rawPerformance as $r) {
        $dt  = new DateTime($r['day']);
        $key = match($chartGrouping) {
            'daily'     => $dt->format('Y-m-d'),
            'weekly'    => $dt->format('o-\WW'),
            'monthly'   => $dt->format('Y-m'),
            'quarterly' => 'Q' . ceil($dt->format('n') / 3) . ' ' . $dt->format('Y'),
            default     => $dt->format('Y-m-d')
        };
        if (!isset($buckets[$key])) $buckets[$key] = ['pnl' => 0, 'start' => $r['day']];
        $buckets[$key]['pnl'] += (float)$r['pnl'];
        if ($r['day'] < $buckets[$key]['start']) $buckets[$key]['start'] = $r['day'];
    }
    ksort($buckets);
    foreach ($buckets as $k => $bucket) {
        $displayLabel = match($chartGrouping) {
            'daily'     => date('D, M j', strtotime($bucket['start'])),
            'weekly'    => 'Week of ' . date('M j', strtotime($bucket['start'])),
            'monthly'   => date('M Y', strtotime($bucket['start'])),
            'quarterly' => $k,
            default     => $k
        };
        $val = round($bucket['pnl'], 2);
        $strategyChartLabels[] = $displayLabel;
        $strategyChartData[]   = $val;
        $strategyChartBg[]     = $val >= 0 ? '#26A69A' : '#EF5350';
    }
}

$maximumDrawdown = $drawdownFrequency = null;
if ($depth >= 3 && $isProOrAbove) {
    $maximumDrawdown  = $db->raw("SELECT MIN(cumulative) AS maximum_drawdown FROM (SELECT SUM($pnlExpr) OVER (ORDER BY t.closed_at) AS cumulative FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL AND t.closed_at>=:s AND t.closed_at<=:e".$svidConditionT.") sub", $periodParams)[0]['maximum_drawdown'] ?? null;
    $drawdownFrequency = (int)($db->raw("SELECT COUNT(*) AS count FROM (SELECT t.closed_at, SUM($pnlExpr) OVER (ORDER BY t.closed_at) AS cumulative FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL AND t.closed_at>=:s AND t.closed_at<=:e".$svidConditionT.") sub WHERE cumulative < -100", $periodParams)[0]['count'] ?? 0);
}

$cagr = $sharpeRatio = $sortinoRatio = $recoveryFactor = $calmarRatio = null;
if ($depth >= 4 && $isProOrAbove) {
    $startingBalance = (float)($db->select('trading_accounts', 'is_active=1 AND deleted_at IS NULL', [], 'starting_balance', 'created_at DESC', 1)[0]['starting_balance'] ?? 0);
    $endingBalance   = $startingBalance + $totalPnl;
    $cagr = $startingBalance > 0 ? round((pow($endingBalance / $startingBalance, 1 / 1) - 1) * 100, 2) : null;
    $returns    = $db->raw("SELECT $pnlExpr AS pnl FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL AND t.closed_at>=:s AND t.closed_at<=:e".$svidConditionT." ORDER BY t.closed_at", $periodParams);
    $pnlValues  = [];
    foreach ($returns as $r) $pnlValues[] = (float)$r['pnl'];
    $averageReturn   = count($pnlValues) > 0 ? array_sum($pnlValues) / count($pnlValues) : 0;
    $variance        = 0;
    if (count($pnlValues) > 1) { $sumSq = 0; foreach ($pnlValues as $p) $sumSq += pow($p - $averageReturn, 2); $variance = $sumSq / (count($pnlValues) - 1); }
    $standardDeviation = sqrt(max($variance, 0.0001));
    $sharpeRatio       = $standardDeviation > 0 ? round($averageReturn / $standardDeviation, 2) : null;
    $negativePnls      = [];
    foreach ($pnlValues as $p) if ($p < 0) $negativePnls[] = $p;
    $downsideVariance  = 0;
    if (count($negativePnls) > 1) { $sumSqDown = 0; foreach ($negativePnls as $p) $sumSqDown += pow($p - $averageReturn, 2); $downsideVariance = $sumSqDown / (count($negativePnls) - 1); }
    $sortinoRatio    = $downsideVariance > 0 ? round($averageReturn / sqrt($downsideVariance), 2) : null;
    $recoveryFactor  = ($maximumDrawdown !== null && $maximumDrawdown < 0) ? round($totalPnl / abs((float)$maximumDrawdown), 2) : null;
    $calmarRatio     = ($maximumDrawdown !== null && $maximumDrawdown < 0 && $cagr !== null) ? round($cagr / abs((float)$maximumDrawdown / 100), 2) : null;
}

if ($selectedVersionId) {
    $allTime = $db->raw("SELECT SUM($pnlExpr) AS total_pnl FROM $tradesAccountsJoin WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL".$svidConditionT, $baseParams);
} else {
    // Same mv_account_performance per-(account,phase)-row fix as above —
    // SUM() across all of the user's rows instead of one row via LIMIT 1.
    // Joined to trading_accounts and $avTypeFilter-scoped so "All-Time"
    // reflects the active account-view tab, not the whole platform, mv_
    // account_performance has no account_type column of its own, only
    // account_id, so the join is required to apply the same prop/live
    // split every other query on this page now uses.
    $allTime = $db->raw("SELECT SUM(mv.total_pnl) AS total_pnl FROM mv_account_performance mv LEFT JOIN trading_accounts ta ON ta.id = mv.account_id WHERE mv.user_id=:uid " . $avTypeFilter, [':uid' => $userId]);
}
$allTimeStats = $allTime[0] ?? [];
$allTimePnl   = round((float)($allTimeStats['total_pnl'] ?? 0), 2);

// pnlClass()/pnlFormat() now live in src/RenderHelpers.php (required globally
// by index.php), removed from here to stop the six-file copy-paste drift.

$classificationBarColors = [
    'disciplined_win' => 'var(--positive)', 'elevated_win'  => 'var(--positive)',
    'breakeven'       => 'var(--warning)',  'controlled_loss' => 'var(--warning)',
    'loss_high_risk'  => '#F97316',         'tilt_warning'    => '#F97316',
    'revenge_trade'   => 'var(--negative)',
];

$sessionLabels         = [];
$sessionChartDataArr   = [];
foreach ($sessionChartRows as $r) { $sessionLabels[] = $r['session']; $sessionChartDataArr[] = (int)$r['trades']; }

$instrumentLabels           = [];
$instrumentChartDataArr     = [];
$instrumentChartBackgrounds = [];
foreach ($instrumentChartRows as $r) {
    $instrumentLabels[]           = $r['instrument'];
    $instrumentChartDataArr[]     = round((float)$r['total_pnl'], 2);
    $instrumentChartBackgrounds[] = (float)$r['total_pnl'] >= 0 ? '#26A69A' : '#EF5350';
}

$metricDefinitions = [
    'expectancy'        => 'The average dollar amount you expect to win or lose per trade over the long run.',
    'profitFactor'      => 'Gross profit divided by gross loss.',
    'averageWin'        => 'The average profit across all winning trades.',
    'averageLoss'       => 'The average loss across all losing trades.',
    'overRisking'       => 'Trades where risk exceeded your declared maximum risk per trade.',
    'cagr'              => 'Compound Annual Growth Rate.',
    'sharpeRatio'       => 'Sharpe Ratio, return relative to total volatility.',
    'sortinoRatio'      => 'Sortino Ratio, like Sharpe but only penalizes downside volatility.',
    'recoveryFactor'    => 'Recovery Factor, net profit divided by maximum drawdown.',
    'calmarRatio'       => 'Calmar Ratio, annual return divided by maximum drawdown percentage.',
    'maximumDrawdown'   => 'Maximum Drawdown, largest peak-to-trough decline.',
    'drawdownFrequency' => 'Drawdown Frequency, times cumulative P&L dipped below -$100.',
];

// ════════════════════════════════════════════════════════════════════
// Journal routing (updated 2026-07-31 for the Active Journeys/Trading
// Archive redesign), Active Journeys' merged Trade Journal section
// (?workspace=active) is the canonical trade viewer across the platform:
// Reports → Filtered Trade Journal → Individual Trade → (Trade Details |
// 4-Stage). It supports real filters (account_id, instrument, session,
// classification, strategy_version_id, date range), so links below are
// genuinely precise instead of landing on "the right account, but not
// the right trades." Aggregate breach/pass counts route to Trading
// Archive (?workspace=archive) instead, since that's a lifecycle-state
// question the Trade Journal's filters can't express.
// ════════════════════════════════════════════════════════════════════

/**
 * Builds an Active Journeys URL for one specific account (optionally one
 * specific trade on it), journal redesign, 2026-07-31 (was Journal
 * Trades' ?view=trades; the merged Trade Journal section reads the same
 * account_id/trade_id params, now under ?workspace=active). Also sets
 * rn_account so the account picker/phase cards above the Trade Journal
 * land on the same account, not just the filtered table.
 * $challengeType/$lifecycleState are accepted but unused, kept for
 * call-site backward compatibility rather than touching every existing
 * call site's argument list.
 */
function journalAccountUrl($challengeType, $lifecycleState, $accountId, $tradeId = null) {
    $params = ['workspace' => 'active'];
    if ($tradeId) { $params['trade_id'] = $tradeId; }
    elseif ($accountId) { $params['account_id'] = $accountId; $params['rn_account'] = $accountId; }
    return '/traderalign/journal?' . http_build_query($params);
}

/** Builds an Active Journeys Trade Journal URL filtered by any combination of instrument/session/classification/strategy/date-range. */
function journalTradesFilterUrl(array $filters) {
    return '/traderalign/journal?' . http_build_query(array_merge(['workspace' => 'active'], array_filter($filters)));
}

/**
 * Builds a Trading Archive URL (item 5, 2026-07-27 finishing pass;
 * repointed 2026-07-31, the old Journal Coaching "Completed Journeys"
 * hub this used to target no longer exists, Trading Archive is its real
 * successor), the correct target for aggregate breach/pass counts,
 * since the Trade Journal's filters have no lifecycle-state param to
 * select "every breached account." $category is 'breached' or 'passed';
 * $accountId narrows to one specific account.
 */
function journalCoachingHubUrl($category, $accountId = null) {
    $params = ['workspace' => 'archive', 'archive_type' => 'prop', 'archive_lens' => $category === 'passed' ? 'success' : 'failure'];
    if ($accountId) { $params['account_id'] = $accountId; }
    return '/traderalign/journal?' . http_build_query($params);
}

// journalFilterUrl() intentionally removed, every call site below now
// uses journalAccountUrl() (a real account/trade destination) or, where
// no single account applies (aggregate stats spanning many accounts,
// e.g. Instrument/Session Performance), the link is dropped rather than
// pointed at a filter Journal can't honor.

// ════════════════════════════════════════════════════════════════
// (Removed) Intent vs Reality, per-trade plan/reality table.
// Superseded by the aggregated Four-Stage Trade Comparison further
// below, which now also computes $executionScore / $biggestGapMetric
// from aggregate deviations instead of a per-trade table.
// ════════════════════════════════════════════════════════════════
$executionScore    = 0;
$biggestGapMetric  = 'None';

// ════════════════════════════════════════════════════════════════
// ANALYTICS ACROSS STRATEGIES
// ════════════════════════════════════════════════════════════════
$crossStrategy = [];
if ($isEliteOrUltimate) {
    // $avTypeFilter appended so "Analytics Across Strategies" (portfolio-
    // level, but still trade-derived) respects the active account-view tab
    // like every other section, was previously platform-wide regardless
    // of the Prop Firms/Live Accounts/Comparison toggle.
    //
    // TRADERALIGN STRATEGY BUSINESS RULES redesign (2026-08-11): trades
    // are now aggregated across EVERY version row a strategy has ever had
    // (`sv`, matters only for strategies edited before this redesign,
    // which may still carry more than one historical row; a strategy
    // edited going forward only ever has one), so a strategy's row here is
    // its real lifetime/period performance, never split by era (§15/§26).
    // Declared limits (max_trades_per_day/week, max_losing_streak,
    // max_risk_per_trade_pct, avg_trades_per_week) are sourced from `cv` —
    // the strategy's CURRENT declaration specifically, via
    // s.current_version_id, since those are being used below to judge
    // "is this strategy in breach of its rules RIGHT NOW," which must mean
    // its live rules, not some past era's.
    $crossStrategy = $db->raw("SELECT s.id AS strategy_id, TRIM(s.name) AS name, cv.id AS version_id, COUNT(t.id) AS trades, SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins, SUM(CASE WHEN $pnlExpr < 0 THEN 1 ELSE 0 END) AS losses, SUM($pnlExpr) AS total_pnl, AVG(t.risk_pct) AS average_risk, AVG(CASE WHEN t.risk_reward_tp1 IS NOT NULL THEN t.risk_reward_tp1 END) AS average_reward, MAX(t.closed_at) AS last_trade_date, DATEDIFF(NOW(), MAX(t.closed_at)) AS days_since_last_trade, CASE WHEN COUNT(t.id) < 10 THEN 1 ELSE 0 END AS is_low_activity, CASE WHEN DATEDIFF(NOW(), MAX(t.closed_at)) > 30 THEN 1 ELSE 0 END AS is_stale, cv.max_trades_per_day, cv.max_trades_per_week, cv.max_losing_streak, cv.max_risk_per_trade_pct, cv.avg_trades_per_week FROM strategies s JOIN strategy_versions cv ON cv.id = s.current_version_id JOIN strategy_versions sv ON sv.strategy_id = s.id JOIN trades t ON t.strategy_version_id = sv.id LEFT JOIN trading_accounts ta ON ta.id = t.account_id WHERE s.user_id = :uid AND t.status = 'closed' AND t.deleted_at IS NULL AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.closed_at >= :s AND t.closed_at <= :e AND s.is_active = 1 {$avTypeFilter} GROUP BY s.id, s.name, cv.id, cv.max_trades_per_day, cv.max_trades_per_week, cv.max_losing_streak, cv.max_risk_per_trade_pct, cv.avg_trades_per_week HAVING trades >= 1 ORDER BY total_pnl DESC", [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]);

    // Real bug found and fixed 2026-08-18 (Reports verification pass): no
    // s.is_validation filter here either — a validation-only strategy has
    // zero non-validation trades by construction, so it always passed the
    // NOT IN subquery below and rendered as a real "0 trades" row in the
    // main-workflow Strategy Comparison table. Same fix, same reasoning as
    // $allStrategies above.
    $untradedStrategies = $db->raw("SELECT s.id AS strategy_id, TRIM(s.name) AS name, cv.id AS version_id, 0 AS trades, 0 AS wins, 0 AS losses, 0 AS total_pnl, NULL AS average_risk, NULL AS average_reward, NULL AS last_trade_date, NULL AS days_since_last_trade, 1 AS is_low_activity, 0 AS is_stale, cv.max_trades_per_day, cv.max_trades_per_week, cv.max_losing_streak, cv.max_risk_per_trade_pct, cv.avg_trades_per_week FROM strategies s JOIN strategy_versions cv ON cv.id = s.current_version_id WHERE s.user_id = :uid AND s.is_active = 1 AND s.deleted_at IS NULL AND (s.is_validation = 0 OR s.is_validation IS NULL) AND s.id NOT IN (SELECT DISTINCT s2.id FROM strategies s2 JOIN strategy_versions sv2 ON sv2.strategy_id = s2.id JOIN trades t2 ON t2.strategy_version_id = sv2.id LEFT JOIN trading_accounts ta ON ta.id = t2.account_id WHERE s2.user_id = :uid2 AND t2.status = 'closed' AND t2.deleted_at IS NULL AND (t2.is_validation = 0 OR t2.is_validation IS NULL) {$avTypeFilter}) ORDER BY s.created_at DESC", [':uid' => $userId, ':uid2' => $userId]);

    $crossStrategy = array_merge($crossStrategy, $untradedStrategies);
    usort($crossStrategy, function($a, $b) {
        if ($a['trades'] > 0 && $b['trades'] > 0) return $b['total_pnl'] <=> $a['total_pnl'];
        if ($a['trades'] > 0) return -1;
        if ($b['trades'] > 0) return 1;
        return 0;
    });
}

$crossStrategyData = [];
if (!empty($crossStrategy)) {
    $globalTotalPnl    = array_sum(array_column($crossStrategy, 'total_pnl'));
    $globalTotalTrades = array_sum(array_column($crossStrategy, 'trades'));
    $globalTotalLosses = array_sum(array_column($crossStrategy, 'losses'));
    $onTrackCount = 0; $warningCount = 0; $inactiveCount = 0;

    foreach ($crossStrategy as &$cs) {
        $cs['win_rate']             = $cs['trades'] > 0 ? round($cs['wins'] / $cs['trades'] * 100, 1) : 0;
        $cs['expectancy']           = $cs['trades'] > 0 ? round($cs['total_pnl'] / $cs['trades'], 2) : 0;
        $cs['average_reward']       = round((float)($cs['average_reward'] ?? 0), 2);
        $cs['percentage_of_profit'] = $globalTotalPnl  != 0 ? round($cs['total_pnl'] / abs($globalTotalPnl) * 100, 1)   : 0;
        $cs['percentage_of_trades'] = $globalTotalTrades > 0 ? round($cs['trades']    / $globalTotalTrades * 100, 1)    : 0;
        $cs['percentage_of_losses'] = $globalTotalLosses > 0 ? round($cs['losses']    / $globalTotalLosses * 100, 1)    : 0;

        $cs['last_trade_display']    = '-';
        $cs['days_since_last_trade'] = null;
        if (!empty($cs['last_trade_date'])) {
            $lastTrade = new DateTime($cs['last_trade_date']);
            $cs['days_since_last_trade'] = (int)(new DateTime())->diff($lastTrade)->days;
            if ($cs['days_since_last_trade'] === 0) $cs['last_trade_display'] = 'Today';
            elseif ($cs['days_since_last_trade'] === 1) $cs['last_trade_display'] = '1 day ago';
            else $cs['last_trade_display'] = $cs['days_since_last_trade'] . ' days ago';
        }
        $cs['last_trade_color'] = $cs['days_since_last_trade'] === null ? 'var(--foreground-dim)' : ($cs['days_since_last_trade'] <= 7 ? 'var(--positive)' : ($cs['days_since_last_trade'] <= 30 ? 'var(--warning)' : 'var(--negative)'));

        $cs['status_context'] = '';
        $cs['status_label']   = 'On Track';
        // $svid = the strategy's CURRENT version id, every "is this
        // strategy in breach right now" check below (peak daily trades,
        // trades this week, current losing streak, risk breaches) is
        // deliberately scoped to only the current declaration's own
        // trades, not the strategy's whole lifetime (see the query comment
        // above), a past era's activity shouldn't count against today's
        // declared limits.
        $svid        = $cs['version_id'];
        $maxPerDay   = $cs['max_trades_per_day'];
        $maxPerWeek  = $cs['max_trades_per_week'];
        $maxLosingStreak = $cs['max_losing_streak'];
        $maxRiskPct  = $cs['max_risk_per_trade_pct'];

        if ($cs['trades'] == 0) {
            $cs['status_label'] = 'Not Yet Traded'; $cs['status_context'] = 'This strategy has no closed trades.'; $inactiveCount++;
        } elseif ($cs['days_since_last_trade'] !== null && $cs['days_since_last_trade'] > 30) {
            $cs['status_label'] = 'Inactive'; $cs['status_context'] = 'No trades in ' . $cs['days_since_last_trade'] . ' days.'; $inactiveCount++;
        } else {
            $peakDaily = $db->raw("SELECT DATE(closed_at) AS trade_date, COUNT(*) AS count FROM trades WHERE strategy_version_id = :svid AND user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL {$avAccountIdFilter} GROUP BY DATE(closed_at) ORDER BY count DESC LIMIT 1", [':svid' => $svid, ':uid' => $userId]);
            $cs['peak_daily_trades'] = !empty($peakDaily) ? (int)$peakDaily[0]['count'] : 0;
            $cs['peak_daily_date']   = !empty($peakDaily) ? $peakDaily[0]['trade_date'] : null;
            $weekStart = (new DateTime())->modify('monday this week')->format('Y-m-d');
            $tradesThisWeek = $db->raw("SELECT COUNT(*) AS count FROM trades WHERE strategy_version_id = :svid AND user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL AND DATE(closed_at) >= :ws {$avAccountIdFilter}", [':svid' => $svid, ':uid' => $userId, ':ws' => $weekStart]);
            $cs['trades_this_week']  = (int)($tradesThisWeek[0]['count'] ?? 0);
            $firstTradeDate          = $db->raw("SELECT MIN(closed_at) AS first_date FROM trades WHERE strategy_version_id = :svid AND user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL {$avAccountIdFilter}", [':svid' => $svid, ':uid' => $userId]);
            $firstDate               = $firstTradeDate[0]['first_date'] ?? null;
            $cs['days_since_first_trade'] = $firstDate ? (int)(new DateTime())->diff(new DateTime($firstDate))->days : 0;
            // Inner subquery finds the most recent winning trade (to bound how far
            // back the current losing streak extends) using its own hand-written
            // copy of the P&L expression, aliased t2/ta2, NOT a reference to
            // $pnlExpr, so it didn't inherit that fix. Found in a final full
            // sweep for this bug class: same missing-fallback pattern
            // (COALESCE(CASE ... THEN challenge_pnl ELSE broker_pnl END,
            // projected_pnl), no broker_pnl fallback for a prop-firm trade with
            // NULL challenge_pnl), now matching $pnlExpr's exact cascade.
            $losingStreak = $db->raw("SELECT COUNT(*) AS streak FROM (SELECT t.closed_at, $pnlExpr AS pnl FROM $tradesAccountsJoin WHERE t.strategy_version_id = :svid AND t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL {$avTypeFilter} ORDER BY t.closed_at DESC) sub WHERE pnl < 0 AND closed_at > (SELECT COALESCE(MAX(t2.closed_at), '1970-01-01') FROM trades t2 LEFT JOIN trading_accounts ta2 ON ta2.id = t2.account_id WHERE t2.strategy_version_id = :svid2 AND t2.user_id = :uid2 AND t2.status = 'closed' AND t2.deleted_at IS NULL AND (CASE WHEN ta2.account_type = 'prop_firm' THEN COALESCE(t2.challenge_pnl, t2.broker_pnl, t2.projected_pnl, 0) ELSE COALESCE(t2.broker_pnl, t2.projected_pnl, 0) END) >= 0 " . str_replace('ta.', 'ta2.', $avTypeFilter) . ")", [':svid' => $svid, ':uid' => $userId, ':svid2' => $svid, ':uid2' => $userId]);
            $cs['current_losing_streak'] = (int)($losingStreak[0]['streak'] ?? 0);
            $riskBreaches = $db->raw("SELECT COUNT(*) AS count FROM trades WHERE strategy_version_id = :svid AND user_id = :uid AND (is_validation = 0 OR is_validation IS NULL) AND status = 'closed' AND deleted_at IS NULL AND risk_pct > :max_risk {$avAccountIdFilter}", [':svid' => $svid, ':uid' => $userId, ':max_risk' => ($maxRiskPct ?? 100) + RISK_BREACH_TOLERANCE_PCT]);
            $cs['risk_breaches'] = (int)($riskBreaches[0]['count'] ?? 0);
            $statusAssigned = false;
            if (!$statusAssigned && $maxPerDay !== null && $cs['peak_daily_trades'] > $maxPerDay) { $cs['status_label'] = 'Overactive'; $peakDateF = $cs['peak_daily_date'] ? date('M j', strtotime($cs['peak_daily_date'])) : ''; $cs['status_context'] = 'Exceeded ' . $maxPerDay . '/day limit, ' . $cs['peak_daily_trades'] . ' trades on ' . $peakDateF . '.'; $statusAssigned = true; }
            if (!$statusAssigned && $maxPerWeek !== null && $cs['trades_this_week'] > $maxPerWeek) { $cs['status_label'] = 'Overactive'; $cs['status_context'] = 'Exceeded ' . $maxPerWeek . '/week limit, ' . $cs['trades_this_week'] . ' trades this week.'; $statusAssigned = true; }
            if (!$statusAssigned && $maxPerDay !== null && $cs['peak_daily_trades'] == $maxPerDay) { $cs['status_label'] = 'At Capacity'; $cs['status_context'] = 'Hitting daily limit of ' . $maxPerDay . ' trades/day.'; $statusAssigned = true; }
            if (!$statusAssigned && $maxLosingStreak !== null && $cs['current_losing_streak'] >= $maxLosingStreak) { $cs['status_label'] = 'Streak Breach'; $cs['status_context'] = $cs['current_losing_streak'] . ' consecutive losses, limit is ' . $maxLosingStreak . '.'; $statusAssigned = true; }
            if (!$statusAssigned && $cs['risk_breaches'] > 0) { $cs['status_label'] = 'Risk Breach'; $cs['status_context'] = $cs['risk_breaches'] . ' trade' . ($cs['risk_breaches'] > 1 ? 's' : '') . ' exceeded ' . ($maxRiskPct ?? 0) . '% risk limit.'; $statusAssigned = true; }
            if (!$statusAssigned && $maxPerWeek !== null && $cs['trades_this_week'] >= ($maxPerWeek * 0.8)) { $cs['status_label'] = 'Near Limit'; $cs['status_context'] = 'Approaching weekly cap, ' . $cs['trades_this_week'] . '/' . $maxPerWeek . ' trades this week.'; $statusAssigned = true; }
            if (!$statusAssigned && $cs['trades'] >= 5) { $expectedWeekly = $cs['avg_trades_per_week'] ?? ($cs['trades'] / max(($cs['days_since_first_trade'] / 7), 1)); if ($expectedWeekly > 0 && $cs['trades_this_week'] < ($expectedWeekly * 0.3)) { $cs['status_label'] = 'Underused'; $cs['status_context'] = 'Below expected activity, ' . $cs['trades_this_week'] . ' trades this week.'; $statusAssigned = true; } }
            if (!$statusAssigned && $cs['trades'] < 10 && $cs['days_since_first_trade'] <= 30) { $cs['status_label'] = 'New'; $cs['status_context'] = 'Building track record, ' . $cs['trades'] . ' trades so far.'; $statusAssigned = true; }
            if (!$statusAssigned) { $cs['status_label'] = 'On Track'; $cs['status_context'] = 'Within declared rules.'; }
            $greenStatuses  = ['On Track', 'At Capacity', 'New'];
            $amberStatuses  = ['Underused', 'Near Limit'];
            $orangeStatuses = ['Overactive', 'Streak Breach', 'Risk Breach'];
            $redStatuses    = ['Inactive', 'Not Yet Traded'];
            if (in_array($cs['status_label'], $greenStatuses))       $onTrackCount++;
            elseif (in_array($cs['status_label'], $amberStatuses) || in_array($cs['status_label'], $orangeStatuses)) $warningCount++;
            elseif (in_array($cs['status_label'], $redStatuses))     $inactiveCount++;
        }
        $positivePnl = 0; $negativePnl = 0;
        if ($cs['trades'] > 0) {
            $details = $db->raw("SELECT $pnlExpr AS pnl FROM $tradesAccountsJoin WHERE t.strategy_version_id = :svid AND t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL {$avTypeFilter}", [':svid' => $cs['version_id'], ':uid' => $userId]);
            foreach ($details as $d) { $p = (float)$d['pnl']; if ($p > 0) $positivePnl += $p; else $negativePnl += abs($p); }
        }
        $cs['profit_factor'] = $negativePnl > 0 ? round($positivePnl / $negativePnl, 2) : ($positivePnl > 0 ? null : 0);
    }
    unset($cs);

    $sortedByPnl          = $crossStrategy;
    usort($sortedByPnl, fn($a, $b) => $b['total_pnl'] <=> $a['total_pnl']);
    $topContributors      = array_slice($sortedByPnl, 0, min(5, count($sortedByPnl)));
    $topTwoPnl            = array_sum(array_column(array_slice($sortedByPnl, 0, min(2, count($sortedByPnl))), 'total_pnl'));
    $concentrationPercentage = $globalTotalPnl > 0 ? round($topTwoPnl / $globalTotalPnl * 100) : 0;
    $topTwoStrategyNames  = array_map(fn($s) => $s['name'], array_slice($sortedByPnl, 0, min(2, count($sortedByPnl))));

    $statusPillClasses = [
        'On Track'      => 'pill-green', 'At Capacity'  => 'pill-green', 'New'          => 'pill-green',
        'Underused'     => 'pill-amber', 'Near Limit'   => 'pill-amber',
        'Overactive'    => 'pill-orange','Streak Breach' => 'pill-orange','Risk Breach'  => 'pill-orange',
        'Inactive'      => 'pill-red',   'Not Yet Traded'=> 'pill-red',
    ];
    $crossStrategyData = compact('onTrackCount', 'warningCount', 'inactiveCount', 'concentrationPercentage', 'topTwoStrategyNames', 'statusPillClasses', 'topContributors');
}

// ════════════════════════════════════════════════════════════════
// Strategy Evolution, TRADERALIGN STRATEGY BUSINESS RULES redesign
// (2026-08-11). Rebuilt on real Strategy Change Events instead of
// version-row diffing (§12/§15/§24/§27): "Original" is a strategy's own
// trades captured BEFORE its first recorded change; "Current" is trades
// captured AT OR AFTER its most recent recorded change. This is lifetime-
// scoped (no reporting-period date filter, matching the old
// implementation's own scope), only account-type-scoped ($avTypeFilter),
// since "how has this strategy evolved" is a fact about the whole
// strategy, not one reporting window. Every strategy in $crossStrategy is
// already grouped by strategy_id (post-redesign), so no per-version
// dedup step is needed here anymore.
$strategyEvolutionData = [];
if ($isEliteOrUltimate && !empty($crossStrategy)) {
    foreach ($crossStrategy as $cs) {
        $sid = $cs['strategy_id'];
        $events = StrategyChangeHistory::fetchEvents($db, $sid);
        if (empty($events)) continue; // §27: no changes recorded, nothing to compare

        $firstChangeAt = $events[0]['changed_at'];
        $lastChangeAt  = $events[count($events) - 1]['changed_at'];

        $fetchSegment = function (string $cmp, string $boundary) use ($db, $userId, $pnlExpr, $avTypeFilter, $sid) {
            $rows = $db->raw(
                "SELECT COUNT(*) AS trades, SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins, SUM($pnlExpr) AS total_pnl, AVG(t.risk_pct) AS avg_risk_pct
                 FROM trades t
                 JOIN strategy_versions sv2 ON sv2.id = t.strategy_version_id
                 LEFT JOIN trading_accounts ta ON ta.id = t.account_id
                 WHERE t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL
                   AND sv2.strategy_id = :sid AND t.captured_at $cmp :boundary {$avTypeFilter}",
                [':uid' => $userId, ':sid' => $sid, ':boundary' => $boundary]
            );
            return $rows[0] ?? [];
        };
        $origData = $fetchSegment('<', $firstChangeAt);
        $currData = $fetchSegment('>=', $lastChangeAt);

        $origTrades  = (int)($origData['trades']    ?? 0); $currTrades  = (int)($currData['trades']    ?? 0);
        $origWins    = (int)($origData['wins']      ?? 0); $currWins    = (int)($currData['wins']      ?? 0);
        $origPnl     = round((float)($origData['total_pnl'] ?? 0), 2); $currPnl = round((float)($currData['total_pnl'] ?? 0), 2);
        $origAvgRisk = round((float)($origData['avg_risk_pct'] ?? 0), 2); $currAvgRisk = round((float)($currData['avg_risk_pct'] ?? 0), 2);
        $origWinRate = $origTrades > 0 ? round(($origWins / $origTrades) * 100, 1) : 0;
        $currWinRate = $currTrades > 0 ? round(($currWins / $currTrades) * 100, 1) : 0;
        $origExpectancy = $origTrades > 0 ? round($origPnl / $origTrades, 2) : 0;
        $currExpectancy = $currTrades > 0 ? round($currPnl / $currTrades, 2) : 0;

        $fetchChecklistPct = function (string $cmp, string $boundary) use ($db, $userId, $avAccountIdFilter, $sid) {
            try {
                $rows = $db->raw(
                    "SELECT AVG(cr.ticked_pct) AS avg_ticked_pct FROM checklist_results cr
                     JOIN trades t ON t.trade_lifecycle_id = cr.trade_lifecycle_id
                     JOIN strategy_versions sv2 ON sv2.id = t.strategy_version_id
                     WHERE t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL
                       AND sv2.strategy_id = :sid AND t.captured_at $cmp :boundary {$avAccountIdFilter}",
                    [':uid' => $userId, ':sid' => $sid, ':boundary' => $boundary]
                );
                return round((float)($rows[0]['avg_ticked_pct'] ?? 0), 1);
            } catch (\Throwable $e) { return 0.0; }
        };
        $origChecklist = $fetchChecklistPct('<', $firstChangeAt);
        $currChecklist = $fetchChecklistPct('>=', $lastChangeAt);

        $winRateGap    = $currWinRate - $origWinRate;
        $expectancyGap = $currExpectancy - $origExpectancy;
        $pnlGapEvo     = $currPnl - $origPnl;
        $riskGapEvo    = $currAvgRisk - $origAvgRisk;
        $checklistGap  = $currChecklist - $origChecklist;

        // §27/§31: no post-change trades yet, or no pre-change trades to
        // compare against, state that plainly rather than claiming an
        // improvement/decline verdict with nothing behind it. §31: an
        // observed relationship, never a causal claim ("performance
        // declined after the change", never "the change caused it").
        if ($origTrades === 0 || $currTrades === 0) {
            $verdict = $currTrades === 0
                ? 'No trades yet since the most recent change, nothing to compare against the original declaration.'
                : 'No trades were recorded under the original declaration, nothing to compare the current declaration against.';
        } elseif ($winRateGap > 0 && $expectancyGap > 0)  $verdict = 'Win rate and expectancy both improved after the most recent change.';
        elseif ($winRateGap <= 0 && $expectancyGap <= 0)  $verdict = 'Win rate and expectancy both declined after the most recent change, worth reviewing.';
        else                                                $verdict = 'Mixed, win rate and expectancy moved in opposite directions after the most recent change.';

        $strategyEvolutionData[] = [
            'strategy_name'   => $cs['name'],
            'strategy_id'     => $sid,
            'change_count'    => count($events),
            'change_events'   => $events,
            'narration'       => StrategyChangeHistory::narrate($events),
            'metrics' => [
                ['label'=>'Trades',        'original'=>$origTrades,                'current'=>$currTrades,                'gap'=>($currTrades - $origTrades >= 0 ? '+' : '') . ($currTrades - $origTrades)],
                ['label'=>'Win Rate',       'original'=>$origWinRate.'%',           'current'=>$currWinRate.'%',           'gap'=>($winRateGap >= 0 ? '+' : '') . round($winRateGap, 1) . '%'],
                ['label'=>'Total P&L',      'original'=>'$'.number_format($origPnl,2), 'current'=>'$'.number_format($currPnl,2), 'gap'=>($pnlGapEvo >= 0 ? '+' : '') . '$' . number_format($pnlGapEvo, 2)],
                ['label'=>'Average Risk %', 'original'=>$origAvgRisk.'%',           'current'=>$currAvgRisk.'%',           'gap'=>($riskGapEvo >= 0 ? '+' : '') . round($riskGapEvo, 2) . '%'],
                ['label'=>'Expectancy',     'original'=>pnlFormat($origExpectancy), 'current'=>pnlFormat($currExpectancy), 'gap'=>($expectancyGap >= 0 ? '+' : '') . pnlFormat($expectancyGap)],
                ['label'=>'Checklist %',    'original'=>$origChecklist.'%',         'current'=>$currChecklist.'%',         'gap'=>($checklistGap >= 0 ? '+' : '') . round($checklistGap, 1) . '%'],
            ],
            'verdict'             => $verdict,
            'has_comparison'      => $origTrades > 0 && $currTrades > 0,
            'win_rate_improved'   => $winRateGap > 0,
            'expectancy_improved' => $expectancyGap > 0,
        ];
    }
}

// (Removed) Four-Stage Trade Comparison, AGGREGATED. This reimplemented
// Stage 1-4 independently of computeFourStageRaw()/getFourStagePeriodSummary()
// (its own strategy_versions read for Stage 1, its own averaging queries for
// Stage 2/3) and its 'stage1'..'stage4' output was never rendered anywhere —
// only $executionScore (Strategy Health accordion header) escaped this block,
// via a side-effect assignment inside it. Execution Score is now computed
// below, straight from $trueFourStageSummary (the same single source of
// truth Journal Trades' per-trade 4-Stage view uses), so it can't drift from
// the real Stage 1/2 numbers the way this parallel copy could.

$svidParams = $selectedVersionId ? [':svid' => $selectedVersionId] : [];

// ════════════════════════════════════════════════════════════════════
// TRUE 4-Stage Performance Engine, one section per reporting period,
// built from ALL trades in that period. Single source of truth: calls
// AnalyticsEngine::getFourStagePeriodSummary(), which internally reuses
// the exact same per-trade computeFourStageRaw() logic that powers
// Journal Trades' per-trade 4-Stage view, never a separate
// reimplementation, so the two can't drift apart.
// ════════════════════════════════════════════════════════════════════
try {
    $trueFourStageSummary = AnalyticsEngine::getFourStagePeriodSummary(
        $userId, $accountView, $startStr, $endStr, $svidCondition, $svidParams, $scopedAccountId ?: null
    );
} catch (\Throwable $e) {
    $trueFourStageSummary = ['trades_in_period' => 0];
}

// Execution Score (Strategy Health accordion header, Elite/Ultimate only) —
// green/amber/red per metric against declared edge, same thresholds as the
// removed reimplementation above. Sourced from $trueFourStageSummary's
// stage1 (declared risk %, declared R:R, both simulated per-trade off the
// strategy's edge, per computeFourStageRaw()) and stage2 (what was actually
// planned), plus the page's already-loaded $declaredBacktestedWinRate/
// $totalWinRate (backtested win rate isn't part of the per-trade Stage 1
// simulation, it's a flat strategy-level constant, so it stays read
// directly from strategy_versions like before).
if ($totalTrades > 0 && $selectedVersionId && !empty($strategyVersionData) && (int)($trueFourStageSummary['trades_in_period'] ?? 0) > 0) {
    $avgPlannedRisk = $trueFourStageSummary['stage2']['avg_risk_pct'] !== null ? round((float)$trueFourStageSummary['stage2']['avg_risk_pct'], 2) : null;
    $avgPlannedRr   = $trueFourStageSummary['stage2']['avg_risk_reward'] !== null ? round((float)$trueFourStageSummary['stage2']['avg_risk_reward'], 2) : null;

    $riskGap = ($avgPlannedRisk !== null) ? round($avgPlannedRisk - $declaredMaxRiskPct, 2) : null;
    $riskGapColor = $riskGap === null ? 'neutral' : ($riskGap <= 0 ? 'green' : (abs($riskGap) <= 0.5 ? 'amber' : 'red'));

    $winRateGap = ($declaredBacktestedWinRate !== null) ? round($totalWinRate - $declaredBacktestedWinRate, 1) : null;
    $winRateGapColor = $winRateGap === null ? 'neutral' : ($winRateGap >= 0 ? 'green' : (abs($winRateGap) <= 10 ? 'amber' : 'red'));

    $rrGap = ($declaredBacktestedAvgRr !== null && $avgPlannedRr !== null) ? round($avgPlannedRr - $declaredBacktestedAvgRr, 2) : null;
    $rrGapColor = $rrGap === null ? 'neutral' : ($rrGap >= 0 ? 'green' : (abs($rrGap) <= 0.3 ? 'amber' : 'red'));

    $scoreChecks = array_filter([$riskGapColor, $winRateGapColor, $rrGapColor], fn($c) => $c !== 'neutral');
    $greenChecks = count(array_filter($scoreChecks, fn($c) => $c === 'green'));
    $executionScore = count($scoreChecks) > 0 ? round(($greenChecks / count($scoreChecks)) * 100) : 0;
}
// $executionScore above is now superseded by Edge Health (below) as this
// page's headline "how good is my edge" metric, per the Reports 4-Stage
// rebuild. Left computed rather than deleted, per the additive-only rule,
// but no longer rendered anywhere, same "orphaned, confirmed unreachable,
// left in place" pattern already used in engine.php for the dead Stage 4/5
// deviation methods.

// ════════════════════════════════════════════════════════════════════
// 4-STAGE PERFORMANCE AUDIT, Edge Health → Trade Set → Commentary →
// Trade Highlights → Classification Analysis → Instrument Analysis →
// Boundary Analysis → Recommendations. ("Most Deviated Trade" and
// "Behaviour Analysis" were consolidated into one "Trade Highlights" card
//, see CLAUDE.md's 2026-07-26 session entry.)
// The "4-Stage Audit Table" step and the separate "Performance Journey"
// section (formerly below, driven by the now-deleted
// four_stage_progressive_engine.php) were both removed in a later
// deletion-only pass, see CLAUDE.md for what was removed and why.
// ════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/../api/analytics/edge_health_engine.php';

$edgeHealth = EdgeHealthEngine::getScore($userId, $selectedVersionId);

$styleDrift = $selectedVersionId
    ? AnalyticsEngine::getStyleDriftSignal($selectedVersionId, $startStr, $endStr)
    : ['available' => false];

// Accounts that actually contributed a closed, non-validation trade to THIS
// period, the account-scope dropdown (4-Stage Summary section below) is
// scoped to these, not every account the user has ever opened. Computed for
// both account types (prop and live) since the 4-Stage Summary section
// renders under either tab; the pre-existing prop-only version fed the old
// deleted Trade Set card and is kept exactly as-is for that reason.
$contribIds = $db->raw(
    "SELECT DISTINCT account_id FROM trades
     WHERE user_id = :uid AND account_id IS NOT NULL AND status = 'closed' AND deleted_at IS NULL
     AND (is_validation = 0 OR is_validation IS NULL)
     AND closed_at >= :s AND closed_at <= :e",
    [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]
);
$contribIdSet = array_map('strval', array_column($contribIds, 'account_id'));

$periodContributingPropAccounts = [];
if (!empty($propAccounts)) {
    foreach ($propAccounts as $pa) {
        if (in_array((string)$pa['id'], $contribIdSet, true)) $periodContributingPropAccounts[] = $pa;
    }
}
$periodContributingLiveAccounts = [];
if (!empty($liveAccounts)) {
    foreach ($liveAccounts as $la) {
        if (in_array((string)$la['id'], $contribIdSet, true)) $periodContributingLiveAccounts[] = $la;
    }
}

// Commentary, plain-language read of the audit numbers above, generated
// directly from $trueFourStageSummary + $styleDrift; simple enough not to
// need a separate narrative engine call.
// Item 6 (2026-07-28 refinement pass): each commentary line is now
// ['text' => ..., 'url' => ...|null] instead of a bare string, so the
// render below can attach a real "View Trades" link wherever the
// sentence names an inspectable subset. Only the over-risk line gets one
//, it reuses the exact same approximate (period-average declared-risk
// threshold, not the true per-instrument tolerance) link already used by
// the Intent vs Reality card's Over-Risk Occurrences tile, so this isn't
// a new, looser standard. The R:R/risk% geometric-mismatch lines and
// contract-constrained trades have NO stored column or existing filter
// that identifies them (geometric_consistency and contract_constrained
// are both computed live per-trade in engine.php, never persisted), a
// truly accurate Journal filter for either would need real filtering
// work, not just a link, so per the "improve the filtering logic first"
// principle those stay unlinked rather than getting a fabricated
// approximate link with no real relationship to the underlying check.
$auditCommentary = [];
if ((int)($trueFourStageSummary['trades_in_period'] ?? 0) > 0) {
    $tS1 = $trueFourStageSummary['stage1']; $tGc = $trueFourStageSummary['geometric_consistency'];
    if ($tS1['trades_over_declared_risk'] > 0) {
        $auditCommentary[] = [
            'text' => $tS1['trades_over_declared_risk'] . ' of ' . (int)$trueFourStageSummary['trades_in_period'] . ' trades this ' . periodNoun($period) . ' risked more than the strategy\'s declared limit' . ($tS1['avg_risk_overage'] !== null ? ' (average overage ' . number_format($tS1['avg_risk_overage'], 2) . '%)' : '') . '.',
            'url' => ($tS1['avg_declared_risk_pct'] !== null) ? journalTradesFilterUrl(['account_type' => match ($accountView) { 'prop' => 'prop_firm', 'live' => 'personal', default => null }, 'min_risk_pct' => $tS1['avg_declared_risk_pct'], 'from' => $startDateOnly, 'to' => $endDateOnly]) : null,
        ];
    }
    if ($tGc['rr_mismatch_count'] > 0) {
        $auditCommentary[] = ['text' => $tGc['rr_mismatch_count'] . ' trade(s) (' . $tGc['rr_mismatch_pct'] . '%) had a saved reward:risk that doesn\'t geometrically match their own entry/SL/TP prices' . ($tGc['avg_rr_mismatch_magnitude'] !== null ? ' (risking $1 to target $' . number_format($tGc['avg_rr_mismatch_magnitude'], 2) . ' off on average)' : '') . '.', 'url' => null];
    }
    if ($tGc['risk_pct_mismatch_count'] > 0) {
        $auditCommentary[] = ['text' => $tGc['risk_pct_mismatch_count'] . ' trade(s) (' . $tGc['risk_pct_mismatch_pct'] . '%) had a saved risk % that doesn\'t match what the dollar risk actually implies.', 'url' => null];
    }
    if (!empty($tS1['trades_contract_constrained'])) {
        $auditCommentary[] = ['text' => $tS1['trades_contract_constrained'] . ' trade(s) (' . $tS1['contract_constrained_pct'] . '%) had no valid lot size on their instrument capable of hitting the declared risk target, a contract limitation, not over-risking, and excluded from the count above.', 'url' => null];
    }
    if (!empty($styleDrift['available']) && !empty($styleDrift['hold_time_drift_note'])) $auditCommentary[] = ['text' => $styleDrift['hold_time_drift_note'], 'url' => null];
    if (!empty($styleDrift['available']) && !empty($styleDrift['tp_distance_drift_note'])) $auditCommentary[] = ['text' => $styleDrift['tp_distance_drift_note'], 'url' => null];
    if (empty($auditCommentary)) $auditCommentary[] = ['text' => 'Execution stayed consistent with the declared plan this ' . periodNoun($period) . ', no risk overages or geometric mismatches to flag.', 'url' => null];
}

// Recommendations, lightweight, rule-based bullets generated directly from
// the audit numbers above. NOT the larger, separate Recommendations module
// described elsewhere in this file (recommendation_context.php,
// resolve_reporting_period_window()), that remains a distinct, not-yet-
// built feature; this is a small, self-contained summary scoped to what
// this one audit table already found.
// Item 6 (2026-07-28): same ['text'=>..,'url'=>..] shape as $auditCommentary
// above, for the same reason, a recommendation that names a trade count
// should let the user go inspect those exact trades.
$auditRecommendations = [];
if ((int)($trueFourStageSummary['trades_in_period'] ?? 0) > 0) {
    $tS1 = $trueFourStageSummary['stage1']; $tGc = $trueFourStageSummary['geometric_consistency'];
    if ($tS1['trades_over_declared_risk'] > 0) $auditRecommendations[] = [
        'text' => 'Bring position sizing back under the declared risk % on future trades, ' . $tS1['trades_over_declared_risk'] . ' trade(s) exceeded it this ' . periodNoun($period) . '.',
        'url' => ($tS1['avg_declared_risk_pct'] !== null) ? journalTradesFilterUrl(['account_type' => match ($accountView) { 'prop' => 'prop_firm', 'live' => 'personal', default => null }, 'min_risk_pct' => $tS1['avg_declared_risk_pct'], 'from' => $startDateOnly, 'to' => $endDateOnly]) : null,
    ];
    if ($tGc['rr_mismatch_pct'] > 20) $auditRecommendations[] = ['text' => 'Double-check saved R:R values against actual entry/SL/TP prices before confirming a trade, mismatches were common this ' . periodNoun($period) . '.', 'url' => null];
    if (!empty($styleDrift['hold_time_drift_note'])) $auditRecommendations[] = ['text' => 'Hold times are drifting from the declared trading style, revisit whether the strategy definition still matches how it\'s actually being traded.', 'url' => null];
    // 2026-07-28 item 4: gated on a real sample (small_sample/developing/
    // confident, n>=20), not just $edgeHealth['gradeable'] (which now
    // means n>=1 as of the Edge Health redesign), a 2-trade score swings
    // wildly and shouldn't generate a confident-sounding recommendation.
    if ($depth >= 3 && !empty($edgeHealth['gradeable']) && !in_array($edgeHealth['confidence_tier'] ?? '', ['single_trade', 'very_small_sample'], true) && $edgeHealth['score'] < 55) $auditRecommendations[] = [
        'text' => 'Edge Health is below average, see the weakest factors above for where to focus.',
        // Edge Health is all-time, not period-scoped, the link matches
        // that same all-time/strategy-only scope, no date filter.
        'url' => $selectedVersionId ? journalTradesFilterUrl(['strategy_version_id' => $selectedVersionId]) : null,
    ];
    if (empty($auditRecommendations)) $auditRecommendations[] = ['text' => 'No specific corrections flagged this ' . periodNoun($period) . ', keep executing as planned.', 'url' => null];
}

// ════════════════════════════════════════════════════════════════════
// Prop Firm Analytics, AGGREGATED (new top-level accordion)
// ─────────────────────────────────────────────────────────────────────
// Built directly from trading_accounts + challenge_accounts only, no
// engine.php dependency. Never lists all accounts: combined totals plus
// top-8 best / top-8 worst / top-8 most-severely-breached call-outs.
// Breach = lifecycle_state = 'breached' OR breached_at IS NOT NULL.
// Pass   = passed_at IS NOT NULL (challenge_accounts is the source of
// truth for pass/breach timestamps; trading_accounts.lifecycle_state is
// used only as a secondary signal, matching what was confirmed).
// ════════════════════════════════════════════════════════════════════
$propFirmAgg = null;
if ($accountView === 'prop') {
    // Combined P&L, SUM(challenge_pnl) across every closed trade on prop
    // firm accounts, CLOSED WITHIN THE SELECTED REPORTING PERIOD. Trade-
    // derived metrics are always period-scoped (P&L, rankings); account
    // lifecycle state (breached/passed/funded counts below) is a
    // lifecycle truth and stays all-time, confirmed design decision.
    // Computed as its own isolated query (not mixed into the counts query
    // below) because that query LEFT JOINs challenge_accounts, and an
    // account with multiple challenge attempts would otherwise fan out
    // and double-count trades.
    // Same missing-fallback bug class as $pnlExpr above, found in a
    // dedicated audit of every raw challenge_pnl/broker_pnl read on this
    // page: this query bypassed $pnlExpr entirely with its own bare
    // SUM(t.challenge_pnl), so a prop-firm trade with challenge_pnl NULL
    // silently contributed 0 instead of its real broker_pnl. This query is
    // already scoped to account_type = 'prop_firm' only (see WHERE below),
    // but the CASE branch is kept anyway to match $pnlExpr's exact formula
    // rather than a shortened equivalent, one less thing to keep in sync
    // if this query is ever copied somewhere account_type isn't pre-filtered.
    $propCombinedPnlRow = $db->raw(
        "SELECT COALESCE(SUM(CASE WHEN ta.account_type = 'prop_firm' THEN COALESCE(t.challenge_pnl, t.broker_pnl, t.projected_pnl, 0) ELSE COALESCE(t.broker_pnl, t.projected_pnl, 0) END), 0) AS combined_pnl
         FROM trades t
         JOIN trading_accounts ta ON ta.id = t.account_id
         WHERE ta.user_id = :uid AND ta.account_type = 'prop_firm' AND ta.deleted_at IS NULL
         AND t.status = 'closed' AND t.deleted_at IS NULL AND (t.is_validation = 0 OR t.is_validation IS NULL)
         AND t.closed_at >= :s AND t.closed_at <= :e",
        [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]
    );
    $propCombinedPnl = round((float)($propCombinedPnlRow[0]['combined_pnl'] ?? 0), 2);

    $propSummaryRow = $db->raw(
        "SELECT
            COUNT(DISTINCT ta.id) AS total_accounts,
            COUNT(DISTINCT CASE WHEN ta.lifecycle_state = 'breached' OR ca.breached_at IS NOT NULL THEN ta.id END) AS breached_accounts,
            COUNT(DISTINCT CASE WHEN ca.passed_at IS NOT NULL THEN ta.id END) AS passed_accounts,
            COUNT(DISTINCT CASE WHEN ta.lifecycle_state LIKE '%funded%' THEN ta.id END) AS funded_accounts,
            COUNT(DISTINCT CASE WHEN ca.challenge_type = 'instant_funding' THEN ta.id END) AS instant_funding_accounts,
            AVG(CASE WHEN ca.breached_at IS NOT NULL THEN DATEDIFF(ca.breached_at, ta.created_at) END) AS avg_days_to_breach,
            AVG(CASE WHEN ca.passed_at IS NOT NULL THEN DATEDIFF(ca.passed_at, ta.created_at) END) AS avg_days_to_pass,
            COUNT(DISTINCT CASE WHEN ca.breached_at IS NOT NULL THEN ta.id END) AS breached_with_date,
            COUNT(DISTINCT CASE WHEN ca.breached_at IS NOT NULL AND DATEDIFF(ca.breached_at, ta.created_at) = 0 THEN ta.id END) AS breached_same_day,
            COUNT(DISTINCT CASE WHEN ca.passed_at IS NOT NULL THEN ta.id END) AS passed_with_date,
            COUNT(DISTINCT CASE WHEN ca.passed_at IS NOT NULL AND DATEDIFF(ca.passed_at, ta.created_at) = 0 THEN ta.id END) AS passed_same_day
         FROM trading_accounts ta
         LEFT JOIN challenge_accounts ca ON ca.account_id = ta.id
         WHERE ta.user_id = :uid AND ta.account_type = 'prop_firm' AND ta.deleted_at IS NULL",
        [':uid' => $userId]
    );
    $propSummary = $propSummaryRow[0] ?? [];
    $propTotalAccounts = (int)($propSummary['total_accounts'] ?? 0);

    if ($propTotalAccounts > 0) {
        $propByType = $db->raw(
            "SELECT ca.challenge_type,
                COUNT(DISTINCT ta.id) AS accounts,
                COUNT(DISTINCT CASE WHEN ta.lifecycle_state = 'breached' OR ca.breached_at IS NOT NULL THEN ta.id END) AS breached,
                COUNT(DISTINCT CASE WHEN ca.passed_at IS NOT NULL THEN ta.id END) AS passed
             FROM trading_accounts ta
             JOIN challenge_accounts ca ON ca.account_id = ta.id
             WHERE ta.user_id = :uid AND ta.account_type = 'prop_firm' AND ta.deleted_at IS NULL
             GROUP BY ca.challenge_type
             ORDER BY accounts DESC",
            [':uid' => $userId]
        );

        // Best/Worst/Most-Breached P&L is a correlated subquery summing
        // challenge_pnl per account, CLOSED WITHIN THE SELECTED REPORTING
        // PERIOD, this is a profitability ranking, which is always
        // trade-scoped per the confirmed design decision. This is what the
        // trader actually made or lost through trading in this period, not
        // just the current balance vs. starting balance (which can differ
        // due to deposits, fees, or resets), and not an all-time total
        // (which would make "best this month" mean the same thing on
        // every period tab). Note: WHICH accounts qualify as "most
        // breached" is a lifecycle fact and stays all-time (unchanged
        // below), only the P&L used to rank/display them is period-scoped.
        // Same fix as $propCombinedPnlRow above, was a bare SUM(pt.challenge_pnl)
        // with no fallback. ta.account_type is in scope here as a correlated
        // reference to the outer query's trading_accounts row.
        $propPnlSubquery = "(SELECT COALESCE(SUM(CASE WHEN ta.account_type = 'prop_firm' THEN COALESCE(pt.challenge_pnl, pt.broker_pnl, pt.projected_pnl, 0) ELSE COALESCE(pt.broker_pnl, pt.projected_pnl, 0) END), 0) FROM trades pt WHERE pt.account_id = ta.id AND pt.status = 'closed' AND pt.deleted_at IS NULL AND (pt.is_validation = 0 OR pt.is_validation IS NULL) AND pt.closed_at >= :s AND pt.closed_at <= :e)";
        // Same fan-out-safe pattern as $propAccounts above, exactly one
        // (the most recent) challenge_type per account, needed to route
        // "View Trades" links to the correct Journal zone/category.
        $propChallengeTypeSubquery = "(SELECT ca2.challenge_type FROM challenge_accounts ca2 WHERE ca2.account_id = ta.id ORDER BY ca2.attempt_number DESC, ca2.created_at DESC LIMIT 1)";

        $propBestAccounts = $db->raw(
            "SELECT ta.id, ta.label, ta.starting_balance, ta.current_balance, ta.lifecycle_state,
                {$propPnlSubquery} AS pnl, {$propChallengeTypeSubquery} AS challenge_type
             FROM trading_accounts ta
             WHERE ta.user_id = :uid AND ta.account_type = 'prop_firm' AND ta.deleted_at IS NULL
             HAVING pnl <> 0
             ORDER BY pnl DESC LIMIT 8",
            [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]
        );
        // Rank explanation, a breached account legitimately outranking a
        // still-active one is correct (a trader can produce exceptional
        // returns before eventually breaching), but the page previously
        // gave no reason why, which reads as an unexplained inconsistency.
        // Generated from data already on the row, no extra query.
        foreach ($propBestAccounts as &$pbRank) {
            $pbPnl = (float)$pbRank['pnl'];
            $pbState = $pbRank['lifecycle_state'] ?? '';
            if ($pbState === 'breached') {
                $pbRank['rank_reason'] = 'Breached, but produced ' . pnlFormat($pbPnl) . ' in trading this ' . periodNoun($period) . ' before the breach, ranked here for trading performance, not final outcome.';
            } elseif (strpos($pbState, '_complete') !== false) {
                $pbRank['rank_reason'] = 'Passed this stage with ' . pnlFormat($pbPnl) . ' this ' . periodNoun($period) . ', one of the strongest results among your prop accounts.';
            } elseif ($pbState === 'funded_active') {
                $pbRank['rank_reason'] = 'Funded and actively trading, ' . pnlFormat($pbPnl) . ' this ' . periodNoun($period) . '.';
            } elseif (strpos($pbState, 'phase_') !== false) {
                $pbRank['rank_reason'] = 'Currently in an active challenge phase, ranks highly on ' . pnlFormat($pbPnl) . ' in trading this ' . periodNoun($period) . '.';
            } else {
                $pbRank['rank_reason'] = 'Ranks here based on ' . pnlFormat($pbPnl) . ' in trading this ' . periodNoun($period) . '.';
            }
        }
        unset($pbRank);
        // "Accounts Needing Attention", this used to just be $propWorstAccounts
        // (worst 8 by P&L) rendered under a misleading title, which is exactly
        // why a profitable account with zero drawdown could show up here: a
        // small account being the "least profitable of the bunch" has nothing
        // to do with actually being at risk. The real rule (per explicit spec):
        // an account only qualifies if its current overall drawdown is at or
        // past HALF of its max allowed drawdown, OR its current day's loss is
        // at or past HALF of its daily drawdown limit, both sourced from
        // getChallengeMetrics()'s real per-account percentages (the same
        // computation already used by Boundary Analytics above), not P&L
        // ranking. Already-breached accounts are excluded (they're covered by
        // "Most Severely Breached" below).
        $propAttentionAccounts = [];
        foreach ($propAccounts as $paAttn) {
            if ($paAttn['lifecycle_state'] === 'breached') continue;
            // Closed accounts can't be traded, so they can't "need
            // attention", same principle as the Live side's fix below. No
            // real prop_firm account currently has lifecycle_state='closed'
            // in this database, but the guard is added for correctness
            // rather than relying on that happening to stay true.
            if ($paAttn['lifecycle_state'] === 'closed') continue;
            try { $cmAttn = $analyticsEngine->getChallengeMetrics($paAttn['id']); }
            catch (\Throwable $e) { continue; }
            if (!$cmAttn || isset($cmAttn['error'])) continue;

            $overallCurPct = (float)arrVal($cmAttn['overall_drawdown'] ?? [], ['current_percentage'], 0);
            $overallLimPct = (float)arrVal($cmAttn['overall_drawdown'] ?? [], ['limit_percentage'], 0);
            $dailyCurPct   = (float)arrVal($cmAttn['daily_drawdown'] ?? [], ['current_percentage'], 0);
            $dailyLimPct   = (float)arrVal($cmAttn['daily_drawdown'] ?? [], ['limit_percentage'], 0);

            $overallRatio = $overallLimPct > 0 ? $overallCurPct / $overallLimPct : 0;
            $dailyRatio   = $dailyLimPct > 0 ? $dailyCurPct / $dailyLimPct : 0;

            if ($overallRatio >= 0.5 || $dailyRatio >= 0.5) {
                $reasonParts = [];
                if ($overallRatio >= 0.5) $reasonParts[] = round($overallCurPct, 1) . '% of ' . round($overallLimPct, 1) . '% max drawdown used';
                if ($dailyRatio >= 0.5)   $reasonParts[] = round($dailyCurPct, 1) . '% of ' . round($dailyLimPct, 1) . '% daily loss limit used today';
                $propAttentionAccounts[] = [
                    'id' => $paAttn['id'], 'label' => $paAttn['label'], 'lifecycle_state' => $paAttn['lifecycle_state'],
                    'challenge_type' => $paAttn['challenge_type'] ?? null,
                    'reason' => implode('; ', $reasonParts),
                    'worst_ratio' => max($overallRatio, $dailyRatio),
                ];
            }
        }
        usort($propAttentionAccounts, fn($a, $b) => $b['worst_ratio'] <=> $a['worst_ratio']);
        $propAttentionAccounts = array_slice($propAttentionAccounts, 0, 8);

        // Investigated (2026-07-27): every row previously showed the exact
        // same -$400.00 P&L, confirmed NOT a computation bug. challenge_pnl
        // is deliberately CAPPED at the breach boundary (see the Lifecycle
        // Rule / resolve_trade_id above), a $5,000 account with an 8% max
        // drawdown has a $400 breach level, and most of this DB's synthetic
        // breach-test accounts share exactly those parameters with a single
        // breaching trade, so their capped challenge_pnl converges on the
        // identical -$400 cap by design, not by accident (verified live: a
        // direct SQL cross-check found trades.broker_pnl - trades.excess_pnl
        // = trades.challenge_pnl exactly on every breached account checked,
        // e.g. broker_pnl -499.46 = challenge_pnl -400.00 + excess_pnl
        // -99.46). The capped "P&L" column is correct and left intact per
        // instruction. What was actually missing: the real, UNCAPPED result
        // (broker_pnl) and the breach threshold itself, neither of which
        // this table exposed, added as breach_payload/breach_level below.
        // Also re-scoped the ranking itself: it previously ORDER BY'd a
        // period-scoped pnl column while SELECTing from an all-time breach
        // filter, an account that breached outside the currently-viewed
        // period shows $0 period P&L and would rank arbitrarily. Breach
        // severity is a one-time lifecycle fact (same "account facts stay
        // all-time" convention as Boundary Analysis elsewhere on this page),
        // so ranking now uses the real, all-time breach_payload instead.
        $propBreachPayloadSubquery = "(SELECT COALESCE(SUM(COALESCE(pt2.broker_pnl, pt2.projected_pnl, 0)), 0) FROM trades pt2 WHERE pt2.account_id = ta.id AND pt2.status = 'closed' AND pt2.deleted_at IS NULL AND (pt2.is_validation = 0 OR pt2.is_validation IS NULL))";
        $propMostBreached = $db->raw(
            "SELECT ta.id, ta.label, ta.starting_balance, ta.current_balance, ta.lifecycle_state,
                {$propPnlSubquery} AS pnl, {$propChallengeTypeSubquery} AS challenge_type,
                {$propBreachPayloadSubquery} AS breach_payload
             FROM trading_accounts ta
             LEFT JOIN challenge_accounts ca ON ca.account_id = ta.id
             WHERE ta.user_id = :uid AND ta.account_type = 'prop_firm' AND ta.deleted_at IS NULL
             AND (ta.lifecycle_state = 'breached' OR ca.breached_at IS NOT NULL)
             GROUP BY ta.id, ta.label, ta.starting_balance, ta.current_balance, ta.lifecycle_state
             ORDER BY breach_payload ASC LIMIT 8",
            [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]
        );
        // Breach Level, the actual dollar threshold this account's rules
        // set (e.g. $400 on a $5,000/8%-max-DD account), sourced from
        // getChallengeMetrics()'s overall_drawdown.limit_amount (already the
        // single source of truth for this figure everywhere else on this
        // page, reused, not reimplemented). Bounded to the final 8 rows,
        // same per-account-call pattern already used by $propAttentionAccounts.
        foreach ($propMostBreached as &$pmbLvl) {
            try { $cmLvl = $analyticsEngine->getChallengeMetrics($pmbLvl['id']); }
            catch (\Throwable $e) { $cmLvl = null; }
            $pmbLvl['breach_level'] = (!$cmLvl || isset($cmLvl['error']))
                ? null
                : (float)arrVal($cmLvl['overall_drawdown'] ?? [], ['limit_amount'], 0);
        }
        unset($pmbLvl);

        $propBreachedAccounts = (int)($propSummary['breached_accounts'] ?? 0);
        $propPassedAccounts   = (int)($propSummary['passed_accounts'] ?? 0);

        $propFirmAgg = [
            'total_accounts'          => $propTotalAccounts,
            'combined_pnl'            => $propCombinedPnl,
            'breached_accounts'       => $propBreachedAccounts,
            'passed_accounts'         => $propPassedAccounts,
            'active_accounts'         => max(0, $propTotalAccounts - $propBreachedAccounts - $propPassedAccounts),
            'funded_accounts'         => (int)($propSummary['funded_accounts'] ?? 0),
            'instant_funding_accounts'=> (int)($propSummary['instant_funding_accounts'] ?? 0),
            'breach_rate_pct'         => $propTotalAccounts > 0 ? round(($propBreachedAccounts / $propTotalAccounts) * 100, 1) : 0,
            'pass_rate_pct'           => $propTotalAccounts > 0 ? round(($propPassedAccounts / $propTotalAccounts) * 100, 1) : 0,
            'avg_days_to_breach'      => $propSummary['avg_days_to_breach'] !== null ? round((float)$propSummary['avg_days_to_breach'], 1) : null,
            'avg_days_to_pass'        => $propSummary['avg_days_to_pass']   !== null ? round((float)$propSummary['avg_days_to_pass'], 1)   : null,
            // Data-quality context so a near-zero average doesn't read as
            // broken, it shows exactly how many accounts had a recorded
            // breach/pass date, and how many of those were same-day as
            // account creation (which is what actually pulls the average
            // toward zero, e.g. backfilled/demo data created and
            // breached/passed on the same day).
            'breached_with_date'      => (int)($propSummary['breached_with_date'] ?? 0),
            'breached_same_day'       => (int)($propSummary['breached_same_day'] ?? 0),
            'passed_with_date'        => (int)($propSummary['passed_with_date'] ?? 0),
            'passed_same_day'         => (int)($propSummary['passed_same_day'] ?? 0),
            'by_type'                 => $propByType,
            'best'                    => $propBestAccounts,
            'needs_attention'         => $propAttentionAccounts,
            'most_breached'           => $propMostBreached,
        ];
    }
}

// ════════════════════════════════════════════════════════════════════
// Live Account Analytics, AGGREGATED (new top-level accordion)
// ─────────────────────────────────────────────────────────────────────
// Built directly from trading_accounts only. Same aggregation discipline:
// combined totals, never an unbounded list, top-8 caps only.
// ════════════════════════════════════════════════════════════════════
$liveAccountAgg = null;
if ($accountView === 'live') {
    // Combined P&L, SUM(broker_pnl) across every closed trade on personal
    // (live) accounts, CLOSED WITHIN THE SELECTED REPORTING PERIOD —
    // trade-derived metric, always period-scoped per confirmed design
    // decision. Computed as its own isolated query for clarity and
    // consistency with the Prop Firm combined P&L above.
    //
    // Fixed: personal accounts never touch challenge_pnl (correctly, no
    // CASE branch needed here), but this was still a bare SUM(broker_pnl)
    // with no per-row fallback to projected_pnl if broker_pnl itself is
    // NULL on a given trade, found in the same audit that caught the
    // Prop Firm Combined P&L bug, though checked live at the time and
    // confirmed 0 of 26 closed personal-account trades were actually
    // affected. Fixed anyway to match the same COALESCE(broker_pnl,
    // projected_pnl, 0) cascade used everywhere else this session, so the
    // formula itself is correct rather than merely "currently harmless."
    $liveCombinedPnlRow = $db->raw(
        "SELECT COALESCE(SUM(COALESCE(t.broker_pnl, t.projected_pnl, 0)), 0) AS combined_pnl
         FROM trades t
         JOIN trading_accounts ta ON ta.id = t.account_id
         WHERE ta.user_id = :uid AND ta.account_type = 'personal' AND ta.deleted_at IS NULL
         AND t.status = 'closed' AND t.deleted_at IS NULL AND (t.is_validation = 0 OR t.is_validation IS NULL)
         AND t.closed_at >= :s AND t.closed_at <= :e",
        [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]
    );
    $liveCombinedPnl = round((float)($liveCombinedPnlRow[0]['combined_pnl'] ?? 0), 2);

    // Per-account trading P&L via correlated subquery, period-scoped —
    // same reasoning as Prop Firm: profitability rankings always reflect
    // the selected period, not an all-time total (which would make "best
    // this month" mean the same thing on every period tab). Not just the
    // balance delta either (which can include deposits/fees/resets).
    // Same broker_pnl/projected_pnl fallback fix as $liveCombinedPnlRow above.
    $livePnlSubquery = "(SELECT COALESCE(SUM(COALESCE(pt.broker_pnl, pt.projected_pnl, 0)), 0) FROM trades pt WHERE pt.account_id = ta.id AND pt.status = 'closed' AND pt.deleted_at IS NULL AND (pt.is_validation = 0 OR pt.is_validation IS NULL) AND pt.closed_at >= :s AND pt.closed_at <= :e)";

    // ROOT CAUSE of the "Invalid parameter number" (ScopedDb.php:99) bug:
    // the query below used to embed {$livePnlSubquery} TWICE in one
    // statement (once for accounts_closed_at_loss, once for
    // accounts_meaningful_profit), which duplicates the :s/:e
    // placeholders within a single prepared statement, invalid under
    // native (non-emulated) MySQL prepares, even though PDO happily
    // allows it in emulated mode. Fixed by giving each embedding its own
    // uniquely-suffixed placeholders, bound to the same date values.
    // Same broker_pnl/projected_pnl fallback fix as $livePnlSubquery above.
    $livePnlSubqueryA = "(SELECT COALESCE(SUM(COALESCE(pt.broker_pnl, pt.projected_pnl, 0)), 0) FROM trades pt WHERE pt.account_id = ta.id AND pt.status = 'closed' AND pt.deleted_at IS NULL AND (pt.is_validation = 0 OR pt.is_validation IS NULL) AND pt.closed_at >= :sa AND pt.closed_at <= :ea)";
    $livePnlSubqueryB = "(SELECT COALESCE(SUM(COALESCE(pt.broker_pnl, pt.projected_pnl, 0)), 0) FROM trades pt WHERE pt.account_id = ta.id AND pt.status = 'closed' AND pt.deleted_at IS NULL AND (pt.is_validation = 0 OR pt.is_validation IS NULL) AND pt.closed_at >= :sb AND pt.closed_at <= :eb)";

    // Deposits/Withdrawals, cash-movement facts, deliberately kept OUT of
    // every P&L/win-rate/trading-performance figure on this page (those
    // all sum broker_pnl/challenge_pnl per CLOSED TRADE, see $liveCombinedPnl
    // above and AnalyticsEngine::computeMetricsFromTrades(), which derives
    // net_pnl/return_pct/win_rate purely from trade P&L, never from
    // current_balance-starting_balance). initial_deposit is NULL on 5 of
    // this DB's 23 real personal accounts (legacy rows predating that
    // column), COALESCE'd to starting_balance (the account's original
    // funding amount in absence of a separately-tracked initial deposit),
    // matching this file's established fallback-cascade convention.
    $liveSummaryRow = $db->raw(
        "SELECT
            COUNT(*) AS total_accounts,
            SUM(COALESCE(net_withdrawals, 0)) AS total_withdrawal_value,
            COUNT(CASE WHEN COALESCE(net_withdrawals, 0) > 0 THEN 1 END) AS accounts_with_withdrawals,
            COUNT(CASE WHEN COALESCE(net_withdrawals, 0) = 0 THEN 1 END) AS accounts_no_withdrawals,
            COUNT(CASE WHEN ta.closed_at IS NOT NULL AND {$livePnlSubqueryA} < 0 THEN 1 END) AS accounts_closed_at_loss,
            COUNT(CASE WHEN {$livePnlSubqueryB} >= 500 THEN 1 END) AS accounts_meaningful_profit,
            AVG(DATEDIFF(COALESCE(ta.closed_at, NOW()), ta.created_at)) AS avg_survival_days,
            SUM(COALESCE(initial_deposit, starting_balance, 0)) AS total_initial_deposit,
            SUM(COALESCE(net_deposits, 0)) AS total_additional_deposits,
            COUNT(CASE WHEN COALESCE(net_deposits, 0) = 0 THEN 1 END) AS accounts_no_additional_deposits,
            SUM(current_balance) AS total_current_balance
         FROM trading_accounts ta
         WHERE ta.user_id = :uid AND ta.account_type = 'personal' AND ta.deleted_at IS NULL",
        [':uid' => $userId, ':sa' => $startStr, ':ea' => $endStr, ':sb' => $startStr, ':eb' => $endStr]
    );
    $liveSummary = $liveSummaryRow[0] ?? [];
    $liveTotalAccounts = (int)($liveSummary['total_accounts'] ?? 0);

    if ($liveTotalAccounts > 0) {
        // HAVING pnl <> 0, a $0.00 account (no trades closed this period,
        // whether because it's never been traded at all or simply wasn't
        // active this period) cannot legitimately be called "top
        // performing." This also structurally excludes closed accounts with
        // no real activity, since a long-closed account almost always shows
        // $0 for whichever period is currently selected.
        $liveBestAccounts = $db->raw(
            "SELECT ta.id, ta.label, ta.starting_balance, ta.current_balance, ta.closed_at, ta.lifecycle_state,
                {$livePnlSubquery} AS pnl
             FROM trading_accounts ta
             WHERE ta.user_id = :uid AND ta.account_type = 'personal' AND ta.deleted_at IS NULL
             HAVING pnl <> 0
             ORDER BY pnl DESC LIMIT 8",
            [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]
        );
        // "Accounts Needing Attention" (Live), same bug as Prop Firms: this
        // used to be $liveWorstAccounts, i.e. just the worst 8 by P&L under a
        // misleading title, which is how a profitable, zero-drawdown account
        // could show up here (it just needs to be the least profitable of the
        // bunch). Personal/live accounts have no firm-imposed drawdown or
        // daily-loss limit (challenge_accounts only applies to prop_firm), so
        // the exact Prop Firms rule (current DD vs. max allowed DD) has no
        // equivalent ceiling to compare against here. Judgment call, made
        // explicit rather than silently guessed: flag an account only if it
        // shows a REAL drawdown of at least 10% from its own historical peak
        // equity, or has already closed with a realized net loss, never a
        // profitable account sitting at or above its own peak.
        // Investigated (2026-07-27): closed accounts were being flagged here
        // ("Closed with a realized net loss") alongside genuinely active
        // accounts in real drawdown, confirmed a real design bug, not
        // intentional. A closed account can't be traded, so it can't "need
        // attention" (there's no action for the trader to take on it right
        // now), that mixing is exactly what made this section's coaching
        // misleading. Closed accounts now go to the Closed Account
        // Retrospective card below instead, which is the correct home for
        // "what happened and why" on an account that's already done.
        // Zero-activity accounts (never traded) are excluded too, an
        // untouched account has no real drawdown to speak of and nothing to
        // coach on.
        $liveAttentionAccounts = [];
        foreach ($liveAccounts as $laAttn) {
            if (($laAttn['lifecycle_state'] ?? '') === 'closed') continue;
            if ((int)($laAttn['closed_trade_count'] ?? 0) === 0) continue;
            $laPeak  = (float)($laAttn['peak_equity'] ?? 0);
            $laCur   = (float)($laAttn['current_balance'] ?? 0);
            $ddFromPeakPct = $laPeak > 0 ? max(0, ($laPeak - $laCur) / $laPeak) * 100 : 0;
            if ($ddFromPeakPct >= 10) {
                $liveAttentionAccounts[] = [
                    'id' => $laAttn['id'], 'label' => $laAttn['label'], 'lifecycle_state' => $laAttn['lifecycle_state'],
                    'drawdown_from_peak_pct' => round($ddFromPeakPct, 1),
                    'reason' => round($ddFromPeakPct, 1) . '% below its peak equity',
                    'sort_key' => $ddFromPeakPct,
                ];
            }
        }
        usort($liveAttentionAccounts, fn($a, $b) => $b['sort_key'] <=> $a['sort_key']);
        $liveAttentionAccounts = array_slice($liveAttentionAccounts, 0, 8);

        // Closed Account Retrospective, a closed account WITH real trading
        // history is still valuable, just retrospective rather than active
        // coaching (the account can no longer be traded, so nothing here is
        // actionable today). Zero-activity closed accounts (opened and
        // closed without ever trading) are excluded entirely, that's
        // future Accounts-page scope, not Reports. All-time figures
        // throughout (survival duration, trading performance, withdrawals,
        // outcome) since these are fixed historical facts about an account
        // that no longer exists to re-scope by period.
        $liveClosedRetrospective = [];
        foreach ($liveAccounts as $laClosed) {
            if (($laClosed['lifecycle_state'] ?? '') !== 'closed') continue;
            if ((int)($laClosed['closed_trade_count'] ?? 0) === 0) continue;
            $survivalDays = null;
            if (!empty($laClosed['created_at']) && !empty($laClosed['closed_at'])) {
                $survivalDays = max(0, (int)round((strtotime($laClosed['closed_at']) - strtotime($laClosed['created_at'])) / 86400));
            }
            $startBal = (float)($laClosed['starting_balance'] ?? 0);
            $endBal   = (float)($laClosed['current_balance'] ?? 0);
            $reasonText = trim((string)($laClosed['current_outcome'] ?? ''));
            if ($reasonText === '') $reasonText = 'Not recorded, closed by trader with no reason logged.';
            $liveClosedRetrospective[] = [
                'id' => $laClosed['id'], 'label' => $laClosed['label'],
                'survival_days' => $survivalDays,
                'closed_trade_count' => (int)($laClosed['closed_trade_count'] ?? 0),
                'alltime_pnl' => (float)($laClosed['alltime_pnl'] ?? 0),
                'withdrawals' => (float)($laClosed['net_withdrawals'] ?? 0),
                'net_result' => round($endBal - $startBal, 2),
                'reason' => $reasonText,
                'closed_at' => $laClosed['closed_at'],
            ];
        }
        usort($liveClosedRetrospective, fn($a, $b) => strtotime($b['closed_at'] ?? '1970-01-01') <=> strtotime($a['closed_at'] ?? '1970-01-01'));
        $liveClosedRetrospective = array_slice($liveClosedRetrospective, 0, 8);

        // Investigated (2026-07-27): accounts with zero trading activity
        // (created, never traded) were dominating this list purely by
        // tenure, confirmed live via direct SQL: 5 of the top 8 "longest
        // surviving" rows for real data had closed_trade_count = 0.
        // Redesigned per explicit spec: qualify only accounts that (a)
        // survived at least 30 days AND (b) closed at least one real trade
        // in that time, "existed" is not "survived." trades_per_month
        // (computed below, not hard-filtered further) is shown as an
        // honest activity-consistency indicator rather than a second hard
        // cutoff, so a one-trade account that happens to qualify still
        // reads very differently from one traded every week.
        $liveLongestSurviving = $db->raw(
            "SELECT ta.id, ta.label, ta.starting_balance, ta.current_balance, ta.created_at, ta.closed_at, ta.lifecycle_state,
                (SELECT COUNT(*) FROM trades t3 WHERE t3.account_id = ta.id AND t3.status = 'closed' AND t3.deleted_at IS NULL AND (t3.is_validation = 0 OR t3.is_validation IS NULL)) AS closed_trade_count,
                {$livePnlSubquery} AS pnl,
                DATEDIFF(COALESCE(ta.closed_at, NOW()), ta.created_at) AS survival_days
             FROM trading_accounts ta
             WHERE ta.user_id = :uid AND ta.account_type = 'personal' AND ta.deleted_at IS NULL
             HAVING survival_days >= 30 AND closed_trade_count >= 1
             ORDER BY survival_days DESC LIMIT 8",
            [':uid' => $userId, ':s' => $startStr, ':e' => $endStr]
        );
        foreach ($liveLongestSurviving as &$lsRow) {
            $lsRow['trades_per_month'] = round((int)$lsRow['closed_trade_count'] / max(1, (int)$lsRow['survival_days'] / 30), 1);
        }
        unset($lsRow);

        $liveAccountAgg = [
            'total_accounts'             => $liveTotalAccounts,
            'combined_pnl'               => $liveCombinedPnl,
            'total_withdrawal_value'     => round((float)($liveSummary['total_withdrawal_value'] ?? 0), 2),
            'accounts_with_withdrawals'  => (int)($liveSummary['accounts_with_withdrawals'] ?? 0),
            'accounts_closed_at_loss'    => (int)($liveSummary['accounts_closed_at_loss'] ?? 0),
            'accounts_meaningful_profit' => (int)($liveSummary['accounts_meaningful_profit'] ?? 0),
            'avg_survival_days'          => $liveSummary['avg_survival_days'] !== null ? round((float)$liveSummary['avg_survival_days'], 0) : null,
            'best'                       => $liveBestAccounts,
            'needs_attention'            => $liveAttentionAccounts,
            'longest_surviving'          => $liveLongestSurviving,
            'closed_retrospective'       => $liveClosedRetrospective,
            // Deposits/Withdrawals, all-time, lifecycle-fact cash-movement
            // figures (same "account facts stay all-time" convention as
            // Prop Firm's breach/pass counts), deliberately never blended
            // into combined_pnl or any other trading-performance figure
            // above. total_deposits = initial funding + any capital added
            // later; net_capital_flow = total money in minus total money
            // out across the account's whole life.
            'accounts_no_withdrawals'          => (int)($liveSummary['accounts_no_withdrawals'] ?? 0),
            'total_initial_deposit'             => round((float)($liveSummary['total_initial_deposit'] ?? 0), 2),
            'total_additional_deposits'         => round((float)($liveSummary['total_additional_deposits'] ?? 0), 2),
            'accounts_no_additional_deposits'   => (int)($liveSummary['accounts_no_additional_deposits'] ?? 0),
            'total_current_balance'             => round((float)($liveSummary['total_current_balance'] ?? 0), 2),
        ];
        $liveAccountAgg['total_deposits'] = round($liveAccountAgg['total_initial_deposit'] + $liveAccountAgg['total_additional_deposits'], 2);
        $liveAccountAgg['net_capital_flow'] = round($liveAccountAgg['total_deposits'] - $liveAccountAgg['total_withdrawal_value'], 2);
    }
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reports, TraderAlign</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/traderalign/assets/tokens.css">
<link rel="icon" type="image/png" sizes="32x32" href="/traderalign/assets/favicon/favicon-32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/traderalign/assets/favicon/favicon-16.png">
    <link rel="apple-touch-icon" sizes="180x180" href="/traderalign/assets/favicon/apple-touch-icon.png">
    <link rel="shortcut icon" href="/traderalign/assets/favicon/favicon.ico">
<link rel="stylesheet" href="/traderalign/assets/css/brand.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
.dash-layout{display:flex;min-height:100vh}
.sidebar{width:4rem;background:var(--sidebar);border-right:1px solid var(--sidebar-border);display:flex;flex-direction:column;align-items:center;padding-top:1rem;gap:.25rem;position:fixed;top:0;left:0;bottom:0;z-index:40}
.sidebar a{display:flex;align-items:center;justify-content:center;width:2.75rem;height:2.75rem;border-radius:var(--radius-md);color:var(--foreground-dim);transition:background var(--t-fast),color var(--t-fast);position:relative}
.sidebar a:hover{background:var(--background-surface);color:var(--foreground)}
.sidebar a.active{background:var(--background-surface);color:var(--sidebar-primary)}
.sidebar a .tt{position:absolute;left:3.5rem;background:var(--popover);color:var(--foreground);font-size:.75rem;padding:.25rem .625rem;border-radius:var(--radius-sm);white-space:nowrap;pointer-events:none;opacity:0;transition:opacity var(--t-fast)}
.sidebar a:hover .tt{opacity:1}
.main{margin-left:4rem;flex:1;padding:2rem;max-width:1360px}
.page-title{font-size:1.5rem;font-weight:700;color:var(--foreground)}
.page-sub{color:var(--foreground-muted);margin-top:.25rem;margin-bottom:1.5rem;font-size:.9rem}
.back-link{font-size:.85rem;color:var(--primary);margin-bottom:1rem;display:inline-block}
.strategy-heading{font-size:1.1rem;font-weight:600;color:var(--foreground);margin-bottom:.25rem}
.strategy-heading-sub{font-size:.78rem;color:var(--foreground-dim);margin-bottom:1.25rem}
.period-tabs{display:flex;gap:.5rem;margin-bottom:1.5rem;flex-wrap:wrap}
.period-tab{padding:.5rem 1.25rem;border-radius:var(--radius-full);font-size:.8rem;font-weight:600;color:var(--foreground-dim);background:var(--background-elevated);border:1px solid var(--border);cursor:pointer;transition:all var(--t-fast);text-decoration:none}
.period-tab:hover{color:var(--foreground);border-color:color-mix(in srgb,var(--foreground) 30%,transparent)}
.period-tab.active{background:var(--primary);color:#fff;border-color:var(--primary)}
.period-tab.locked{opacity:0.4;cursor:not-allowed;pointer-events:auto;position:relative}
.period-tab.locked .lock-tooltip{position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:var(--popover);color:var(--foreground);font-size:.72rem;font-weight:500;padding:.5rem .75rem;border-radius:var(--radius-md);border:1px solid var(--border);white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s ease;box-shadow:0 4px 12px rgba(0,0,0,.25);z-index:50}
.period-tab.locked .lock-tooltip::after{content:'';position:absolute;top:100%;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:var(--border)}
.period-tab.locked:hover .lock-tooltip{opacity:1}
/* 2026-07-28 stabilization pass, item 2, the prior flatten (borderless,
   4px track, .68rem label) overcorrected from an earlier headline-card
   version and ended up reading as a debug indicator instead of production
   UI. Refined, not redesigned: still no card chrome (background/border),
   still visually secondary to the report content below it, just given
   enough height/spacing/type weight to read as an intentional metadata
   row rather than a stray leftover. Both bars unchanged in meaning. */
.completeness-row{display:flex;flex-direction:column;gap:.55rem;margin:.65rem 0 1.35rem}
.completeness-bar{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:.85rem;padding:0;background:none;border:none;cursor:help}
.completeness-bar.secondary{opacity:.82}
.completeness-label{font-size:.74rem;font-weight:600;color:var(--foreground-dim);white-space:nowrap;letter-spacing:.01em}
.completeness-window-note{font-weight:400;opacity:.78}
.completeness-track{width:100%;max-width:220px;height:6px;border-radius:3px;background:var(--background-elevated);overflow:hidden}
.completeness-fill{height:100%;border-radius:3px;background:var(--primary);opacity:.85}
.completeness-fill.data-fill{background:var(--warning);opacity:.85}
.annual-early-banner{position:relative;margin-bottom:1.25rem;padding:.9rem 2.5rem .9rem 1rem;background:var(--background-elevated);border:1px solid var(--border);border-left:3px solid var(--primary);border-radius:var(--radius-lg)}
.annual-early-banner strong{display:block;font-size:.82rem;color:var(--foreground);margin-bottom:.35rem}
.annual-early-banner p{font-size:.76rem;color:var(--foreground-muted);line-height:1.55;margin:0}
.annual-early-dismiss{position:absolute;top:.6rem;right:.75rem;background:none;border:none;font-size:1.1rem;line-height:1;color:var(--foreground-dim);cursor:pointer;padding:.15rem .4rem;border-radius:var(--radius-sm)}
.annual-early-dismiss:hover{color:var(--foreground);background:var(--background)}
.section-card{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);padding:1.5rem;margin-bottom:1.25rem}
.section-card h3{font-size:1rem;font-weight:600;color:var(--foreground);margin-bottom:.75rem}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-bottom:1rem}
.stat-tile{background:var(--background);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem;text-align:center;display:flex;flex-direction:column;align-items:center}
.stat-tile .label{font-size:.68rem;font-weight:500;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-dim)}
.stat-tile .value{font-family:var(--font-mono);font-size:1.3rem;font-weight:600;color:var(--foreground);margin-top:.15rem}
.stat-tile .sub{font-size:.65rem;color:var(--foreground-dim);margin-top:.25rem;line-height:1.35;font-family:var(--font-sans)}
/* Unified drill-link, one definition, used everywhere */
.drill-link{display:inline-block;font-size:.68rem;font-weight:500;color:var(--foreground-dim);text-decoration:none;transition:color var(--t-fast)}
.drill-link:hover{color:var(--foreground);text-decoration:underline}
/* Placement variant: tile-bottom, own line, centred, below the stat value */
.drill-link-tile{margin-top:.5rem}
/* Placement variant: header-right, sits at far right of a card header row */
.drill-link-header{margin-left:auto;flex-shrink:0}
/* Placement variant: table-cell, lives in its own trailing <td> */
/* (no extra class needed, just the base .drill-link) */
.positive{color:var(--positive)}.negative{color:var(--negative)}
.pill{display:inline-flex;align-items:center;padding:.15rem .5rem;border-radius:var(--radius-full);font-size:.65rem;font-weight:600;letter-spacing:.03em;text-transform:uppercase;white-space:nowrap}
.pill-green{background:color-mix(in srgb,var(--positive) 18%,transparent);color:var(--positive);border:1px solid color-mix(in srgb,var(--positive) 30%,transparent)}
.pill-amber{background:color-mix(in srgb,var(--warning) 18%,transparent);color:var(--warning);border:1px solid color-mix(in srgb,var(--warning) 30%,transparent)}
.pill-orange{background:color-mix(in srgb,#F97316 18%,transparent);color:#F97316;border:1px solid color-mix(in srgb,#F97316 30%,transparent)}
.pill-red{background:color-mix(in srgb,var(--negative) 18%,transparent);color:var(--negative);border:1px solid color-mix(in srgb,var(--negative) 30%,transparent)}
.pill-neutral{background:var(--background-surface);color:var(--foreground-dim);border:1px solid var(--border)}
.chart-wrap{position:relative;width:100%;min-height:250px;margin-top:.5rem}
.chart-wrap canvas{width:100% !important;height:100% !important}
.compare-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:1rem;font-size:.8rem}
.compare-item{background:var(--background);border:1px solid var(--border);border-radius:var(--radius-md);padding:.75rem 1rem;text-align:center}
.compare-item .clabel{font-size:.65rem;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-dim)}
.compare-item .cvalue{font-family:var(--font-mono);font-size:1rem;font-weight:600;color:var(--foreground);margin-top:.15rem}
.setup-table{width:100%;border-collapse:collapse;font-size:.78rem;margin-top:.5rem}
.setup-table th{text-align:left;padding:.4rem .5rem;font-weight:500;color:var(--foreground-dim);font-size:.68rem;text-transform:uppercase;border-bottom:1px solid var(--border)}
.setup-table td{padding:.4rem .5rem;border-bottom:1px solid var(--border);font-family:var(--font-mono)}
.insight-text{font-size:.8rem;color:var(--foreground-muted);line-height:1.55;margin-top:.5rem;padding:.75rem;background:var(--background);border-radius:var(--radius-md)}
.empty-state{text-align:center;padding:3rem;color:var(--foreground-dim)}
.advanced-metric .label{cursor:help;border-bottom:1px dotted var(--foreground-dim)}
.locked-card{background:var(--background-elevated);border:1px solid var(--border);border-left:3px solid var(--primary);border-radius:var(--radius-xl);padding:2rem;margin-bottom:1.25rem;text-align:center}
.locked-card .lock-icon{font-size:2.5rem;margin-bottom:.75rem}
.locked-card h3{font-size:1.1rem;color:var(--foreground);margin-bottom:.5rem}
.locked-card p{font-size:.82rem;color:var(--foreground-muted);line-height:1.6;max-width:480px;margin:0 auto 1.25rem}
.classification-row{display:flex;align-items:center;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--border)}
.classification-row:last-child{border-bottom:none}
.classification-label{min-width:120px;font-size:.78rem;font-weight:500;color:var(--foreground)}
.classification-count{min-width:40px;font-family:var(--font-mono);font-size:.8rem;color:var(--foreground);text-align:right}
.classification-pct{min-width:45px;font-family:var(--font-mono);font-size:.75rem;color:var(--foreground-dim);text-align:right}
.classification-bar-wrap{flex:1;height:8px;background:var(--background-surface);border-radius:4px;overflow:hidden}
.classification-bar-fill{height:100%;border-radius:4px;transition:width .3s ease}
.gap-green{color:var(--positive)}.gap-amber{color:var(--warning)}.gap-red{color:var(--negative)}
.edge-badge{display:inline-block;padding:.25rem .65rem;border-radius:var(--radius-full);font-size:.68rem;font-weight:600;letter-spacing:.02em;margin-top:.5rem}
.edge-strong{background:color-mix(in srgb,var(--positive) 16%,transparent);color:var(--positive);border:1px solid color-mix(in srgb,var(--positive) 35%,transparent)}
.edge-found{background:color-mix(in srgb,var(--primary) 16%,transparent);color:var(--primary);border:1px solid color-mix(in srgb,var(--primary) 35%,transparent)}
.edge-early{background:color-mix(in srgb,var(--warning) 14%,transparent);color:var(--warning);border:1px solid color-mix(in srgb,var(--warning) 32%,transparent)}
.edge-none{background:color-mix(in srgb,var(--negative) 12%,transparent);color:var(--negative);border:1px solid color-mix(in srgb,var(--negative) 28%,transparent)}
.edge-insufficient{background:var(--background-surface);color:var(--foreground-dim);border:1px solid var(--border)}
/* All-versions collapse panel */
.versions-toggle{font-size:.7rem;color:var(--foreground-dim);text-decoration:none;cursor:pointer;background:none;border:none;padding:0;font-family:inherit;transition:color var(--t-fast)}
.versions-toggle:hover{color:var(--foreground);text-decoration:underline}
.versions-panel{display:none;margin-top:.75rem}
.versions-panel.open{display:block}
@media(max-width:768px){.main{padding:1rem}.stat-grid{grid-template-columns:repeat(2,1fr)}.compare-row{grid-template-columns:1fr}.classification-label{min-width:90px}}

/* ── 4-Stage Summary (2026-07-26 rebuild; row-grid alignment 2026-07-27), reuses tokens/components above, no new visual language ── */
.fs-header{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem}
.fs-header-right{display:flex;flex-wrap:wrap;gap:.6rem;align-items:center}
.fs-select{background:var(--background-elevated);border:1px solid var(--border);color:var(--foreground);padding:.4rem .65rem;border-radius:var(--radius-md);font-size:.75rem}
.fs-top-metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:.75rem;margin-bottom:1.5rem}
/* Row-grid alignment: the outer grid owns BOTH axes, 4 card columns (+3
   arrow columns) AND 9 explicit row tracks (title, sub, 6 comparison rows,
   tagline). Each .fs-stage-card spans every row track and uses
   grid-template-rows:subgrid so its own children (title/sub/6 rows/tagline,
   in DOM order, no per-child grid-row needed except where noted) share
   those SAME tracks, meaning row N's height is the max content height of
   row N across all 4 cards, automatically, with zero JS. This is what makes
   the four cards read as one six-row comparison table instead of four
   independently-sized columns. Needs subgrid (Chrome/Edge 117+, Firefox
   71+, Safari 16+); the sub-1100px breakpoint below drops back to a plain
   stacked flex layout, where cross-card alignment isn't a meaningful
   concept anyway (cards no longer sit side by side). */
.fs-stage-row{display:grid;grid-template-columns:1fr auto 1fr auto 1fr auto 1fr;grid-template-rows:auto auto repeat(6,auto) auto;column-gap:.5rem;row-gap:0;align-items:stretch;margin-bottom:1rem}
.fs-stage-card{background:var(--background);border:1px solid var(--border);border-left:3px solid var(--primary);border-radius:var(--radius-lg);padding:1.1rem;display:grid;grid-template-rows:subgrid;grid-row:1/-1;row-gap:0}
.fs-stage-title{grid-row:1;font-size:.85rem;font-weight:700;color:var(--foreground);margin-bottom:.15rem}
.fs-stage-sub{grid-row:2;font-size:.68rem;color:var(--foreground-dim);margin-bottom:.85rem;line-height:1.4}
.fs-stage-metric{padding:.7rem 0;display:flex;flex-direction:column;justify-content:center}
.fs-stage-metric+.fs-stage-metric{border-top:1px solid var(--border)}
/* Stage 4's "no reconciled trades yet" placeholder replaces all 6 metric
   rows with one paragraph, spans rows 3-8 so the tagline (row 9) still
   lands in the same track as the other three cards' taglines. */
.fs-stage-empty-note{grid-row:3/9;font-size:.75rem;color:var(--foreground-muted);line-height:1.5}
.fs-m-label{font-size:.68rem;color:var(--foreground-dim);display:block}
.fs-m-value{font-family:var(--font-mono);font-size:1rem;font-weight:600;color:var(--foreground)}
.fs-m-caption{font-size:.65rem;color:var(--foreground-dim)}
.fs-stage-list{list-style:none;margin:0 0 .65rem;padding:0;font-size:.72rem;color:var(--foreground-muted)}
.fs-stage-list li{display:flex;justify-content:space-between;gap:.5rem;padding:.1rem 0}
.fs-stage-tagline{grid-row:9;font-size:.68rem;font-weight:600;letter-spacing:.02em;padding-top:.6rem;border-top:1px solid var(--border);align-self:end}
.fs-arrow{display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:var(--foreground-dim);grid-row:1/-1}
.fs-bottom-grid{display:grid;grid-template-columns:1.4fr 1fr 1.1fr;gap:1rem;align-items:stretch}
.fs-mini-row{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-top:.85rem}
.fs-mini-tile{background:var(--background);border:1px solid var(--border);border-radius:var(--radius-md);padding:.55rem .4rem;text-align:center}
.fs-mini-label{font-size:.58rem;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.03em;display:block}
.fs-mini-value{font-family:var(--font-mono);font-size:.9rem;font-weight:700;color:var(--foreground);margin-top:.1rem}
.fs-rec-list{list-style:none;margin:0;padding:0;font-size:.78rem;color:var(--foreground-muted)}
.fs-rec-list li{display:flex;gap:.5rem;padding:.4rem 0;border-bottom:1px solid var(--border);line-height:1.45}
.fs-rec-list li:last-child{border-bottom:none}
.fs-rec-check{flex-shrink:0;color:var(--positive)}
.fs-footer-note{font-size:.68rem;color:var(--foreground-dim);text-align:center;margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border)}
/* "Show More" row, now sits below Recommendations and toggles the
   standalone Intent vs Reality card beneath the whole 4-Stage Summary, NOT
   anything inside the stage cards themselves (see CLAUDE.md's 2026-07-27
   refinement-pass entry). */
.fs-show-more-row{text-align:center;margin:1.25rem 0 0}
.fs-gap-card{display:none}
.fs-gap-card.open{display:block}
/* Intent vs Reality, Execution Gap: a compact %-bar per drift dimension,
   reused for every metric so the whole card reads as one ranked list
   (worst-adherence dimension visually shortest) rather than a stat grid. */
.fs-gap-metric{margin-bottom:.9rem}
.fs-gap-metric:last-child{margin-bottom:0}
.fs-gap-metric-head{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;margin-bottom:.3rem}
.fs-gap-metric-label{font-size:.78rem;color:var(--foreground);font-weight:600}
.fs-gap-metric-value{font-family:var(--font-mono);font-size:.85rem;font-weight:700}
.fs-gap-bar-track{background:var(--border);border-radius:99px;height:7px;overflow:hidden}
.fs-gap-bar-fill{height:100%;border-radius:99px}
.fs-gap-metric-caption{font-size:.65rem;color:var(--foreground-dim);margin-top:.2rem}
@media(max-width:1100px){
  .fs-stage-row{grid-template-columns:1fr;grid-template-rows:none}
  .fs-stage-card{grid-template-rows:none;grid-row:auto;display:flex;flex-direction:column}
  .fs-stage-title,.fs-stage-sub,.fs-stage-tagline,.fs-stage-empty-note{grid-row:auto}
  .fs-stage-tagline{margin-top:auto}
  .fs-arrow{transform:rotate(90deg);padding:.15rem 0;grid-row:auto}
  .fs-bottom-grid{grid-template-columns:1fr}
  .fs-top-metrics{grid-template-columns:repeat(3,1fr)}
}
@media(max-width:600px){.fs-top-metrics{grid-template-columns:repeat(2,1fr)}.fs-mini-row{grid-template-columns:1fr}}
/* ── Instrument Performance (2026-07-27 compression), one card, a compact
   comparison table + relative-P&L bar, expandable rows for classification
   detail instead of one full section-card per instrument. ── */
.instr-perf-table{width:100%;border-collapse:collapse;font-size:.78rem}
.instr-perf-table th{text-align:left;font-size:.66rem;text-transform:uppercase;letter-spacing:.03em;color:var(--foreground-dim);padding:.4rem .6rem;border-bottom:1px solid var(--border)}
.instr-perf-table td{padding:.55rem .6rem;border-bottom:1px solid var(--border);vertical-align:middle}
.instr-perf-table tr:last-child td{border-bottom:none}
.instr-perf-bar-cell{min-width:90px}
.instr-perf-bar-track{background:var(--border);border-radius:99px;height:6px;overflow:hidden}
.instr-perf-bar-fill{height:100%;border-radius:99px}
.instr-perf-row{cursor:pointer}
.instr-perf-row:hover{background:color-mix(in srgb,var(--primary) 4%,transparent)}
.instr-perf-detail{display:none;background:var(--background)}
.instr-perf-detail.open{display:table-row}
/* ── Boundary Analysis (relocated into Prop Firm Analytics, 2026-07-27) ── */
.boundary-split-bar{display:flex;height:22px;border-radius:var(--radius-md);overflow:hidden;background:var(--border);margin-bottom:.4rem}
.boundary-split-seg{display:flex;align-items:center;justify-content:center;font-size:.65rem;font-weight:700;color:#0b0f13;white-space:nowrap;overflow:hidden}
.boundary-split-legend{display:flex;flex-wrap:wrap;gap:.9rem;font-size:.7rem;color:var(--foreground-muted);margin-bottom:1rem}
.boundary-split-legend span.dot{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:.35rem}

/* ── Hero: Strategy Summary (always visible) ── */
.hero-card{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);padding:1.5rem;margin-bottom:1.25rem}
.hero-meta-bar{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1.25rem;padding-bottom:1.25rem;border-bottom:1px solid var(--border)}
.hero-meta-chip{display:inline-flex;align-items:center;gap:.4rem;font-size:.75rem;color:var(--foreground-muted);background:var(--background);border:1px solid var(--border);border-radius:var(--radius-full);padding:.35rem .8rem}
.hero-meta-chip strong{color:var(--foreground);font-weight:600}
.hero-kpi-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:.75rem}
.hero-kpi-grid .stat-tile{background:var(--background)}
@media(max-width:900px){.hero-kpi-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:480px){.hero-kpi-grid{grid-template-columns:repeat(2,1fr)}}

/* ── Performance Overview: charts side-by-side ── */
.chart-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:1.25rem;margin-bottom:1.25rem;align-items:stretch}
.chart-row .section-card{margin-bottom:0}
.chart-row .chart-wrap{min-height:230px;max-height:280px}

/* ── Accordion groups for progressive disclosure ── */
.accordion-group{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);margin-bottom:1.25rem;overflow:hidden}
.accordion-group-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:1.1rem 1.5rem;cursor:pointer;user-select:none;flex-wrap:wrap;background:none;border:none;width:100%;text-align:left;font-family:inherit}
.accordion-group-header:hover{background:var(--background-surface)}
.accordion-group-title{display:flex;align-items:center;gap:.6rem;font-size:1rem;font-weight:700;color:var(--foreground)}
.accordion-group-title .accordion-icon{font-size:1.1rem}
.accordion-group-summary{display:flex;gap:1.25rem;flex-wrap:wrap;font-size:.75rem;color:var(--foreground-dim)}
.accordion-group-summary span strong{color:var(--foreground);font-family:var(--font-mono);font-weight:600}
.accordion-chevron{transition:transform var(--t-fast);color:var(--foreground-dim);flex-shrink:0;margin-left:.5rem}
.accordion-group.open .accordion-chevron{transform:rotate(180deg)}
.accordion-group-body{display:none;padding:0 1.5rem 1.5rem;border-top:1px solid var(--border)}
.accordion-group.open .accordion-group-body{display:block;padding-top:1.25rem}
.accordion-group-body .section-card{background:var(--background);margin-bottom:1rem}
.accordion-group-body .section-card:last-child{margin-bottom:0}
@media(max-width:768px){.hero-meta-bar{gap:.4rem}.accordion-group-summary{display:none}}
</style>
</head>
<body class="dash-layout">

<?php
// 2026-08-20: the sessionStorage['active_strategy_id'] read/redirect that
// used to live here (a client-side <script> block) is retired. Superseded
// by the real server-side active-strategy pointer this page now resolves
// before it ever renders (see the PHP block above,
// spGetActiveStrategyId()/spRecordActiveStrategy() in
// src/StrategyPicker.php). Leaving it running alongside the new
// server-side fix would have been actively harmful, not just redundant: a
// stale sessionStorage value from a previous day/tab could silently
// override the correct, freshly-resolved strategy this page had already
// rendered. See migration_add_user_active_strategy.sql for the full
// incident writeup.
?>

<?php require __DIR__ . '/sidebar.php'; ?>

<main class="main">
    <a href="/traderalign/dashboard" class="back-link">← Back to Dashboard</a>
    <h1 class="page-title">Performance Reports</h1>

    <h2 class="strategy-heading">
        Report for <?= htmlspecialchars($strategyDisplayName) ?>
    </h2>
    <p class="strategy-heading-sub"><?= $label ?> · <?= $totalTrades ?> trades · <?= $start->format('M j, Y') ?> - <?= $end->format('M j, Y') ?></p>

    <?php if ($strategyMilestoneReview): ?>
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- 50-Trade Strategy Review — strategy-level, milestone-based, NOT   -->
    <!-- scoped to Weekly/Monthly/Quarterly/Annual or to Prop/Live below.  -->
    <!-- Renders identically no matter which period tab is active — see   -->
    <!-- src/StrategyMilestoneReview.php and $strategyMilestoneReview's    -->
    <!-- computation earlier in this file (deliberately outside every     -->
    <!-- $period/$accountView branch). Exactly ONE card, for the currently-->
    <!-- selected/active trading strategy only — absent entirely when     -->
    <!-- that strategy hasn't reached 50 trades yet.                      -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php StrategyMilestoneReview::renderCard($strategyMilestoneReview, 'reports'); ?>
    <?php endif; ?>

    <div class="period-tabs">
        <?php $tabLabels = ['weekly'=>'Weekly','monthly'=>'Monthly','quarterly'=>'Quarterly','annual'=>'Annual'];
        foreach ($tabLabels as $key => $tabLabel):
            $isUnlocked = $periodUnlocked[$key];
            $isActive   = ($period === $key);
            if ($isUnlocked): ?>
                <a href="?period=<?= $key ?><?= $selectedStrategy ? '&strategy_id=' . htmlspecialchars($selectedStrategy) : '' ?>" class="period-tab <?= $isActive ? 'active' : '' ?>"><?= $tabLabel ?></a>
            <?php else: ?>
                <span class="period-tab locked">🔒 <?= $tabLabel ?><span class="lock-tooltip"><?= htmlspecialchars($periodTooltips[$key] ?? 'Not yet available') ?></span></span>
            <?php endif;
        endforeach; ?>
    </div>

    <!-- Time Progress + Data Completeness, two independent measurements,
         deliberately never blended into one number (2026-07-28 second
         refinement pass, item 2). Time Progress answers "how far through
         the calendar period are we" (pure date math, replaces the old
         dev-only period-unlock toggle); Data Completeness answers "how much
         real trading evidence exists" (pure trade-count math). A trader can
         be at 5% Time Progress with a large early sample, or 100% Time
         Progress with almost no evidence, both states are real and the UI
         must be able to say so distinctly. -->
    <?php
    // "since your first trade" vs "since {period} start", say plainly which
    // window this percentage is measured against rather than letting the
    // label imply pure calendar math once the effective start has shifted
    // (see the Time Progress computation above / CLAUDE.md's "Effective
    // Reporting-Period Start" note for why these can differ).
    if ($effectivePeriodStart === null) {
        $timeProgressWindowNote = 'no trades yet this ' . periodNoun($period);
    } elseif ($periodStartedMidway) {
        $timeProgressWindowNote = 'since your first trade this ' . periodNoun($period) . ' (' . $effectivePeriodStart->format('M j') . ')';
    } else {
        $timeProgressWindowNote = 'since ' . periodNoun($period) . ' start';
    }
    ?>
    <div class="completeness-row">
        <div class="completeness-bar" title="<?= htmlspecialchars(reportsCoach('progress.time')) ?>">
            <span class="completeness-label"><?= htmlspecialchars(ucfirst(periodNoun($period))) ?> Time Progress: <?= $periodTimeProgressPct ?>% <span class="completeness-window-note">(<?= htmlspecialchars($timeProgressWindowNote) ?>)</span> ⓘ</span>
            <div class="completeness-track"><div class="completeness-fill" style="width:<?= $periodTimeProgressPct ?>%"></div></div>
        </div>
        <div class="completeness-bar secondary" title="<?= htmlspecialchars(reportsCoach('progress.data_completeness')) ?>">
            <span class="completeness-label">Data Completeness: <?= htmlspecialchars($periodDataCompletenessLabel) ?> (<?= $totalTrades ?> <?= $totalTrades === 1 ? 'trade' : 'trades' ?>) ⓘ</span>
            <div class="completeness-track"><div class="completeness-fill data-fill" style="width:<?= $periodDataCompletenessPct ?>%"></div></div>
        </div>
    </div>
    <?php
    // Combined coaching line, the two metrics answer different questions,
    // so a reader who only glances at one bar can still be misled; this
    // sentence exists specifically to say what BOTH together mean, per the
    // exact "This quarter is only 28% complete..." / "Although the quarter
    // is still early, enough trades exist..." examples this item was scoped
    // around. Four real combinations, not a single templated line.
    $timeEarly = $periodTimeProgressPct < 40;
    $dataThin  = $totalTrades < 10;
    if ($timeEarly && $dataThin) {
        $progressCoachLine = "This " . periodNoun($period) . " is only {$periodTimeProgressPct}% complete and only {$totalTrades} trade(s) have closed so far, conclusions here should stay tentative on both counts.";
    } elseif ($timeEarly && !$dataThin) {
        $progressCoachLine = "Although this " . periodNoun($period) . " is still early ({$periodTimeProgressPct}% complete), {$totalTrades} trades already exist, enough to start identifying real patterns, even before the period closes.";
    } elseif (!$timeEarly && $dataThin) {
        $progressCoachLine = "This " . periodNoun($period) . " is well underway ({$periodTimeProgressPct}% complete), but only {$totalTrades} trade(s) have closed, a quiet period, not a data problem, but treat any conclusions here as provisional until more trades close.";
    } else {
        $progressCoachLine = "This " . periodNoun($period) . " has both enough elapsed time ({$periodTimeProgressPct}%) and enough closed trades ({$totalTrades}) to support real conclusions below.";
    }
    ?>
    <p class="strategy-heading-sub" style="margin-top:-.35rem;margin-bottom:1rem;font-style:italic"><?= htmlspecialchars($progressCoachLine) ?></p>

    <?php if ($showAnnualEarlyStageBanner): ?>
    <!-- Item 4 (2026-07-28): calm, non-blocking coaching, never a hard
         block. Dismissible per page load only (no localStorage flag), so it
         appears again the next time Annual is opened while still below
         threshold, exactly as specified. -->
    <div class="annual-early-banner" id="annualEarlyBanner">
        <button class="annual-early-dismiss" onclick="document.getElementById('annualEarlyBanner').style.display='none'" aria-label="Dismiss">&times;</button>
        <strong>Early in the year, annual analytics are still developing</strong>
        <p>This year is only <?= $periodTimeProgressPct ?>% complete (Time Progress), so Annual figures reflect a partial slice of the year rather than a full picture, independent of how much trade evidence exists (see Data Completeness above). Trends, seasonality, and averages shown here will keep shifting as more of the year closes, treat conclusions cautiously for now, and lean on Quarterly or Monthly views for anything you need to act on today. You're always free to keep viewing Annual Reports; this is just a heads-up, not a restriction.</p>
    </div>
    <?php endif; ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Account Analytics, Prop Firms / Live Accounts                    -->
    <!-- Row 2 toggle. Works alongside the Weekly/Monthly/Quarterly/Annual  -->
    <!-- timeframe toggle above (Row 1), untouched, original behavior.    -->
    <!-- 'Comparison' replaced 2026-07-28 (correction pass) with a plain   -->
    <!-- link out to the separate Trader Profile module, Trader Profile  -->
    <!-- is NOT a third internal $accountView state; Reports itself only  -->
    <!-- ever knows about 'prop'/'live', unchanged, per the "Reports and  -->
    <!-- Trader Profile stay fully separate modules" architecture.        -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <h2 class="strategy-heading" style="margin-top:.5rem">Account Analytics</h2>
    <p class="strategy-heading-sub">Balance, drawdown, challenge progress, and lifecycle data, by trading account</p>

    <div class="period-tabs">
        <?php
        $accountViewLabels = ['prop' => 'Prop Firms', 'live' => 'Live Accounts'];
        foreach ($accountViewLabels as $avKey => $avLabel):
            $avUrl = '?account_view=' . $avKey
                . '&period=' . urlencode($period)
                . ($selectedStrategy ? '&strategy_id=' . htmlspecialchars($selectedStrategy) : '');
        ?>
            <a href="<?= $avUrl ?>" class="period-tab <?= $accountView === $avKey ? 'active' : '' ?>"><?= $avLabel ?></a>
        <?php endforeach; ?>
        <?php
        // Trader Profile has no Weekly period, falls back to Monthly when
        // Reports' own period toggle is set to Weekly.
        $tpPeriod = $period === 'weekly' ? 'monthly' : $period;
        ?>
        <a href="/traderalign/trader-profile?period=<?= urlencode($tpPeriod) ?>" class="period-tab">Trader Profile</a>
    </div>

    <?php
    // Live-account single-account dropdown removed, it used to drive the
    // Account Overview / Challenge Progress / Lifecycle Journey cards
    // below, which were themselves removed (single-account drill-down has
    // no place in an aggregate report: with many accounts, arbitrarily
    // showing one account's numbers here with no indication of why that
    // one was picked was actively misleading). Per-account drill-down will
    // be rebuilt properly inside the 4-Stage Performance Audit section
    // instead, the same way the prop-firm account dropdown already was
    // (see that section's "Trade Set" step below), not yet done for Live
    // Accounts, flagged as follow-up work rather than done here.
    $accountListForView = $accountView === 'live' ? $liveAccounts : $propAccounts;
    ?>

    <?php if ($accountView !== 'comparison' && empty($accountListForView)): ?>
        <div class="empty-state"><p>No <?= $accountView === 'prop' ? 'prop firm' : 'live' ?> accounts found yet.</p></div>
    <?php endif; ?>



    <?php
    // Comparison tab, a previous pass replaced the old Prop-vs-Live table
    // with a placeholder card, but every other section on the page (hero
    // stats, charts, Trade Analysis, 4-Stage Performance Audit, Advanced
    // Statistics, none of which are actually account-view-specific) kept
    // rendering underneath it regardless of $accountView. Per explicit
    // instruction, the Comparison tab shows ONLY the Weekly/Monthly/
    // Quarterly/Annual period toggle (and the Prop Firms/Live Accounts/
    // Comparison toggle itself, needed to navigate off this tab), nothing
    // else, not even a placeholder, until its replacement is designed as a
    // separate task. Everything below is skipped entirely when comparison
    // is selected; the $comparisonProp/$comparisonLive queries above are
    // left in place (cheap, harmless) since the eventual redesign will
    // likely need the same per-account-type metrics.
    if ($accountView !== 'comparison'):
    ?>
    <?php if ($totalTrades === 0): ?>
        <div class="empty-state"><p>No closed trades in this period yet.</p></div>
    <?php else: ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Strategy Summary, Hero (always visible) -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <div class="hero-card">
        <div class="hero-meta-bar">
            <span class="hero-meta-chip">Strategy: <strong><?= htmlspecialchars($selectedStrategyName ?? 'All Strategies') ?></strong></span>
            <?php if ($selectedStrategyChangeCount > 0): ?><span class="hero-meta-chip" title="<?= htmlspecialchars(StrategyChangeHistory::narrate($selectedStrategyChangeEvents)) ?>">Changes: <strong><?= (int)$selectedStrategyChangeCount ?></strong></span><?php endif; ?>
            <?php if ($tradingStyle): ?><span class="hero-meta-chip">Style: <strong><?= htmlspecialchars(ucwords(str_replace('_', ' ', $tradingStyle))) ?></strong></span><?php endif; ?>
            <?php if (!empty($declaredInstruments)): ?><span class="hero-meta-chip">Instruments: <strong><?= htmlspecialchars(implode(', ', array_slice($declaredInstruments, 0, 4))) ?><?= count($declaredInstruments) > 4 ? ' +' . (count($declaredInstruments) - 4) : '' ?></strong></span><?php endif; ?>
            <?php if (!empty($declaredSessions)): ?><span class="hero-meta-chip">Sessions: <strong><?= htmlspecialchars(implode(', ', $declaredSessions)) ?></strong></span><?php endif; ?>
            <span class="hero-meta-chip"><?= htmlspecialchars($label) ?>: <strong><?= $start->format('M j') ?> - <?= $end->format('M j, Y') ?></strong></span>
        </div>
        <div class="hero-kpi-grid">
            <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('hero.win_rate')) ?>">Win Rate ⓘ</p><p class="value"><?= $totalWinRate ?>%</p></div>
            <?php
            // A metric that genuinely belongs to a higher period tier (Profit
            // Factor/Expectancy need depth>=2 i.e. Monthly+; Max Drawdown needs
            // depth>=3 i.e. Quarterly+, and Pro tier) must not appear at all on a
            // lower tier, previously every one of these rendered its tile
            // unconditionally and showed a bare "—" with a tooltip revealing the
            // real reason was "not available at this tier yet", which is exactly
            // the anti-pattern this session's Reports pass fixes. The tile is now
            // only rendered once the period tier (and, for Max Drawdown,
            // subscription tier) actually qualifies, at which point a null value
            // is a genuine data-availability case (e.g. "no losing trades yet"),
            // not a tier gate, and still gets its dash + tooltip.
            // Coaching tooltip is always shown now (item 1); when a data-
            // availability reason also applies, it's prepended so the tile still
            // explains why it's showing a dash, not just what the metric means in
            // general.
            $heroPfReason  = $profitFactor !== null ? '' : "No losing trades yet, nice work. Profit Factor compares wins to losses, so it will show once your history includes both.\n\n";
            $heroDdReason  = $maximumDrawdown !== null ? '' : "No drawdown this period, nice, your P&L never dipped below where it started.\n\n";
            $heroPfTitle = $heroPfReason . reportsCoach('hero.profit_factor');
            $heroDdTitle = $heroDdReason . reportsCoach('hero.max_drawdown');
            ?>
            <?php if ($depth >= 2): ?>
            <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($heroPfTitle) ?>">Profit Factor ⓘ</p><p class="value"><?= $profitFactor !== null ? number_format($profitFactor, 2) : '-' ?></p></div>
            <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('hero.expectancy')) ?>">Expectancy ⓘ</p><p class="value <?= $expectancy !== null ? pnlClass($expectancy) : '' ?>"><?= $expectancy !== null ? pnlFormat($expectancy) : '-' ?></p></div>
            <?php endif; ?>
            <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('hero.total_trades')) ?>">Total Trades ⓘ</p><p class="value"><?= $totalTrades ?></p><p class="sub"><?= $totalWins ?>W / <?= $totalLosses ?>L / <?= $totalBreakevens ?>BE</p></div>
            <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('hero.net_profit')) ?>">Net Profit ⓘ</p><p class="value <?= pnlClass($totalPnl) ?>"><?= pnlFormat($totalPnl) ?></p></div>
            <?php if ($depth >= 3 && $isProOrAbove): ?>
            <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($heroDdTitle) ?>">Max Drawdown ⓘ</p><p class="value <?= $maximumDrawdown !== null ? 'negative' : '' ?>"><?= $maximumDrawdown !== null ? pnlFormat((float)$maximumDrawdown) : '-' ?></p></div>
            <?php endif; ?>
        </div>
        <div class="compare-row">
            <div class="compare-item"><p class="clabel"><?= $label ?></p><p class="cvalue <?= pnlClass($totalPnl) ?>"><?= pnlFormat($totalPnl) ?></p></div>
            <div class="compare-item"><p class="clabel">All-Time</p><p class="cvalue <?= pnlClass($allTimePnl) ?>"><?= pnlFormat($allTimePnl) ?></p></div>
        </div>
    </div>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Performance Overview, headline charts, side-by-side (always visible) -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <div class="chart-row">
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Strategy Performance Table + Chart (Starter+) -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($isStarterOrAbove): ?>
        <?php if (!empty($strategyPerformance)): ?>
        <div class="section-card">
            <h3>Strategy Performance</h3>
            <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.5"><?= htmlspecialchars(ProgressiveInsightsEngine::strategyPerformance($period, $strategyPerformance)) ?></p>
            <div style="overflow-x:auto;margin-bottom:1rem">
                <table class="setup-table">
                    <thead><tr><th>Strategy</th><th>Trades</th><th>Win Rate</th><th>Total P&amp;L</th><th>Avg Risk</th><th>Best Trade</th><th>Worst Trade</th><th></th></tr></thead>
                    <tbody>
                        <?php foreach ($strategyPerformance as $sp):
                            $spTrades  = (int)$sp['trades'];
                            $spWins    = (int)$sp['wins'];
                            $spWinRate = $spTrades > 0 ? round(($spWins / $spTrades) * 100, 1) : 0;
                        ?>
                        <tr>
                            <td><strong><?= htmlspecialchars($sp['name']) ?></strong></td>
                            <td><?= $spTrades ?></td>
                            <td><?= $spWinRate ?>%</td>
                            <td class="<?= pnlClass((float)$sp['total_pnl']) ?>"><?= pnlFormat((float)$sp['total_pnl']) ?></td>
                            <td><?= round((float)$sp['avg_risk'], 2) ?>%</td>
                            <td class="<?= $sp['best_trade'] !== null ? 'positive' : '' ?>"><?= $sp['best_trade'] !== null ? pnlFormat((float)$sp['best_trade']) : 'No wins yet' ?></td>
                            <td class="<?= $sp['worst_trade'] !== null ? 'negative' : '' ?>"><?= $sp['worst_trade'] !== null ? pnlFormat((float)$sp['worst_trade']) : 'No losses yet' ?></td>
                            <!-- Journal Trades supports strategy_version_id filtering, a
                                 real filtered list, not just the current account. -->
                            <td><a href="<?= journalTradesFilterUrl(['strategy_version_id' => $sp['version_id'], 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link">View trades →</a></td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <p style="font-size:.68rem;color:var(--foreground-dim)">Showing data for <?= $label ?>: <?= $start->format('M j, Y') ?> - <?= $end->format('M j, Y') ?></p>
            <?php if (!empty($strategyChartLabels)): ?>
            <div class="chart-wrap"><canvas id="strategyPnlChart"></canvas></div>
            <?php endif; ?>
        </div>
        <?php else: ?>
        <div class="section-card"><div class="empty-state"><p>No strategy data available for this period.</p></div></div>
        <?php endif; ?>
    <?php else: ?>
    <div class="locked-card">
        <div class="lock-icon">🔒</div>
        <h3>Strategy Performance</h3>
        <p>Upgrade to <strong>Pro</strong> to see per-strategy performance metrics and P&amp;L charts for each period.</p>
        <a href="/traderalign/pricing" class="btn btn-primary btn-sm">View Plans →</a>
    </div>
    <?php endif; ?>
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Instrument P&L Chart -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if (!empty($instrumentChartRows)): ?>
    <div class="section-card">
        <h3>Instrument P&amp;L</h3>
        <div class="chart-wrap"><canvas id="instrumentChart"></canvas></div>
        <!-- Links to this period's whole trade set rather than one
             arbitrarily-picked account, this chart is already aggregated
             across every account in the active Prop Firms/Live Accounts
             view, so an account-specific link here would misrepresent it. -->
        <div style="margin-top:.75rem;padding-top:.5rem;border-top:1px solid var(--border)">
            <a href="<?= journalTradesFilterUrl(['account_type' => match ($accountView) { 'prop' => 'prop_firm', 'live' => 'personal', default => null }, 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link">View this <?= htmlspecialchars(periodNoun($period)) ?>'s trades in Journal →</a>
        </div>
    </div>
    <?php endif; ?>
    </div>
    <?php endif; /* totalTrades > 0 */ ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Key Analytics & Expandable Details, progressive disclosure       -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- (Removed) Strategy Health accordion. Its two cards, Classification -->
    <!-- Distribution and Boundary Analytics, became "Classification        -->
    <!-- Analysis" and "Boundary Analysis" steps inside the (also since       -->
    <!-- deleted) "4-Stage Performance Audit" accordion. Classification      -->
    <!-- Distribution now lives in the 4-Stage Summary's own pie chart       -->
    <!-- below; Boundary Analysis is its own standalone section further      -->
    <!-- down this page (see that section's comment for why it was          -->
    <!-- reconnected only after fixing its lifecycle_state bug). Execution   -->
    <!-- Score (this accordion's old header badge) is superseded by Edge     -->
    <!-- Health, now its own standalone card further down this page,        -->
    <!-- see the comment above $executionScore's computation.                -->
    <!-- ════════════════════════════════════════════════════════════════ -->

    <div class="accordion-group" id="accordion-trade-analysis">
        <button type="button" class="accordion-group-header" onclick="toggleAccordionGroup('accordion-trade-analysis')">
            <span class="accordion-group-title"><span class="accordion-icon">📈</span>Trade Analysis</span>
            <span class="accordion-group-summary"><span><strong><?= $totalTrades ?></strong> trades this period</span></span>
            <span class="accordion-chevron">▾</span>
        </button>
        <div class="accordion-group-body">
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- (Removed) Trade Summary and Instrument Performance cards. Both    -->
    <!-- are now merged into the "Instrument Analysis" step (along with a  -->
    <!-- new classification pie chart) inside the new 4-Stage Performance  -->
    <!-- Audit section below, per the Reports 4-Stage rebuild.             -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($totalTrades > 0): ?>
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Asset Class Edge Detection, Quarterly/Annual only (depth >= 3).   -->
    <!-- "Edge" is a claim about a real, repeatable advantage; it needs a   -->
    <!-- real sample and a real time horizon (~90 days), a single trade   -->
    <!-- in a week or month can't support it. Weekly/Monthly render a      -->
    <!-- plain, factual Best/Worst Instrument card instead (below) so this -->
    <!-- accordion never goes blank. See CLAUDE.md's 2026-07-26 edge-      -->
    <!-- language restriction.                                             -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($depth >= 3 && !empty($assetClassData)):
        $globalAbsPnl = array_sum(array_map(fn($ac) => abs($ac['total_pnl']), $assetClassData));
    ?>
    <div class="section-card">
        <h3>Asset Class Edge Detection</h3>
        <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Where your edge lives, based on <?= htmlspecialchars($strategyDisplayName) ?> trading data</p>
        <?php
        $assetClassForNarrative = [];
        foreach ($assetClassData as $className => $acRow) { $assetClassForNarrative[] = ['instrument' => $className, 'total_pnl' => $acRow['total_pnl'], 'trades' => $acRow['trades']]; }
        ?>
        <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.5"><?= htmlspecialchars(ProgressiveInsightsEngine::edge($period, $assetClassForNarrative)) ?></p>
        <?php foreach ($assetClassData as $className => $acData):
            $acTrades       = $acData['trades'];
            $acWinRate      = $acData['win_rate'];
            $acPnl          = round($acData['total_pnl'], 2);
            $acPnlPct       = $globalAbsPnl > 0 ? round((abs($acPnl) / $globalAbsPnl) * 100, 1) : 0;
            $acAvgRisk      = $acData['avg_risk'];
            $acRiskBreaches = $acData['total_risk_breaches'];

            if ($acPnl > 0 && $acWinRate > 50 && $acTrades >= 5) {
                $edgeLabel = 'Strong Edge'; $edgeClass = 'edge-strong';
                $edgeInsight = "This category is performing well. Your execution here aligns with your declared strategy.";
            } elseif ($acPnl > 0 && $acTrades >= 5) {
                $edgeLabel = 'Edge Found'; $edgeClass = 'edge-found';
                $edgeInsight = "This category shows positive returns.";
            } elseif ($acPnl > 0 && $acTrades < 5) {
                $edgeLabel = 'Early Signal'; $edgeClass = 'edge-early';
                $edgeInsight = "Emerging performance, more trades needed for confidence.";
            } elseif ($acPnl <= 0 && $acTrades >= 5) {
                $edgeLabel = 'No Edge'; $edgeClass = 'edge-none';
                $inDeclaredStr = $acData['in_declared'] ? 'These instruments ARE in your declared strategy' : 'These instruments are NOT in your declared strategy';
                $edgeInsight = "This category is underperforming. Your average risk was {$acAvgRisk}% against a declared cap of {$declaredMaxRiskPct}%. {$acRiskBreaches} of {$acTrades} trades exceeded your risk limit. {$inDeclaredStr}, review your execution quality and risk sizing.";
            } else {
                $edgeLabel = 'Insufficient Data'; $edgeClass = 'edge-insufficient';
                $edgeInsight = "More trades needed to assess this category.";
            }
        ?>
        <div style="background:var(--background);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem;margin-bottom:.75rem">
            <div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:.5rem;margin-bottom:.5rem">
                <strong style="font-size:.9rem;color:var(--foreground)"><?= htmlspecialchars($className) ?></strong>
                <span class="edge-badge <?= $edgeClass ?>"><?= $edgeLabel ?></span>
            </div>
            <div class="stat-grid" style="margin-bottom:.5rem">
                <div class="stat-tile"><p class="label">Trades</p><p class="value" style="font-size:1rem"><?= $acTrades ?></p></div>
                <div class="stat-tile"><p class="label">Win Rate</p><p class="value" style="font-size:1rem"><?= $acWinRate ?>%</p></div>
                <div class="stat-tile"><p class="label">Total P&amp;L</p><p class="value <?= pnlClass($acPnl) ?>" style="font-size:1rem"><?= pnlFormat($acPnl) ?></p></div>
                <div class="stat-tile"><p class="label">% of Total P&amp;L</p><p class="value" style="font-size:1rem"><?= $acPnlPct ?>%</p></div>
            </div>
            <div class="insight-text" style="font-size:.72rem;padding:.5rem .75rem"><?= htmlspecialchars($edgeInsight) ?></div>
        </div>
        <?php endforeach; ?>
    </div>
    <?php elseif ($depth < 3 && !empty($instrumentDetailRows)):
        // Best/Worst Instrument, factual-only fallback for Weekly/Monthly.
        // No edge/pattern/signal language (see the restriction note above).
        // Reuses $instrumentDetailRows, already computed above for the
        // Instrument P&L chart / Instrument Analysis, no new query.
        $biRanked = $instrumentDetailRows;
        usort($biRanked, fn($a, $b) => (float)$b['total_pnl'] <=> (float)$a['total_pnl']);
        $biBest = $biRanked[0];
        $biWorst = $biRanked[count($biRanked) - 1];
        $biSingle = count($biRanked) < 2 || $biBest['instrument'] === $biWorst['instrument'];
        $biPeriodLabel = $period === 'weekly' ? 'This Week' : 'This Month';
        $biEntries = $biSingle
            ? [['label' => 'Only Instrument Traded', 'row' => $biBest]]
            : [['label' => 'Best', 'row' => $biBest], ['label' => 'Worst', 'row' => $biWorst]];
    ?>
    <div class="section-card">
        <h3>Best<?= $biSingle ? '' : ' &amp; Worst' ?> Instrument, <?= $biPeriodLabel ?></h3>
        <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Factual instrument performance only, a full edge/asset-class breakdown needs a longer time horizon (see Quarterly or Annual reports)</p>
        <?php foreach ($biEntries as $biEntry):
            $biRow = $biEntry['row'];
            $biPnl = round((float)$biRow['total_pnl'], 2);
        ?>
        <div style="background:var(--background);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem;margin-bottom:.75rem">
            <div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:.5rem;margin-bottom:.5rem">
                <span style="font-size:.62rem;text-transform:uppercase;letter-spacing:.05em;color:var(--foreground-dim)"><?= htmlspecialchars($biEntry['label']) ?></span>
                <strong style="font-size:.9rem;color:var(--foreground)"><?= htmlspecialchars($biRow['instrument']) ?></strong>
            </div>
            <div class="stat-grid">
                <div class="stat-tile"><p class="label">Trades</p><p class="value" style="font-size:1rem"><?= (int)$biRow['trades'] ?></p></div>
                <div class="stat-tile"><p class="label">Win Rate</p><p class="value" style="font-size:1rem"><?= $biRow['win_rate'] ?>%</p></div>
                <div class="stat-tile"><p class="label">Total P&amp;L</p><p class="value <?= pnlClass($biPnl) ?>" style="font-size:1rem"><?= pnlFormat($biPnl) ?></p></div>
            </div>
        </div>
        <?php endforeach; ?>
    </div>
    <?php endif; ?>
    <?php endif; ?>
        </div>
    </div>

    <?php
    // ════════════════════════════════════════════════════════════════════
    // 4-STAGE PERFORMANCE AUDIT, the canonical rebuild. One structure per
    // reporting period, built entirely from AnalyticsEngine::
    // getFourStagePeriodSummary() (same single source of truth Journal
    // Trades' per-trade 4-Stage view uses via computeFourStageRaw()), plus
    // EdgeHealthEngine::getScore() and AnalyticsEngine::getStyleDriftSignal().
    // ════════════════════════════════════════════════════════════════════
    $tfsHasData = !empty($trueFourStageSummary) && (int)($trueFourStageSummary['trades_in_period'] ?? 0) > 0;
    if ($tfsHasData) {
        $tfsAccountType = match ($accountView) { 'prop' => 'prop_firm', 'live' => 'personal', default => null };
    }
    // Minimum trades in the period before "highlight" cards (largest win/
    // loss, most disciplined) are shown at all. Below this, calling the
    // only trade taken "the most disciplined" or "the largest win" implies
    // a comparison that can't exist with a sample that small, an empty
    // state is shown instead (see the consolidated Trade Highlights card
    // below).
    $minTradesForHighlights = 3;

    /** One-line, real (never invented) explanation for why a trade was picked as "most disciplined". */
    function disciplineReason($raw) {
        $gc = $raw['geometric_consistency'];
        $riskOverage = (!empty($raw['stage1']['available']) && $raw['stage1']['risk_overage'] !== null) ? abs($raw['stage1']['risk_overage']) : null;
        $rrMismatch = !empty($gc['rr_mismatch_flag']);
        $riskMismatch = !empty($gc['risk_pct_mismatch_flag']);
        $hasOverage = $riskOverage !== null && $riskOverage > 0.01;
        if (!$hasOverage && !$rrMismatch && !$riskMismatch) {
            return 'Stayed within declared risk, with a saved R:R and risk % that both match its own entry/SL/TP prices.';
        }
        $parts = [];
        if ($hasOverage) $parts[] = 'risk ran ' . number_format($riskOverage, 2) . ' percentage points over the declared limit';
        if ($rrMismatch) $parts[] = "saved R:R didn't quite match its entry/SL/TP prices";
        if ($riskMismatch) $parts[] = "saved risk % didn't quite match the dollar risk actually taken";
        return 'Closest to plan of this period\'s trades, though ' . implode(' and ', $parts) . '.';
    }

    /** Renders one compact highlight row (largest win / largest loss / most disciplined), instrument, account, net P&L, and a one-line reason. No invented deviation metric. */
    function renderHighlightRow($raw, $label, $reason, $badgeColor, $accountLabelsById) {
        if (!$raw) return '';
        $pnl = $raw['stage3']['effective_pnl'];
        $html  = '<div style="background:var(--background);border:1px solid var(--border);border-left:3px solid ' . $badgeColor . ';border-radius:var(--radius-lg);padding:.75rem 1rem;margin-bottom:.6rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem 1rem">';
        $html .= '<div style="min-width:0">';
        $html .= '<div style="display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap;margin-bottom:.15rem">';
        $html .= '<span style="font-size:.62rem;text-transform:uppercase;letter-spacing:.05em;color:var(--foreground-dim)">' . htmlspecialchars($label) . '</span>';
        $html .= '<strong style="font-family:var(--font-sans);font-size:.9rem;color:var(--foreground)">' . htmlspecialchars($raw['instrument']) . '</strong>';
        $html .= '<span style="font-size:.75rem;color:var(--foreground-muted)">' . htmlspecialchars(accountLabelFor($accountLabelsById, $raw['account_id'])) . '</span>';
        $html .= '<span class="value ' . pnlClass((float)($pnl ?? 0)) . '" style="font-size:.9rem">' . ($pnl !== null ? pnlFormat($pnl) : '-') . '</span>';
        $html .= '</div>';
        $html .= '<div style="font-size:.72rem;color:var(--foreground-dim)">' . htmlspecialchars($reason) . '</div>';
        $html .= '</div>';
        $html .= '<a href="' . journalAccountUrl(null, null, $raw['account_id'], $raw['trade_id']) . '" class="drill-link" style="flex-shrink:0">View trade →</a>';
        $html .= '</div>';
        return $html;
    }
    ?>
    <?php
    // ════════════════════════════════════════════════════════════════════
    // 4-STAGE SUMMARY, built 2026-07-26 from Kevin's exact visual
    // prototype. Period-level aggregates only (rates/counts/averages/
    // distributions), no individual Entry/SL/TP prices, no TP2-10, no
    // single trade's lot size. That per-trade detail is Journal's 4-Stage
    // Audit (a later session). Prop/Live tabs only, Comparison stays a
    // placeholder pending its own redesign (matches the established rule
    // that any computation blending both account types belongs there, not
    // in Reports' main body).
    // ════════════════════════════════════════════════════════════════════
    if ($accountView !== 'comparison'):
        $fsAccountType = match ($accountView) { 'prop' => 'prop_firm', 'live' => 'personal', default => null };
        $fsAllAccounts = $accountView === 'prop' ? $propAccounts : $liveAccounts;
        $fsContributingAccounts = $accountView === 'prop' ? $periodContributingPropAccounts : $periodContributingLiveAccounts;

        // Account-scope dropdown, reuses the page's existing `account_id`
        // GET param (same one the old Trade Set card's dropdown used, and
        // that $activeAccount already resolves) rather than a second,
        // separate param, per instruction. Empty/absent means the
        // aggregate "All Prop Firms"/"All Live Accounts" state, which is
        // this section's default, $activeAccount itself can't express
        // that (pickSelectedAccount() always falls back to the first
        // account), so this section resolves the raw GET value directly.
        // Now fully functional (previously rendered correctly but never
        // re-scoped anything, see CLAUDE.md's account-selector fix note):
        // selecting one account re-scopes every number in this section —
        // Top Summary Metrics, Stage 1-4, Commentary, Classification
        // Distribution, Recommendations, to that one account's trades for
        // the selected period, via $fsSelectedAccountId/$fsAcctCond*
        // below. Reuses $scopedAccountId (resolved once, early in this file,
        // right after $propAccounts/$liveAccounts load) rather than
        // re-deriving it here, since $scopedAccountId already scopes
        // $trueFourStageSummary/$auditCommentary/$auditRecommendations
        // above, a second, independent resolution here could otherwise
        // drift from that one.
        $fsSelectedAccountId = $scopedAccountId;
        $fsSelectedAccountLabel = null;
        if ($fsSelectedAccountId !== '') {
            foreach ($fsAllAccounts as $fa) { if ((string)$fa['id'] === (string)$fsSelectedAccountId) { $fsSelectedAccountLabel = $fa['label']; break; } }
        }
        $fsScopeLabel = $fsSelectedAccountLabel ?? ($accountView === 'prop' ? 'All Prop Firms' : 'All Live Accounts');

        // Account-condition SQL fragments, appended to every query this
        // section runs so the whole section re-scopes together. `T` variants
        // are for queries already joined to trading_accounts as `ta` (alias
        // `t.account_id`); the bare variant is for queries against a plain
        // `trades` table with no such join, same T/bare split convention
        // as $svidConditionT/$svidCondition and $avTypeFilter/$avAccountIdFilter
        // above.
        $fsAcctCondT = $fsSelectedAccountId !== '' ? ' AND t.account_id = :facctid' : '';
        $fsAcctCond  = $fsSelectedAccountId !== '' ? ' AND account_id = :facctid' : '';
        $fsAcctParams = $fsSelectedAccountId !== '' ? [':facctid' => $fsSelectedAccountId] : [];

        $fsDateRangeLabel = $start->format('j M') . ' - ' . $end->format('j M Y') . ' (' . ucfirst($period) . ')';
        // Proper period noun ("week"/"month"/"quarter"/"year") for use in
        // sentences, $period itself ("weekly"/"monthly"/...) is adjective-
        // shaped and reads wrong mid-sentence ("this weekly"). Shared helper
        // (src/RenderHelpers.php::periodNoun()), previously duplicated
        // ad hoc in six places in progressive_insights_engine.php plus this
        // file's own copy; one of those six (crossStrategy()'s weekly/monthly
        // branch) had been missed and still emitted "this weekly"/"this
        // monthly" verbatim until this pass consolidated all of them here.
        $fsPeriodNoun = periodNoun($period);

        // Top Summary Metrics, section-local, account-scoped copies of the
        // page-level $st/$sPrev stats query (lines ~541/572 above), with
        // $fsAcctCondT folded in. Deliberately NOT reusing $totalTrades/
        // $totalWinRate/$totalPnl/$deltaWinRate/$deltaTotalPnl directly:
        // those remain the page-wide, every-account-of-this-type aggregate
        // that other sections of this page (Trade Set hero, Session/
        // Instrument Performance, etc.) still rely on, re-pointing them at
        // one selected account would have silently re-scoped those other
        // sections too. This section gets its own $fs*-prefixed numbers so
        // the account selector only ever affects the 4-Stage Summary.
        $fsPeriodParamsScoped = array_merge($periodParams, $fsAcctParams);
        $fsSt = $db->raw(
            "SELECT COUNT(*) AS total_trades,
                SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins,
                COALESCE(SUM($pnlExpr), 0) AS total_pnl
             FROM $tradesAccountsJoin
             WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL
             AND t.closed_at>=:s AND t.closed_at<=:e" . $svidConditionT . $fsAcctCondT,
            $fsPeriodParamsScoped
        )[0] ?? [];
        $fsTotalTrades  = (int)($fsSt['total_trades'] ?? 0);
        $fsTotalWins    = (int)($fsSt['wins'] ?? 0);
        $fsTotalPnl     = round((float)($fsSt['total_pnl'] ?? 0), 2);
        $fsTotalWinRate = $fsTotalTrades > 0 ? round(($fsTotalWins / $fsTotalTrades) * 100, 1) : 0;

        $fsPrevPeriodParamsScoped = array_merge($prevPeriodParams, $fsAcctParams);
        $fsStPrev = $db->raw(
            "SELECT COUNT(*) AS total_trades,
                SUM(CASE WHEN $pnlExpr > 0 THEN 1 ELSE 0 END) AS wins,
                COALESCE(SUM($pnlExpr), 0) AS total_pnl
             FROM $tradesAccountsJoin
             WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL
             AND t.closed_at>=:s AND t.closed_at<=:e" . $svidConditionT . $fsAcctCondT,
            $fsPrevPeriodParamsScoped
        )[0] ?? [];
        $fsPrevTotalTrades  = (int)($fsStPrev['total_trades'] ?? 0);
        $fsPrevTotalWins    = (int)($fsStPrev['wins'] ?? 0);
        $fsPrevTotalPnl     = round((float)($fsStPrev['total_pnl'] ?? 0), 2);
        $fsPrevTotalWinRate = $fsPrevTotalTrades > 0 ? round(($fsPrevTotalWins / $fsPrevTotalTrades) * 100, 1) : null;

        $fsDeltaWinRate  = ($fsPrevTotalTrades > 0) ? round($fsTotalWinRate - $fsPrevTotalWinRate, 1) : null;
        $fsDeltaTotalPnl = ($fsPrevTotalTrades > 0) ? round($fsTotalPnl - $fsPrevTotalPnl, 2) : null;

        // Ending Balance, an all-time-anchored account fact (current_balance,
        // right now), per this file's established convention that account
        // facts stay all-time. Every period tab here represents "current"
        // (no prev/next navigation exists), so current_balance already IS
        // the true end-of-selected-period balance. With one account
        // selected, this is simply that account's own current_balance;
        // otherwise summed across whichever accounts contributed a trade
        // this period (falling back to every account of this type if none
        // contributed, so the tile still shows a real number rather than $0
        // for an all-quiet period).
        if ($fsSelectedAccountId !== '') {
            $fsBalanceAccounts = array_filter($fsAllAccounts, fn($fa) => (string)$fa['id'] === (string)$fsSelectedAccountId);
        } else {
            $fsBalanceAccounts = !empty($fsContributingAccounts) ? $fsContributingAccounts : $fsAllAccounts;
        }
        $fsEndingBalance = 0.0;
        foreach ($fsBalanceAccounts as $fba) $fsEndingBalance += (float)($fba['current_balance'] ?? 0);
        $fsEndingBalance = round($fsEndingBalance, 2);
        $fsDeltaEndingBalance = $fsDeltaTotalPnl; // balance moves by exactly the period's own net P&L (same approximation as page-level $deltaEndingBalance)

        // Max Drawdown, period-scoped, computed directly from this
        // period's own ordered P&L sequence (running peak minus trough of
        // cumulative P&L within the period), not from the account's real
        // all-time equity curve. Deliberately self-contained: works
        // identically whether "All" or one account is in scope, and needs
        // no new engine method.
        $fsDdRows = $db->raw(
            "SELECT $pnlExpr AS pnl FROM $tradesAccountsJoin
             WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL
             AND t.closed_at>=:s AND t.closed_at<=:e" . $svidConditionT . $fsAcctCondT . "
             ORDER BY t.closed_at ASC",
            $fsPeriodParamsScoped
        );
        $fsCum = 0.0; $fsPeak = 0.0; $fsMaxDd = 0.0;
        foreach ($fsDdRows as $fdd) {
            $fsCum += (float)$fdd['pnl'];
            if ($fsCum > $fsPeak) $fsPeak = $fsCum;
            $fsDdHere = $fsPeak - $fsCum;
            if ($fsDdHere > $fsMaxDd) $fsMaxDd = $fsDdHere;
        }
        $fsMaxDrawdown = round($fsMaxDd, 2);
        $fsMaxDrawdownPct = $fsEndingBalance > 0 ? round(($fsMaxDrawdown / $fsEndingBalance) * 100, 1) : null;

        // Expectancy, realized $ per trade this period. Simple, standard
        // definition (net P&L / trade count); doesn't need a win/loss
        // breakdown the way Edge Health's R-multiple expectancy does.
        $fsExpectancy = $fsTotalTrades > 0 ? round($fsTotalPnl / $fsTotalTrades, 2) : null;

        // Prior period's Stage 1-4 summary, only for the Recommendations'
        // "RR trend" bullet below (compares this period's avg planned R:R
        // to last period's). Same try/catch pattern as the main call.
        // $fsSelectedAccountId threaded through so a selected account's
        // "prior period" comparison is against that same account, not the
        // whole account-view aggregate.
        try {
            $trueFourStageSummaryPrev = AnalyticsEngine::getFourStagePeriodSummary(
                $userId, $accountView, $prevStartStr, $prevEndStr, $svidCondition, $svidParams, $fsSelectedAccountId ?: null
            );
        } catch (\Throwable $e) {
            $trueFourStageSummaryPrev = ['trades_in_period' => 0];
        }

        // Discipline Score / Consistency Score, period-scoped, ungated
        // sibling of Edge Health (reuses its exact scoring core, just
        // without the 100-trade confidence gate). n-aware tier drives the
        // wording below, not a different formula per tier.
        $fsPeriodFactors = EdgeHealthEngine::getPeriodFactors($userId, $accountView, $startStr, $endStr, $svidCondition, $svidParams, $fsSelectedAccountId ?: null);

        /** Qualitative tag for a 0-100 score. */
        // $tier overrides the tag itself (not just a tooltip) for the two
        // lowest-confidence tiers, a 1-2 trade period showing a confident
        // "Fair"/"Poor" tag right next to the raw number would still read
        // as a real judgment even with a caveat sentence underneath it.
        // Confident/developing/small_sample tiers keep the real qualitative
        // tag (the number itself is genuinely computed at those sizes,
        // just with a caveat noted separately below).
        function fsScoreTag($score, $tier = 'confident') {
            if ($tier === 'single_trade' || $tier === 'very_small_sample') return ['Too Early', 'pill-neutral'];
            if ($score === null) return ['-', 'pill-neutral'];
            if ($score >= 80) return ['Excellent', 'pill-green'];
            if ($score >= 65) return ['Good', 'pill-green'];
            if ($score >= 50) return ['Fair', 'pill-amber'];
            if ($score >= 35) return ['Weak', 'pill-orange'];
            return ['Poor', 'pill-red'];
        }
        $fsTierNote = match ($fsPeriodFactors['tier'] ?? 'none') {
            'single_trade' => 'Only 1 trade this ' . $fsPeriodNoun . ', not enough of a sample to trust this number yet.',
            'very_small_sample' => 'Based on just ' . $fsPeriodFactors['n'] . ' trades this ' . $fsPeriodNoun . ', treat as directional, not confident.',
            'small_sample' => 'Based on ' . $fsPeriodFactors['n'] . ' trades this ' . $fsPeriodNoun . ', still a fairly small sample.',
            'developing' => 'Based on ' . $fsPeriodFactors['n'] . ' trades, a real, computed number, still settling as more trades close.',
            default => null,
        };

        // Classification Distribution, relabeled onto the A+/A/B/C tiers
        // Kevin confirmed (2026-07-26): reuses the existing classification
        // engine's real output (sp_classify_trade), no new grading system.
        // A+ = disciplined_win. A = controlled_loss, breakeven (plan
        // respected, no behavioral violation). B = elevated_win (the one
        // real "minor deviation" case, there is no distinct "elevated
        // loss" value in the schema). C = loss_high_risk, tilt_warning,
        // revenge_trade, matches api/analytics/behavior.php's existing
        // "undisciplined" bucket exactly (confirmed precedent, not a new
        // grouping invented for this page). Sourced from
        // src/TradeGrading.php (2026-08-08), was hand-duplicated here and
        // in TraderProfileBehaviorEngine::GRADE_MAP, now the one shared
        // constant both read (docs/architecture/SINGLE_SOURCE_OF_TRUTH_AUDIT.md
        // §9, Wave 4d).
        $fsGradeMap = TradeGrading::GRADE_MAP;
        $fsGradeColors = ['A+' => '#26A69A', 'A' => '#60A5FA', 'B' => '#E09B3A', 'C' => '#EF5350'];
        $fsGradeLabels = ['A+' => 'A+, Disciplined Wins', 'A' => 'A, Disciplined Managed Outcomes', 'B' => 'B, Minor Deviations', 'C' => 'C, Undisciplined Trades'];
        // Coaching copy for the hover, the tier letter alone ("A+ 40%")
        // labels without teaching; this explains what actually landed a
        // trade in that bucket, in plain language, so hovering is itself a
        // small lesson rather than a lookup. Centralized in
        // src/ReportsCoaching.php (item 1, 2026-07-27 finishing pass) rather
        // than hardcoded here, same content, single source now.
        $fsGradeCoaching = [
            'A+' => reportsCoach('classification.a_plus'),
            'A'  => reportsCoach('classification.a'),
            'B'  => reportsCoach('classification.b'),
            'C'  => reportsCoach('classification.c', ['period' => $fsPeriodNoun]),
        ];
        // Short (2-4 word) chart-only labels, separate from $fsGradeCoaching
        // above. A Chart.js tooltip is a hover-to-glance UI element, not a
        // reading surface: the full coaching paragraph used to render inside
        // afterLabel() and visually cover most of the pie (2026-07-27 item 1
        // finishing pass). Derived from $fsGradeLabels by stripping the
        // "X, " grade-letter prefix, so there's still one source of truth
        // for the wording itself, only the delivery mechanism differs.
        $fsGradeChartLabel = array_map(fn($l) => preg_replace('/^.+?, /', '', $l), $fsGradeLabels);
        // Section-local classification breakdown, mirrors the page-level
        // $classificationRows query (bare `trades` table) with $fsAcctCond
        // folded in, so a selected account re-scopes this pie the same way
        // it re-scopes everything else in this section. Not reusing
        // $classificationRows directly for the same reason $totalTrades/
        // $totalWinRate aren't reused above, that variable stays the
        // page-wide aggregate other sections depend on.
        $fsClassificationRows = $db->raw(
            "SELECT trade_classification, COUNT(*) AS count FROM trades
             WHERE user_id=:uid AND (is_validation = 0 OR is_validation IS NULL) AND status='closed' AND deleted_at IS NULL
             AND closed_at>=:s AND closed_at<=:e" . $svidCondition . " " . $avAccountIdFilter . $fsAcctCond . "
             GROUP BY trade_classification ORDER BY count DESC",
            $fsPeriodParamsScoped
        );
        $fsGradeCounts = ['A+' => 0, 'A' => 0, 'B' => 0, 'C' => 0];
        foreach ($fsClassificationRows as $fcr) {
            $fcc = $fcr['trade_classification'] ?? '';
            if (isset($fsGradeMap[$fcc])) $fsGradeCounts[$fsGradeMap[$fcc]] += (int)$fcr['count'];
        }
        $fsGradeTotal = array_sum($fsGradeCounts);

        // Recommendations, starts from $auditRecommendations (already
        // computed above: over-risk occurrences, R:R geometric mismatches,
        // style drift, Edge Health), extended with two more real,
        // metric-traceable bullets rather than a parallel engine.
        // Best-session bullet uses a section-local, account-scoped session
        // breakdown (mirrors the page-level $sessionRows query) rather than
        // $sessionRows itself, for the same "don't re-scope other sections"
        // reason as $fsClassificationRows above.
        $fsRecommendations = $auditRecommendations;
        $fsSessionRows = $db->raw(
            "SELECT t.session AS session, COUNT(*) AS trades, SUM($pnlExpr) AS total_pnl
             FROM $tradesAccountsJoin
             WHERE t.user_id = :uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status = 'closed' AND t.deleted_at IS NULL
             AND t.closed_at >= :s AND t.closed_at <= :e
             AND t.session IN ('Sydney', 'Tokyo', 'London', 'New York')" . $svidConditionT . $fsAcctCondT . "
             GROUP BY t.session",
            $fsPeriodParamsScoped
        );
        $fsBestSession = null; $fsBestSessionPnl = -PHP_FLOAT_MAX;
        foreach ($fsSessionRows as $fsr) {
            if ((int)($fsr['trades'] ?? 0) > 0 && (float)($fsr['total_pnl'] ?? 0) > $fsBestSessionPnl) {
                $fsBestSessionPnl = (float)$fsr['total_pnl']; $fsBestSession = $fsr;
            }
        }
        if ($fsBestSession !== null && $fsBestSessionPnl > 0) {
            // Exact filter, journal.php's 'session' filter matches this
            // section-local query's own WHERE clause precisely.
            $fsRecommendations[] = [
                'text' => 'The ' . $fsBestSession['session'] . ' session was your best performer this ' . $fsPeriodNoun . ' (' . pnlFormat(round($fsBestSessionPnl, 2)) . ' across ' . (int)$fsBestSession['trades'] . ' trade(s)), consider weighting size toward it.',
                'url' => journalTradesFilterUrl(['account_type' => $fsAccountType, 'session' => $fsBestSession['session'], 'from' => $startDateOnly, 'to' => $endDateOnly]),
            ];
        }
        $fsCurRr = $trueFourStageSummary['stage2']['avg_risk_reward'] ?? null;
        $fsPrevRr = $trueFourStageSummaryPrev['stage2']['avg_risk_reward'] ?? null;
        if ($fsCurRr !== null && $fsPrevRr !== null && abs($fsCurRr - $fsPrevRr) >= 0.1) {
            $fsRrTrendUp = $fsCurRr > $fsPrevRr;
            $fsRecommendations[] = [
                'text' => 'Average planned reward:risk this ' . $fsPeriodNoun . ' (risking $1 to target $' . number_format($fsCurRr, 2) . ') is ' . ($fsRrTrendUp ? 'up' : 'down') . ' from last ' . $fsPeriodNoun . ' (risking $1 to target $' . number_format($fsPrevRr, 2) . '), trending toward ' . ($fsRrTrendUp ? 'higher' : 'lower') . '-reward setups.',
                'url' => journalTradesFilterUrl(array_filter(['account_type' => $fsAccountType, 'strategy_version_id' => $selectedVersionId, 'from' => $startDateOnly, 'to' => $endDateOnly])),
            ];
        }
        $fsRecommendations = array_slice($fsRecommendations, 0, 6);

        // Strategy Performance Alert, Quarterly/Annual only (matches the
        // existing edge-language restriction: Edge Health, Asset Class
        // Edge Detection are also gated to depth>=3). Confirmed with Kevin
        // (2026-07-26): a "your edge may be negative" claim stays on the
        // higher-commitment timeframes; the underlying stat is all-time
        // (not period-scoped) so nothing is lost by not showing it Weekly/
        // Monthly, the trader sees it next time they open Quarterly/Annual.
        $fsShowStrategyAlert = false; $fsAllTimeN = 0; $fsAllTimeExpectancy = 0;
        if ($depth >= 3) {
            $fsAllTimeRow = $db->raw(
                "SELECT COUNT(*) AS n, COALESCE(AVG($pnlExpr), 0) AS avg_pnl FROM $tradesAccountsJoin
                 WHERE t.user_id=:uid AND (t.is_validation = 0 OR t.is_validation IS NULL) AND t.status='closed' AND t.deleted_at IS NULL" . $svidConditionT . $fsAcctCondT,
                array_merge($baseParams, $fsAcctParams)
            );
            $fsAllTimeN = (int)($fsAllTimeRow[0]['n'] ?? 0);
            $fsAllTimeExpectancy = round((float)($fsAllTimeRow[0]['avg_pnl'] ?? 0), 2);
            $fsShowStrategyAlert = $fsAllTimeN >= 100 && $fsAllTimeExpectancy < 0;
        }
    endif;
    ?>
    <?php if ($accountView !== 'comparison'): ?>
    <div class="section-card" id="four-stage-summary">
        <div class="fs-header">
            <div>
                <h2 style="margin:0 0 .25rem;font-size:1.15rem">Reports - 4 Stage Summary (<?= htmlspecialchars(ucfirst($period)) ?>)</h2>
                <p style="font-size:.8rem;color:var(--foreground-muted);margin:0">Cumulative performance across all trades for the selected period.</p>
            </div>
            <div class="fs-header-right">
                <span class="pill pill-neutral"><?= htmlspecialchars($fsDateRangeLabel) ?></span>
                <?php if (count($fsAllAccounts) > 1): ?>
                <select class="fs-select" onchange="window.location.href='?account_view=<?= htmlspecialchars($accountView) ?>&period=<?= urlencode($period) ?>&account_id='+this.value<?= $selectedStrategy ? " + '&strategy_id=" . htmlspecialchars($selectedStrategy) . "'" : '' ?>">
                    <option value="" <?= $fsSelectedAccountId === '' ? 'selected' : '' ?>>All <?= $accountView === 'prop' ? 'Prop Firms' : 'Live Accounts' ?></option>
                    <?php foreach ($fsContributingAccounts as $fca): ?>
                    <option value="<?= htmlspecialchars($fca['id']) ?>" <?= (string)$fsSelectedAccountId === (string)$fca['id'] ? 'selected' : '' ?>><?= htmlspecialchars($fca['label']) ?></option>
                    <?php endforeach; ?>
                </select>
                <?php else: ?>
                <span class="pill pill-neutral"><?= htmlspecialchars($fsScopeLabel) ?></span>
                <?php endif; ?>
                <a class="drill-link" href="<?= journalTradesFilterUrl(array_filter(['account_type' => $fsAccountType, 'account_id' => $fsSelectedAccountId ?: null, 'strategy_version_id' => $selectedVersionId, 'from' => $startDateOnly, 'to' => $endDateOnly])) ?>">View Trades (<?= $fsTotalTrades ?>) →</a>
            </div>
        </div>

        <?php if ($selectedStrategy && $selectedStrategyChangeCount > 0): ?>
        <!-- TRADERALIGN STRATEGY BUSINESS RULES.docx §21, a compact "View
             Changes" summary for the strategy in scope, expanding into a
             real, data-driven narration + field-level history. Kept
             deliberately small (one line + a toggle), per the doc's own
             "keep the main interface clean" instruction, full before/after
             performance analysis lives in Strategy Evolution further down
             this page, not duplicated here. -->
        <div style="margin:-.75rem 0 1.25rem">
            <button class="versions-toggle" onclick="togglePanel('fs-strategy-changes', this)">Strategy: <?= htmlspecialchars($selectedStrategyName) ?>, View Changes (<?= (int)$selectedStrategyChangeCount ?>) ▾</button>
            <div id="fs-strategy-changes" class="versions-panel">
                <p style="font-size:.78rem;color:var(--foreground-muted);margin:.6rem 0 0"><?= htmlspecialchars(StrategyChangeHistory::narrate($selectedStrategyChangeEvents)) ?> Performance before and after the most recent change is shown in Strategy Evolution below.</p>
            </div>
        </div>
        <?php endif; ?>

        <?php if ($fsTotalTrades === 0): ?>
        <div class="empty-state"><p>No closed trades in this period yet.</p></div>
        <?php else: ?>

        <!-- Top Summary Metrics -->
        <div class="fs-top-metrics">
            <div class="stat-tile">
                <p class="label" title="<?= htmlspecialchars(reportsCoach('fs.total_trades')) ?>">Total Trades ⓘ</p>
                <p class="value"><?= $fsTotalTrades ?></p>
                <p class="sub">100% of period</p>
            </div>
            <div class="stat-tile">
                <p class="label" title="<?= htmlspecialchars(reportsCoach('fs.win_rate')) ?>">Win Rate ⓘ</p>
                <p class="value"><?= $fsTotalWinRate ?>%</p>
                <p class="sub <?= $fsDeltaWinRate === null ? '' : ($fsDeltaWinRate >= 0 ? 'positive' : 'negative') ?>"><?= $fsDeltaWinRate === null ? 'No prior ' . $fsPeriodNoun . ' to compare' : 'vs prev ' . $fsPeriodNoun . ' ' . ($fsDeltaWinRate >= 0 ? '+' : '') . $fsDeltaWinRate . '%' ?></p>
            </div>
            <div class="stat-tile">
                <p class="label" title="<?= htmlspecialchars(reportsCoach('fs.net_pnl')) ?>">Total Net P&amp;L ⓘ</p>
                <p class="value <?= pnlClass($fsTotalPnl) ?>"><?= pnlFormat($fsTotalPnl) ?></p>
                <p class="sub <?= $fsDeltaTotalPnl === null ? '' : ($fsDeltaTotalPnl >= 0 ? 'positive' : 'negative') ?>"><?= $fsDeltaTotalPnl === null ? 'No prior ' . $fsPeriodNoun . ' to compare' : 'vs prev ' . $fsPeriodNoun . ' ' . pnlFormat($fsDeltaTotalPnl) ?></p>
            </div>
            <div class="stat-tile">
                <p class="label" title="<?= htmlspecialchars(reportsCoach('fs.ending_balance')) ?>">Ending Balance ⓘ</p>
                <p class="value">$<?= number_format($fsEndingBalance, 2) ?></p>
                <p class="sub <?= $fsDeltaEndingBalance === null ? '' : ($fsDeltaEndingBalance >= 0 ? 'positive' : 'negative') ?>"><?= $fsDeltaEndingBalance === null ? 'No prior ' . $fsPeriodNoun . ' to compare' : 'vs prev ' . $fsPeriodNoun . ' ' . pnlFormat($fsDeltaEndingBalance) ?></p>
            </div>
            <div class="stat-tile">
                <p class="label" title="<?= htmlspecialchars(reportsCoach('fs.max_drawdown')) ?>">Max Drawdown ⓘ</p>
                <p class="value <?= $fsMaxDrawdown > 0 ? 'negative' : '' ?>"><?= $fsMaxDrawdown > 0 ? pnlFormat(-$fsMaxDrawdown) : '$0.00' ?></p>
                <p class="sub"><?= $fsMaxDrawdownPct !== null ? $fsMaxDrawdownPct . '% of balance' : '-' ?></p>
            </div>
            <div class="stat-tile">
                <p class="label" title="<?= htmlspecialchars(reportsCoach('fs.expectancy')) ?>">Expectancy ⓘ</p>
                <p class="value <?= $fsExpectancy !== null ? pnlClass($fsExpectancy) : '' ?>"><?= $fsExpectancy !== null ? pnlFormat($fsExpectancy) : '-' ?></p>
                <p class="sub">per trade</p>
            </div>
        </div>

        <?php
        $fsN = (int)$trueFourStageSummary['trades_in_period'];
        $fsS1 = $trueFourStageSummary['stage1']; $fsS2 = $trueFourStageSummary['stage2'];
        $fsS3 = $trueFourStageSummary['stage3']; $fsS4 = $trueFourStageSummary['stage4'];

        // ── Row-aligned architecture (2026-07-27 rebuild) ──────────────────
        // Every stage card now renders the SAME 6 rows in the SAME order —
        // Adherence / Risk / Reward:Risk / Trading Session / Instrument /
        // Timeframe, so the reader can scan horizontally and compare the
        // same concept across all four stages, instead of four cards each
        // starting a different subject. Metrics that aren't part of that
        // core comparison (Wins/Losses/Breakeven, Average Planned Profit/
        // Loss, Over-Risk Occurrences, Strategy Distribution, Slippage,
        // Spread Cost, Holding Time, Reconciliation Accuracy) moved into a
        // shared "Show More" section below the 6 rows, see CLAUDE.md's
        // dated entry for the full reuse/restructure/gap accounting.
        // Overflow indicator (item 3, 2026-07-27 finishing pass), every
        // instrument list on this card grid is capped to the top 4 by trade
        // count; this surfaces how many more distinct instruments exist
        // beyond that cap instead of silently truncating. Not applied to
        // session lists, sessions are a small fixed set (Sydney/Tokyo/
        // London/New York), not something that grows with trade volume.
        $fsOverflowNote = fn(array $full, array $sliced) => (count($full) - count($sliced)) > 0 ? '+' . (count($full) - count($sliced)) . ' more' : '';

        $fsSessionsSorted = $fsS1['sessions']; arsort($fsSessionsSorted); $fsSessionsSorted = array_slice($fsSessionsSorted, 0, 4, true);
        $fsS2SessionsSorted = $fsS2['sessions']; arsort($fsS2SessionsSorted); $fsS2SessionsSorted = array_slice($fsS2SessionsSorted, 0, 4, true);
        $fsStrategyDistSorted = array_slice($fsS1['strategy_distribution'], 0, 4, true); // already arsort()'d in engine.php
        $fsS1InstrumentsSorted = $fsS1['instruments']; arsort($fsS1InstrumentsSorted); $fsS1InstrumentsFull = $fsS1InstrumentsSorted; $fsS1InstrumentsSorted = array_slice($fsS1InstrumentsSorted, 0, 4, true); $fsS1InstrumentsOverflow = $fsOverflowNote($fsS1InstrumentsFull, $fsS1InstrumentsSorted);
        $fsInstrumentsSorted = $fsS2['instruments']; arsort($fsInstrumentsSorted); $fsInstrumentsFull = $fsInstrumentsSorted; $fsInstrumentsSorted = array_slice($fsInstrumentsSorted, 0, 4, true); $fsInstrumentsOverflow = $fsOverflowNote($fsInstrumentsFull, $fsInstrumentsSorted); // Stage 2's Planned Instruments, fallback for Stage 3's Executed Instruments when no actual_instrument was captured (see Row 5 caption)
        $fsS4InstrumentsSorted = $fsS4['instruments']; arsort($fsS4InstrumentsSorted); $fsS4InstrumentsFull = $fsS4InstrumentsSorted; $fsS4InstrumentsSorted = array_slice($fsS4InstrumentsSorted, 0, 4, true); $fsS4InstrumentsOverflow = $fsOverflowNote($fsS4InstrumentsFull, $fsS4InstrumentsSorted);
        // Manually-captured actual instrument/timeframe (2026-07-27 finishing
        // pass, item 2), Stage 3's own Row 5/6 data, and Stage 4's carried-
        // through timeframe distribution (see engine.php's stage4.actual_timeframes
        // comment for why this isn't broker-verified).
        $fsS3ActualInstrumentsSorted = $fsS3['actual_instruments']; $fsS3ActualInstrumentsFull = $fsS3ActualInstrumentsSorted; $fsS3ActualInstrumentsSorted = array_slice($fsS3ActualInstrumentsSorted, 0, 4, true); $fsS3ActualInstrumentsOverflow = $fsOverflowNote($fsS3ActualInstrumentsFull, $fsS3ActualInstrumentsSorted);
        $fsS3ActualTimeframesSorted = array_slice($fsS3['actual_timeframes'], 0, 4, true);
        $fsS4ActualTimeframesSorted = array_slice($fsS4['actual_timeframes'], 0, 4, true);
        // $fsS1StylesSorted/$fsS3StylesSorted/$fsS4StylesSorted/$fsStyleList
        // below are trading-STYLE (scalper/day_trader/swing/position, bucketed
        // from holding duration, engine.php's bucketHoldHoursToStyle()), kept
        // computed but deliberately NOT rendered as Row 6 "Timeframe", chart
        // timeframe (M5/H1/H4/etc.) and trading style/holding duration are
        // different concepts and must not be conflated (reverted once already
        // after a prior pass made exactly this mistake, see CLAUDE.md). Left
        // in place, unused here, per the additive-only rule, real data, may
        // be surfaced again later under its own correct "Trading Style" label.
        $fsS1StylesSorted = $fsS1['style_distribution']; arsort($fsS1StylesSorted); $fsS1StylesSorted = array_slice($fsS1StylesSorted, 0, 4, true);
        $fsS3StylesSorted = $fsS3['style_distribution']; arsort($fsS3StylesSorted); $fsS3StylesSorted = array_slice($fsS3StylesSorted, 0, 4, true);
        $fsS4StylesSorted = $fsS4['style_distribution']; arsort($fsS4StylesSorted); $fsS4StylesSorted = array_slice($fsS4StylesSorted, 0, 4, true);
        $fsStyleLabels = ['scalper' => 'Scalper', 'day_trader' => 'Day Trader', 'swing' => 'Swing', 'position' => 'Position'];
        // Stage 1/2 Timeframe, REAL data as of 2026-07-27 item 2. Strategy
        // Builder now has a mandatory Timeframes traded field (Stage 1) and
        // the Calculator now captures a per-trade planned timeframe (Stage
        // 2), see CLAUDE.md's item 2 entry for the full verification
        // trail (a prior version of this comment, and $fsTimeframeGapTitle,
        // documented this as an honest gap; that's now fixed at the
        // source, not just here).
        $fsS1TimeframesSorted = $fsS1['timeframes']; $fsS1TimeframesFull = $fsS1TimeframesSorted; $fsS1TimeframesSorted = array_slice($fsS1TimeframesSorted, 0, 4, true); $fsS1TimeframesOverflow = $fsOverflowNote($fsS1TimeframesFull, $fsS1TimeframesSorted);
        $fsS2TimeframesSorted = $fsS2['timeframes']; $fsS2TimeframesFull = $fsS2TimeframesSorted; $fsS2TimeframesSorted = array_slice($fsS2TimeframesSorted, 0, 4, true); $fsS2TimeframesOverflow = $fsOverflowNote($fsS2TimeframesFull, $fsS2TimeframesSorted);

        $fsPct = fn($count) => $fsN > 0 ? round(($count / $fsN) * 100, 1) : 0;
        // Item 5 (2026-07-28 refinement pass): never render a bare "X:1" or
        // "1:X" ratio, different trading communities read those two the
        // same way, so a reader can't safely infer which side is risk and
        // which is reward without an explicit label. This app's own stored
        // convention is "reward per 1 unit of risk" (declaredMaxProfit =
        // declaredMaxLoss * declaredRr, see engine.php), so risk is always
        // the fixed "1" and $v is always the reward side, spelled out
        // explicitly here rather than left to a bare ratio.
        $fsRatio = fn($v) => $v === null ? '-' : '1 : ' . number_format($v, 2);
        $fsPctFmt = fn($v, $d = 1) => $v === null ? '-' : number_format($v, $d) . '%';
        $fsR = fn($v) => $v === null ? '-' : (($v >= 0 ? '+' : '') . number_format($v, 2) . 'R');
        $fsHoldFmt = fn($mins) => $mins === null ? '-' : ($mins < 60 ? number_format($mins, 1) . ' min' : ($mins < 1440 ? number_format($mins / 60, 1) . ' hr' : number_format($mins / 1440, 1) . ' days'));
        $fsStyleList = function ($sorted) use ($fsPct, $fsStyleLabels) {
            $html = '<ul class="fs-stage-list">';
            foreach ($sorted as $key => $cnt) $html .= '<li><span>' . htmlspecialchars($fsStyleLabels[$key] ?? ucfirst($key)) . '</span><span>' . $fsPct($cnt) . '%</span></li>';
            return $html . '</ul>';
        };

        // $fsReconTag / Average Slippage / Holding Time / Reconciliation
        // Accuracy no longer have a render site since the 2026-07-27
        // refinement pass deleted the stage cards' "Show More" secondary
        // block outright (see CLAUDE.md's dated entry), computed here,
        // unused below, same additive-only "orphaned but harmless" pattern
        // already used elsewhere in this file. Reconciliation Accuracy/
        // Holding Time weren't migrated into the new Intent vs Reality card
        // because they measure data coverage/duration, not execution drift.
        $fsReconTag = $fsS4['reconciled_count'] === 0 ? null
            : ($fsS4['reconciliation_accuracy_pct'] >= 90 ? ['high', 'pill-green']
            : ($fsS4['reconciliation_accuracy_pct'] >= 50 ? ['medium', 'pill-amber'] : ['low', 'pill-red']));
        ?>

        <!-- Four Stage Cards, each renders the same 6 rows in the same
             order (Adherence, Risk, Reward:Risk, Trading Session,
             Instrument, Timeframe) so the grid aligns horizontally; a
             secondary block per card (below row 6) holds everything else,
             expanded together by the one "Show More" toggle underneath. -->
        <div class="fs-stage-row-wrap" id="fsStageRowWrap">
        <div class="fs-stage-row">
            <!-- Stage 1, Declared Edge (blue) -->
            <div class="fs-stage-card" style="border-left-color:var(--primary)">
                <div class="fs-stage-title">Stage 1, Declared Edge</div>
                <div class="fs-stage-sub">What was planned before the trade.</div>

                <!-- Row 1: Adherence -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage1.strategy_adherence')) ?>">Strategy Adherence ⓘ</span>
                    <?php if ($fsS1['strategy_adherence_m'] > 0): ?>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS1['strategy_adherence_pct']) ?></span>
                    <span class="fs-m-caption">(<?= $fsS1['strategy_adherence_n'] ?> of <?= $fsS1['strategy_adherence_m'] ?> trades followed the declared edge)</span>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">No strategy attached to any trade this <?= htmlspecialchars($fsPeriodNoun) ?></span>
                    <?php endif; ?>
                </div>
                <!-- Row 2: Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.risk', ['stage' => 'Declared'])) ?>">Declared Risk ⓘ</span>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS1['avg_declared_risk_pct'], 2) ?></span>
                    <span class="fs-m-caption">avg</span>
                </div>
                <!-- Row 3: Reward:Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.rr', ['stage' => 'Declared'])) ?>">Declared Risk:Reward ⓘ</span>
                    <span class="fs-m-value"><?= $fsRatio($fsS1['avg_declared_rr']) ?></span>
                    <span class="fs-m-caption">avg</span>
                </div>
                <!-- Row 4: Trading Session -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.session', ['stage' => 'Declared'])) ?>">Declared Sessions ⓘ</span>
                    <?php if (!empty($fsSessionsSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsSessionsSorted as $fsSess => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsSess) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <?php endif; ?>
                </div>
                <!-- Row 5: Instrument -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.instrument', ['stage' => 'Declared'])) ?>">Declared Instruments ⓘ</span>
                    <?php if (!empty($fsS1InstrumentsSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS1InstrumentsSorted as $fsInstr => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsInstr) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <?php if ($fsS1InstrumentsOverflow): ?><span class="fs-m-caption"><?= htmlspecialchars($fsS1InstrumentsOverflow) ?></span><?php endif; ?>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <?php endif; ?>
                </div>
                <!-- Row 6: Timeframe, real data since 2026-07-27 item 2
                     (Strategy Builder's mandatory Timeframes traded field). -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.timeframe', ['stage' => 'Declared'])) ?>">Declared Timeframe ⓘ</span>
                    <?php if (!empty($fsS1TimeframesSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS1TimeframesSorted as $fsTf => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsTf) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <?php if ($fsS1TimeframesOverflow): ?><span class="fs-m-caption"><?= htmlspecialchars($fsS1TimeframesOverflow) ?></span><?php endif; ?>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">No strategy attached to any trade this <?= htmlspecialchars($fsPeriodNoun) ?></span>
                    <?php endif; ?>
                </div>

                <div class="fs-stage-tagline" style="color:var(--primary)">Goal: Plan with edge</div>
            </div>
            <div class="fs-arrow">→</div>

            <!-- Stage 2, Calculator Intent (purple) -->
            <div class="fs-stage-card" style="border-left-color:#A78BFA">
                <div class="fs-stage-title">Stage 2, Calculator Intent</div>
                <div class="fs-stage-sub">What the calculator planned.</div>

                <!-- Row 1: Adherence -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage2.calculator_adherence')) ?>">Calculator Adherence ⓘ</span>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS2['calculator_adherence_pct']) ?></span>
                    <span class="fs-m-caption">(<?= $fsS2['calculator_adherence_n'] ?> of <?= $fsS2['calculator_adherence_m'] ?> trades matched the plan)</span>
                </div>
                <!-- Row 2: Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.risk', ['stage' => 'Planned'])) ?>">Average Planned Risk ⓘ</span>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS2['avg_risk_pct'], 2) ?></span>
                    <span class="fs-m-caption">per trade</span>
                </div>
                <!-- Row 3: Reward:Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.rr', ['stage' => 'Planned'])) ?>">Average Planned Risk:Reward ⓘ</span>
                    <span class="fs-m-value"><?= $fsRatio($fsS2['avg_risk_reward']) ?></span>
                    <span class="fs-m-caption">avg</span>
                </div>
                <!-- Row 4: Trading Session -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.session', ['stage' => 'Planned'])) ?>">Planned Sessions ⓘ</span>
                    <?php if (!empty($fsS2SessionsSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS2SessionsSorted as $fsSess => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsSess) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <?php endif; ?>
                </div>
                <!-- Row 5: Instrument -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.instrument', ['stage' => 'Planned'])) ?>">Planned Instruments ⓘ</span>
                    <?php if (!empty($fsInstrumentsSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsInstrumentsSorted as $fsInstr => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsInstr) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <?php if ($fsInstrumentsOverflow): ?><span class="fs-m-caption"><?= htmlspecialchars($fsInstrumentsOverflow) ?></span><?php endif; ?>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <?php endif; ?>
                </div>
                <!-- Row 6: Timeframe, real data since 2026-07-27 item 2
                     (Calculator's own planned_timeframe field). -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.timeframe', ['stage' => 'Planned'])) ?>">Planned Timeframe ⓘ</span>
                    <?php if (!empty($fsS2TimeframesSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS2TimeframesSorted as $fsTf => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsTf) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <?php if ($fsS2TimeframesOverflow): ?><span class="fs-m-caption"><?= htmlspecialchars($fsS2TimeframesOverflow) ?></span><?php endif; ?>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">No trades this <?= htmlspecialchars($fsPeriodNoun) ?> recorded a planned timeframe</span>
                    <?php endif; ?>
                </div>

                <div class="fs-stage-tagline" style="color:#A78BFA">Goal: Plan accurately</div>
            </div>
            <div class="fs-arrow">→</div>

            <!-- Stage 3, Manual Outcome (amber) -->
            <div class="fs-stage-card" style="border-left-color:var(--warning)">
                <div class="fs-stage-title">Stage 3, Manual Outcome</div>
                <div class="fs-stage-sub">What you did manually.</div>

                <!-- Row 1: Adherence -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage3.execution_adherence')) ?>">Execution Adherence ⓘ</span>
                    <?php if ($fsS3['execution_adherence_m'] > 0): ?>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS3['execution_adherence_pct']) ?></span>
                    <span class="fs-m-caption">(<?= $fsS3['execution_adherence_n'] ?> of <?= $fsS3['execution_adherence_m'] ?> trades within tolerance of plan)</span>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">No trades with both planned and confirmed entry/lot data this <?= htmlspecialchars($fsPeriodNoun) ?></span>
                    <?php endif; ?>
                </div>
                <!-- Row 2: Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage3.executed_risk')) ?>">Executed Risk ⓘ</span>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS3['avg_executed_risk_pct'], 2) ?></span>
                    <span class="fs-m-caption">avg, scaled by actual lot size</span>
                </div>
                <!-- Row 3: Reward:Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage3.executed_rr')) ?>">Executed RR ⓘ</span>
                    <span class="fs-m-value"><?= $fsR($fsS3['avg_r_multiple']) ?></span>
                    <span class="fs-m-caption">avg realized R (wins &amp; losses)</span>
                </div>
                <!-- Row 4: Trading Session -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="A trade's session is captured once at entry and isn't re-confirmed manually, same data as Stage 2's Planned Sessions.">Executed Sessions ⓘ</span>
                    <?php if (!empty($fsS2SessionsSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS2SessionsSorted as $fsSess => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsSess) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <span class="fs-m-caption">Same as planned</span>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <?php endif; ?>
                </div>
                <!-- Row 5: Instrument, real manually-captured data (item 2) when
                     available, falling back to Stage 2's Planned Instruments for
                     legacy trades resolved before actual_instrument existed. -->
                <div class="fs-stage-metric">
                    <?php if (!empty($fsS3ActualInstrumentsSorted)): ?>
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.instrument', ['stage' => 'Executed'])) ?>">Executed Instruments ⓘ</span>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS3ActualInstrumentsSorted as $fsInstr => $fsCnt): ?>
                        <li><span><?= htmlspecialchars(strtoupper($fsInstr)) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <span class="fs-m-caption">Manually confirmed at resolution<?= $fsS3ActualInstrumentsOverflow ? ' · ' . htmlspecialchars($fsS3ActualInstrumentsOverflow) : '' ?></span>
                    <?php elseif (!empty($fsInstrumentsSorted)): ?>
                    <span class="fs-m-label" title="No trade this <?= htmlspecialchars($fsPeriodNoun) ?> had an actual instrument captured at resolution, showing Stage 2's planned instruments instead.">Executed Instruments ⓘ</span>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsInstrumentsSorted as $fsInstr => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsInstr) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <span class="fs-m-caption">Same as planned, not captured at resolution</span>
                    <?php else: ?>
                    <span class="fs-m-label">Executed Instruments</span>
                    <span class="fs-m-value">-</span>
                    <?php endif; ?>
                </div>
                <!-- Row 6: Timeframe, real manually-captured data (item 2). -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.timeframe', ['stage' => 'Executed'])) ?>">Executed Timeframe ⓘ</span>
                    <?php if (!empty($fsS3ActualTimeframesSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS3ActualTimeframesSorted as $fsTf => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsTf) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <span class="fs-m-caption">Manually confirmed at resolution</span>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">Not captured for these trades</span>
                    <?php endif; ?>
                </div>

                <div class="fs-stage-tagline" style="color:var(--warning)">Goal: Execute the plan</div>
            </div>
            <div class="fs-arrow">→</div>

            <!-- Stage 4, Broker Reconciliation (green) -->
            <div class="fs-stage-card" style="border-left-color:var(--positive)">
                <div class="fs-stage-title">Stage 4, Broker Reconciliation</div>
                <div class="fs-stage-sub">What actually happened in the market.</div>
                <?php if ($fsS4['reconciled_count'] === 0): ?>
                <p class="fs-stage-empty-note">No trades for this <?= htmlspecialchars($fsPeriodNoun) ?> have been broker-reconciled yet. Upload your MT5 statement to verify broker execution and complete reconciliation, it fills in verified adherence, risk, RR, and instrument/timeframe figures for this period.</p>
                <?php else: ?>
                <!-- Row 1: Adherence -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage4.reconciled_adherence')) ?>">Reconciled Adherence ⓘ</span>
                    <?php if ($fsS4['reconciled_adherence_m'] > 0): ?>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS4['reconciled_adherence_pct']) ?></span>
                    <span class="fs-m-caption">(<?= $fsS4['reconciled_adherence_n'] ?> of <?= $fsS4['reconciled_adherence_m'] ?>)</span>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">Upload an MT5 statement to verify lot size against broker data</span>
                    <?php endif; ?>
                </div>
                <!-- Row 2: Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage4.actual_broker_risk')) ?>">Actual Broker Risk ⓘ</span>
                    <span class="fs-m-value"><?= $fsPctFmt($fsS4['actual_risk_avg'], 2) ?></span>
                    <span class="fs-m-caption">per trade, broker lot size</span>
                </div>
                <!-- Row 3: Reward:Risk -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage4.actual_rr')) ?>">Actual Risk:Reward ⓘ</span>
                    <span class="fs-m-value"><?= $fsRatio($fsS4['actual_rr_realized']) ?></span>
                    <span class="fs-m-caption">winners only, realized avg</span>
                </div>
                <!-- Row 4: Trading Session -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="No session-bucketing-from-broker-timestamp logic exists in this codebase yet, not approximated. See CLAUDE.md.">Actual Sessions ⓘ</span>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">Not available from broker data</span>
                </div>
                <!-- Row 5: Instrument -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage_row.instrument', ['stage' => 'Broker-verified'])) ?>">Actual Broker Instruments ⓘ</span>
                    <?php if (!empty($fsS4InstrumentsSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS4InstrumentsSorted as $fsInstr => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsInstr) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <?php if ($fsS4InstrumentsOverflow): ?><span class="fs-m-caption"><?= htmlspecialchars($fsS4InstrumentsOverflow) ?></span><?php endif; ?>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <?php endif; ?>
                    <?php if ($fsS4['instrument_confirmed_m'] > 0): ?>
                    <span class="fs-m-caption" title="<?= htmlspecialchars(reportsCoach('stage4.instrument_confirmed')) ?>"><?= $fsS4['instrument_confirmed_n'] ?> of <?= $fsS4['instrument_confirmed_m'] ?> manually-recorded instruments confirmed by broker match ⓘ</span>
                    <?php endif; ?>
                </div>
                <!-- Row 6: Timeframe, manually captured (item 2), carried through
                     for reconciled trades; not broker-verifiable (see
                     stage4.timeframe_manual coaching for why). -->
                <div class="fs-stage-metric">
                    <span class="fs-m-label" title="<?= htmlspecialchars(reportsCoach('stage4.timeframe_manual')) ?>">Broker Timeframe ⓘ</span>
                    <?php if (!empty($fsS4ActualTimeframesSorted)): ?>
                    <ul class="fs-stage-list">
                        <?php foreach ($fsS4ActualTimeframesSorted as $fsTf => $fsCnt): ?>
                        <li><span><?= htmlspecialchars($fsTf) ?></span><span><?= $fsPct($fsCnt) ?>%</span></li>
                        <?php endforeach; ?>
                    </ul>
                    <span class="fs-m-caption">Manually captured, not broker-verifiable</span>
                    <?php else: ?>
                    <span class="fs-m-value">-</span>
                    <span class="fs-m-caption">Not captured for these trades</span>
                    <?php endif; ?>
                </div>

                <?php endif; ?>
                <div class="fs-stage-tagline" style="color:var(--positive)">Goal: Match market reality</div>
            </div>
        </div>
        </div>

        <?php if ($fsShowStrategyAlert): ?>
        <div class="insight-text" style="border:1px solid color-mix(in srgb,var(--negative) 35%,transparent);background:color-mix(in srgb,var(--negative) 8%,var(--background))">
            <strong style="color:var(--negative)">Strategy Performance Alert:</strong>
            This <?= $accountView === 'prop' ? 'prop firm' : 'live account' ?> scope has a negative all-time expectancy (<?= pnlFormat($fsAllTimeExpectancy) ?> per trade across <?= $fsAllTimeN ?> closed trades), historically not profitable over a large enough sample to trust. Worth a strategy review before continuing to trade it as-is.
        </div>
        <?php endif; ?>

        <!-- Bottom row: Commentary+Scores / Classification Distribution / Recommendations -->
        <div class="fs-bottom-grid">
            <div class="section-card" style="margin-bottom:0">
                <h3><?= htmlspecialchars(ucfirst($period)) ?> Commentary</h3>
                <?php
                // Item 6 (2026-07-28): one line per commentary item instead
                // of one run-on paragraph, needed so each line can carry
                // its own "View Trades" link where a real filter exists
                // (see $auditCommentary's construction above for which
                // lines get one and why).
                foreach ($auditCommentary as $fsAc): ?>
                <p style="font-size:.8rem;color:var(--foreground-muted);line-height:1.55;margin:0 0 .4rem"><?= htmlspecialchars($fsAc['text']) ?><?php if (!empty($fsAc['url'])): ?> <a href="<?= $fsAc['url'] ?>" class="drill-link">View Trades →</a><?php endif; ?></p>
                <?php endforeach; ?>
                <?php
                // Discipline/Consistency Score are computed over every
                // trade in this period (EdgeHealthEngine::getPeriodFactors())
                //, not a special subset, so one shared "View Trades" link
                // (same scope as the Trade Set section's own link) covers
                // both tiles honestly.
                $fsScoresTradesUrl = journalTradesFilterUrl(array_filter(['account_type' => $fsAccountType, 'account_id' => $fsSelectedAccountId ?: null, 'strategy_version_id' => $selectedVersionId, 'from' => $startDateOnly, 'to' => $endDateOnly]));
                ?>
                <div class="fs-mini-row">
                    <?php $fsDiscTag = fsScoreTag($fsPeriodFactors['discipline_score'], $fsPeriodFactors['tier'] ?? 'confident'); ?>
                    <div class="fs-mini-tile">
                        <span class="fs-mini-label" title="<?= htmlspecialchars(reportsCoach('commentary.discipline_score')) ?>">Discipline Score ⓘ</span>
                        <div class="fs-mini-value"><?= $fsPeriodFactors['discipline_score'] !== null ? number_format($fsPeriodFactors['discipline_score'], 0) . '/100' : '-' ?></div>
                        <span class="pill <?= $fsDiscTag[1] ?>" title="<?= htmlspecialchars($fsTierNote ?? '') ?>"><?= $fsDiscTag[0] ?><?= $fsTierNote ? ' ⓘ' : '' ?></span>
                        <div><a href="<?= $fsScoresTradesUrl ?>" class="drill-link" style="font-size:.62rem">View Trades →</a></div>
                    </div>
                    <?php $fsImprovementTitle = $fsDeltaWinRate === null
                        ? 'No prior ' . $fsPeriodNoun . ' with closed trades to compare win rate against.'
                        : 'Win rate this ' . $fsPeriodNoun . ' (' . $fsTotalWinRate . '%) vs win rate last ' . $fsPeriodNoun . ' (' . $fsPrevTotalWinRate . '%).'; ?>
                    <div class="fs-mini-tile">
                        <span class="fs-mini-label" title="<?= htmlspecialchars($fsImprovementTitle . "\n\n" . reportsCoach('commentary.improvement')) ?>">Improvement vs Prev Period ⓘ</span>
                        <div class="fs-mini-value <?= $fsDeltaWinRate === null ? '' : ($fsDeltaWinRate >= 0 ? 'positive' : 'negative') ?>"><?= $fsDeltaWinRate === null ? '-' : (($fsDeltaWinRate >= 0 ? '+' : '') . $fsDeltaWinRate . '%') ?></div>
                        <span class="fs-mini-label" style="text-transform:none;letter-spacing:0">win rate improvement</span>
                    </div>
                    <?php $fsConsTag = fsScoreTag($fsPeriodFactors['consistency_score'], $fsPeriodFactors['tier'] ?? 'confident'); ?>
                    <div class="fs-mini-tile">
                        <span class="fs-mini-label" title="<?= htmlspecialchars(reportsCoach('commentary.consistency_score')) ?>">Consistency Score ⓘ</span>
                        <div class="fs-mini-value"><?= $fsPeriodFactors['consistency_score'] !== null ? number_format($fsPeriodFactors['consistency_score'], 0) . '/100' : '-' ?></div>
                        <span class="pill <?= $fsConsTag[1] ?>" title="<?= htmlspecialchars($fsTierNote ?? '') ?>"><?= $fsConsTag[0] ?><?= $fsTierNote ? ' ⓘ' : '' ?></span>
                        <div><a href="<?= $fsScoresTradesUrl ?>" class="drill-link" style="font-size:.62rem">View Trades →</a></div>
                    </div>
                </div>
                <?php if ($fsTierNote): ?><p style="font-size:.68rem;color:var(--foreground-dim);margin-top:.6rem;margin-bottom:0"><?= htmlspecialchars($fsTierNote) ?></p><?php endif; ?>
            </div>

            <div class="section-card" style="margin-bottom:0;text-align:center">
                <h3>Classification Distribution</h3>
                <?php if ($fsGradeTotal > 0): ?>
                <div class="chart-wrap" style="max-width:220px;min-height:180px;margin:0 auto"><canvas id="fsGradePieChart"></canvas></div>
                <?php
                // Grade → underlying trade_classification values, reversed
                // from $fsGradeMap above, feeds the comma-list classification
                // filter journal.php now supports (item 5, 2026-07-27 pass).
                $fsGradeClassifications = [];
                foreach ($fsGradeMap as $fsCls => $fsGr) { $fsGradeClassifications[$fsGr][] = $fsCls; }
                ?>
                <div style="text-align:left;margin-top:.75rem">
                    <?php foreach (['A+', 'A', 'B', 'C'] as $fsGrade): $fsGc = $fsGradeCounts[$fsGrade]; if ($fsGc === 0) continue; $fsGradeTip = $fsGradeLabels[$fsGrade] . ', ' . $fsGradeCoaching[$fsGrade];
                        $fsGradeUrl = journalTradesFilterUrl(['classification' => implode(',', $fsGradeClassifications[$fsGrade] ?? []), 'account_type' => $fsAccountType, 'from' => $startDateOnly, 'to' => $endDateOnly]);
                    ?>
                    <a href="<?= $fsGradeUrl ?>" style="display:flex;align-items:center;gap:.5rem;font-size:.72rem;padding:.2rem 0;text-decoration:none;color:inherit" title="<?= htmlspecialchars($fsGradeTip) ?>">
                        <span style="width:10px;height:10px;border-radius:2px;background:<?= $fsGradeColors[$fsGrade] ?>;flex-shrink:0"></span>
                        <span style="flex:1;color:var(--foreground-muted)"><?= $fsGrade ?> ⓘ</span>
                        <span style="font-family:var(--font-mono);color:var(--foreground)"><?= $fsGc ?></span>
                        <span style="font-family:var(--font-mono);color:var(--foreground-dim);min-width:42px;text-align:right"><?= $fsPct($fsGc) ?>%</span>
                    </a>
                    <?php endforeach; ?>
                </div>
                <p style="font-size:.68rem;color:var(--foreground-dim);margin-top:.5rem">Total Trades: <?= $fsGradeTotal ?></p>
                <?php else: ?>
                <div class="empty-state" style="padding:1.5rem"><p style="font-size:.78rem">No classified trades this <?= htmlspecialchars($fsPeriodNoun) ?> yet.</p></div>
                <?php endif; ?>
            </div>

            <div class="section-card" style="margin-bottom:0">
                <h3>Key Recommendations</h3>
                <ul class="fs-rec-list">
                    <?php foreach ($fsRecommendations as $fsRecLine): ?>
                    <li><span class="fs-rec-check">✓</span><span><?= htmlspecialchars($fsRecLine['text']) ?><?php if (!empty($fsRecLine['url'])): ?> <a href="<?= $fsRecLine['url'] ?>" class="drill-link">View Trades →</a><?php endif; ?></span></li>
                    <?php endforeach; ?>
                </ul>
            </div>
        </div>

        <p class="fs-footer-note">All metrics are cumulative for the selected period and account scope.</p>

        <!-- "Show More", no longer expands anything inside the stage cards
             above (see CLAUDE.md's 2026-07-27 refinement-pass entry); it now
             reveals the standalone "Intent vs Reality, Execution Gap" card
             directly beneath this whole section. -->
        <div class="fs-show-more-row"><button class="versions-toggle" onclick="toggleFsSecondary(this)">Show More ▾</button></div>

        <?php endif; // $fsTotalTrades > 0 ?>
    </div>
    <?php endif; // $accountView !== 'comparison' ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Intent vs Reality, Execution Gap. Standalone card revealed by the -->
    <!-- 4-Stage Summary's "Show More" toggle above.                        -->
    <!-- Refined 2026-07-27 (tolerance/execution-gap pass): Session          -->
    <!-- Precision and Instrument Precision were removed from this card,    -->
    <!-- both compared Stage 1 (declared strategy) against Stage 2 (what     -->
    <!-- was planned), not Stage 2 against Stage 3 (what was manually        -->
    <!-- confirmed), so they measured planning discipline, not an execution  -->
    <!-- gap, and they duplicated the 4-Stage Summary's own Stage 1         -->
    <!-- Declared Session/Instrument rows plus Stage 3/4's "Same as planned" -->
    <!-- captions (a trade's session/instrument is captured once and never   -->
    <!-- re-confirmed, so this card showing them again added no new          -->
    <!-- information, see this file's "Row 4/Row 5" comments elsewhere).    -->
    <!-- The underlying engine.php fields (session_precision_pct/            -->
    <!-- instrument_precision_pct) are unchanged and still computed,        -->
    <!-- orphaned here per the additive-only rule, same pattern already      -->
    <!-- used elsewhere in this file for retired display concepts.           -->
    <!-- Replaced with Lot Size Precision, a genuine Stage2-vs-Stage3        -->
    <!-- execution gap (planned position size vs manually-confirmed size)    -->
    <!-- that was previously only folded into the combined Execution         -->
    <!-- Adherence boolean, never shown on its own; this also completes the   -->
    <!-- tolerance system's coverage across Entry/Stop/TP/Lot Size (Risk %    -->
    <!-- and RR are covered by Over-Risk Occurrences/RR Drift below).         -->
    <!-- Every metric below is still a bounded %-of-trades-within-tolerance   -->
    <!-- or a reuse of an already-audited point-difference, deliberately     -->
    <!-- NOT the unclamped ratio-scaling pattern (planned risk% × actual-     -->
    <!-- lots/planned-lots) used elsewhere in this file's Stage 3/4           -->
    <!-- "Executed Risk"/"Actual Broker Risk" tiles, which is mathematically  -->
    <!-- capable of producing an impossible output (e.g. a hypothetical       -->
    <!-- "24,624.48% Average Risk Used") whenever its lot-size denominator     -->
    <!-- is small, confirmed live this DB's actual_lots always mirrors       -->
    <!-- lots_per_position exactly (no trade currently deviates by more than  -->
    <!-- 5%), so that formula happens not to misfire on today's data, but it   -->
    <!-- remains an unguarded ratio and was deliberately not reused here.      -->
    <!-- See engine.php's execution_gap block for the computation itself.     -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($accountView !== 'comparison' && $fsTotalTrades > 0):
        $fsGap = $trueFourStageSummary['execution_gap'];
        $fsGapBarColor = fn($pct) => $pct === null ? 'var(--foreground-dim)' : ($pct >= 80 ? 'var(--positive)' : ($pct >= 50 ? 'var(--warning)' : 'var(--negative)'));
        $fsRiskDriftAvg = $fsS1['avg_risk_overage'] !== null ? round((float)$fsS1['avg_risk_overage'], 2) : null;
        $fsRrDrift = ($fsS1['avg_declared_rr'] !== null && $fsS2['avg_risk_reward'] !== null) ? round((float)$fsS2['avg_risk_reward'] - (float)$fsS1['avg_declared_rr'], 2) : null;
        $fsGapMetric = function ($label, $pct, $n, $m, $caption, $coachKey = null) use ($fsGapBarColor, $fsPctFmt) { ?>
        <div class="fs-gap-metric">
            <div class="fs-gap-metric-head">
                <span class="fs-gap-metric-label"<?= $coachKey ? ' title="' . htmlspecialchars(reportsCoach($coachKey)) . '"' : '' ?>><?= htmlspecialchars($label) ?><?= $coachKey ? ' ⓘ' : '' ?></span>
                <span class="fs-gap-metric-value" style="color:<?= $fsGapBarColor($pct) ?>"><?= $fsPctFmt($pct) ?></span>
            </div>
            <?php if ($pct !== null): ?>
            <div class="fs-gap-bar-track"><div class="fs-gap-bar-fill" style="width:<?= max(2, $pct) ?>%;background:<?= $fsGapBarColor($pct) ?>"></div></div>
            <?php endif; ?>
            <div class="fs-gap-metric-caption"><?= htmlspecialchars($caption) ?></div>
        </div>
        <?php };
    ?>
    <div class="section-card fs-gap-card" id="fsGapCard">
        <h3>Intent vs Reality, Execution Gap (<?= htmlspecialchars(ucfirst($period)) ?>)</h3>
        <p style="font-size:.78rem;color:var(--foreground-muted);margin-bottom:1.1rem;line-height:1.55">Where execution actually drifted from the original trading plan this <?= htmlspecialchars($fsPeriodNoun) ?>, not a repeat of the performance stats above, only the gap between what was planned (Stage 1/2) and what was manually confirmed (Stage 3). Every tile applies the same calculator tolerance used throughout Reports, so normal lot-size rounding is never mistaken for a real execution gap.</p>

        <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:0 2rem">
            <?= $fsGapMetric('Entry Precision', $fsGap['entry_precision_pct'], $fsGap['entry_precision_n'], $fsGap['entry_precision_m'], $fsGap['entry_precision_m'] > 0 ? "{$fsGap['entry_precision_n']} of {$fsGap['entry_precision_m']} trades filled within 0.1% of the planned entry" : 'No trades with both a planned and confirmed entry price this ' . $fsPeriodNoun, 'gap.entry_precision') ?>
            <?= $fsGapMetric('Stop Precision', $fsGap['stop_precision_pct'], $fsGap['stop_precision_n'], $fsGap['stop_precision_m'], $fsGap['stop_precision_m'] > 0 ? "{$fsGap['stop_precision_n']} of {$fsGap['stop_precision_m']} trades closed with a stop within 0.1% of plan" : 'No trades with both a planned and confirmed stop this ' . $fsPeriodNoun, 'gap.stop_precision') ?>
            <?= $fsGapMetric('TP Precision', $fsGap['tp_precision_pct'], $fsGap['tp_precision_n'], $fsGap['tp_precision_m'], $fsGap['tp_precision_m'] > 0 ? "{$fsGap['tp_precision_n']} of {$fsGap['tp_precision_m']} winning trades exited at a declared TP level" : 'No winning trades with a recorded exit price this ' . $fsPeriodNoun, 'gap.tp_precision') ?>
            <?= $fsGapMetric('Lot Size Precision', $fsGap['lot_precision_pct'], $fsGap['lot_precision_n'], $fsGap['lot_precision_m'], $fsGap['lot_precision_m'] > 0 ? "{$fsGap['lot_precision_n']} of {$fsGap['lot_precision_m']} trades closed within 5% of the planned position size" : 'No trades with both a planned and confirmed lot size this ' . $fsPeriodNoun, 'gap.lot_precision') ?>
            <div class="fs-gap-metric">
                <div class="fs-gap-metric-head">
                    <span class="fs-gap-metric-label" title="<?= htmlspecialchars(reportsCoach('gap.over_risk')) ?>">Over-Risk Occurrences ⓘ</span>
                    <span class="fs-gap-metric-value" style="color:<?= $fsS2['over_risk_occurrences'] > 0 ? 'var(--negative)' : 'var(--positive)' ?>"><?php if ($fsS2['over_risk_occurrences'] > 0 && $fsS1['avg_declared_risk_pct'] !== null): ?><a href="<?= journalTradesFilterUrl(['account_type' => $fsAccountType, 'min_risk_pct' => $fsS1['avg_declared_risk_pct'], 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" style="color:inherit"><?= $fsS2['over_risk_occurrences'] ?></a><?php else: ?><?= $fsS2['over_risk_occurrences'] ?><?php endif; ?></span>
                </div>
                <div class="fs-gap-bar-track"><div class="fs-gap-bar-fill" style="width:<?= max(2, $fsS2['over_risk_pct']) ?>%;background:var(--negative)"></div></div>
                <div class="fs-gap-metric-caption"><?= $fsS2['over_risk_pct'] ?>% of trades risked more than the strategy's declared limit, beyond the normal lot-rounding tolerance<?= $fsRiskDriftAvg !== null ? " (avg +{$fsRiskDriftAvg}% over the declared risk target)" : '' ?><?= ($fsS2['over_risk_occurrences'] > 0 && $fsS1['avg_declared_risk_pct'] !== null) ? ', trade count links to an approximate view (period average risk threshold, not the per-instrument tolerance used above)' : '' ?></div>
            </div>
            <?php
            // Item 7 (2026-07-27): a structurally different bucket from
            // Over-Risk Occurrences above, never mixed together. A
            // Contract-Constrained trade risked more than declared because
            // no valid lot size on that instrument, at that account
            // balance, could have hit the target (e.g. Coffee's minimum
            // lot on a small account), an instrument/contract limitation,
            // not trader behavior. See CLAUDE.md's item 7 entry for the
            // full minimum-achievable-risk methodology.
            $fsCcCount = $fsS2['contract_constrained_occurrences'] ?? 0;
            $fsCcPct = $fsS2['contract_constrained_occurrences_pct'] ?? 0;
            ?>
            <div class="fs-gap-metric">
                <div class="fs-gap-metric-head">
                    <span class="fs-gap-metric-label" title="<?= htmlspecialchars(reportsCoach('gap.contract_constrained')) ?>">Contract-Constrained ⓘ</span>
                    <span class="fs-gap-metric-value" style="color:<?= $fsCcCount > 0 ? 'var(--warning)' : 'var(--positive)' ?>"><?= $fsCcCount ?></span>
                </div>
                <div class="fs-gap-bar-track"><div class="fs-gap-bar-fill" style="width:<?= max(2, $fsCcPct) ?>%;background:var(--warning)"></div></div>
                <div class="fs-gap-metric-caption"><?= $fsCcCount > 0 ? "{$fsCcPct}% of trades had no valid lot size on that instrument capable of hitting the declared risk target, a contract limitation, not over-risking" : 'No trades this ' . htmlspecialchars($fsPeriodNoun) . ' were structurally incapable of hitting the declared risk target' ?></div>
            </div>
        </div>

        <?php if ($fsRrDrift !== null): ?>
        <p style="font-size:.75rem;color:var(--foreground-muted);margin-top:1.1rem;padding-top:1rem;border-top:1px solid var(--border)">
            <strong style="color:var(--foreground)" title="<?= htmlspecialchars(reportsCoach('gap.rr_drift')) ?>">RR Drift ⓘ:</strong>
            The calculator's own planned reward:risk this <?= htmlspecialchars($fsPeriodNoun) ?> averaged <?= $fsRatio($fsS2['avg_risk_reward']) ?>, <?= $fsRrDrift >= 0 ? 'above' : 'below' ?> the strategy's declared <?= $fsRatio($fsS1['avg_declared_rr']) ?> edge (<?= $fsRrDrift >= 0 ? '+' : '' ?><?= $fsRrDrift ?>).
            <a href="<?= journalTradesFilterUrl(array_filter(['account_type' => $fsAccountType, 'strategy_version_id' => $selectedVersionId, 'from' => $startDateOnly, 'to' => $endDateOnly])) ?>" class="drill-link">View Trades →</a>
        </p>
        <?php endif; ?>

        <p style="font-size:.68rem;color:var(--foreground-dim);margin-top:1rem;padding-top:.85rem;border-top:1px solid var(--border)">Based on manually-confirmed trade resolutions. Upload your MT5 statement to complete broker reconciliation for this reporting period, it sharpens these figures with verified fill prices, lot sizes, and timestamps.</p>
    </div>
    <?php endif; ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Edge Health, its own standalone card, deliberately NOT merged     -->
    <!-- into the 4-Stage Summary above. Edge Health answers "has this      -->
    <!-- strategy become statistically reliable over its whole real trading -->
    <!-- history" (all-time, backward-looking, real scores from Trade 1,   -->
    <!-- 100 trades is a confidence footnote, not a gate, see              -->
    <!-- EdgeHealthEngine's own docblock); the 4-Stage Summary's            -->
    <!-- Discipline/Consistency mini-tiles answer "how disciplined          -->
    <!-- was trading during just this reporting period" (period-scoped,     -->
    <!-- ungated), same scoring core, deliberately different questions,    -->
    <!-- kept independent per instruction. $edgeHealth itself has been      -->
    <!-- computed every request since the original 4-Stage rebuild, but was -->
    <!-- never rendered anywhere after the old accordion housing it was     -->
    <!-- deleted (see CLAUDE.md's 2026-07-26 fifth-pass entry), reconnected -->
    <!-- here as its own card. Same Quarterly/Annual-only gate ($depth>=3)  -->
    <!-- as Asset Class Edge Detection/the rest of this file's "edge"       -->
    <!-- language (see CLAUDE.md's edge-language scoping note), a strategy -->
    <!-- being "statistically reliable" isn't a claim Weekly/Monthly data    -->
    <!-- can support either. Not accountView-scoped (EdgeHealthEngine reads -->
    <!-- every one of the user's non-validation closed trades regardless of -->
    <!-- account type), shown identically on the Prop Firms/Live Accounts  -->
    <!-- tabs (a strategy's real edge doesn't change depending on which tab -->
    <!-- you're viewing), hidden on Comparison per this file's existing     -->
    <!-- "blended-account computations belong on the Comparison page, not   -->
    <!-- duplicated in the main body" convention.                          -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($accountView !== 'comparison' && $depth >= 3): ?>
    <div class="section-card">
        <h3>Edge Health<?= !empty($edgeHealth['gradeable']) ? ', ' . str_repeat('★', $edgeHealth['stars']) . str_repeat('☆', 5 - $edgeHealth['stars']) : '' ?></h3>
        <p style="font-size:.78rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.55">Coaches whether this strategy is becoming consistently profitable, never from a declared risk %/R:R (that's Stage 1's job above), only from what actually happened. The objective is a strategy that holds up under real trading, not a trade count: every closed trade updates the four scores below from Trade 1 onward. This is a different question from the Discipline/Consistency scores above, which grade just this reporting period.</p>
        <?php if (!empty($edgeHealth['gradeable'])): ?>
        <div class="stat-grid" style="margin-bottom:1rem">
            <div class="stat-tile"><p class="label">Overall Score</p><p class="value" style="font-size:1.3rem"><?= number_format($edgeHealth['score'], 1) ?>/100</p></div>
            <?php foreach ($edgeHealth['factors'] as $ehf):
                $ehTrendIcon = match ($ehf['trend']) { 'improving' => '▲', 'declining' => '▼', 'stable' => '▬', default => '' };
                $ehTrendColor = match ($ehf['trend']) { 'improving' => 'var(--positive)', 'declining' => 'var(--negative)', default => 'var(--foreground-dim)' };
            ?>
            <div class="stat-tile">
                <p class="label"><?= htmlspecialchars($ehf['label']) ?></p>
                <p class="value" style="font-size:1rem"><?= number_format($ehf['score'], 1) ?><?php if ($ehTrendIcon): ?> <span style="font-size:.7rem;color:<?= $ehTrendColor ?>"><?= $ehTrendIcon ?></span><?php endif; ?></p>
            </div>
            <?php endforeach; ?>
        </div>
        <!-- Per-factor coaching, every factor, always, not just the
             weakest two, per the explicit "redesign around coaching, not
             counting" instruction. What it measures, the real current
             value, whether it's trending up or down, and what that means. -->
        <div style="display:grid;gap:.6rem;margin-bottom:1rem">
            <?php foreach ($edgeHealth['factors'] as $ehKey => $ehf): ?>
            <div style="padding:.6rem .75rem;background:var(--background);border-radius:var(--radius-md);border-left:3px solid <?= $ehf['score'] >= 70 ? 'var(--positive)' : ($ehf['score'] >= 45 ? 'var(--warning)' : 'var(--negative)') ?>">
                <p style="font-size:.72rem;font-weight:600;color:var(--foreground);margin:0 0 .2rem"><?= htmlspecialchars($ehf['label']) ?></p>
                <p style="font-size:.76rem;color:var(--foreground-muted);margin:0;line-height:1.5"><?= htmlspecialchars($edgeHealth['narrative']['factor_lines'][$ehKey] ?? '') ?></p>
            </div>
            <?php endforeach; ?>
        </div>
        <!-- The one line sample confidence is allowed to appear on this
             whole card, everything above is about what the numbers show,
             this alone is about how much to trust them yet. -->
        <?php if (!empty($edgeHealth['narrative']['confidence_line'])): ?>
        <p style="font-size:.7rem;color:var(--foreground-dim);margin:0;padding-top:.7rem;border-top:1px solid var(--border)">ⓘ <?= htmlspecialchars($edgeHealth['narrative']['confidence_line']) ?><?php if (!$edgeHealth['fully_confident']): ?> (<?= $edgeHealth['trades_journaled'] ?> <?= $edgeHealth['trades_journaled'] === 1 ? 'trade' : 'trades' ?> journaled so far.)<?php endif; ?></p>
        <?php endif; ?>
        <?php else: ?>
        <p style="font-size:.78rem;color:var(--foreground-muted);margin:0">No closed trades yet, Edge Health begins coaching from your very first closed trade. Nothing to show until then.</p>
        <?php endif; ?>
    </div>
    <?php endif; ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Instrument Performance, its own reporting section beneath the   -->
    <!-- 4-Stage Summary. Answers "which markets contributed", one         -->
    <!-- compact card (2026-07-27 compression pass, was one full-width     -->
    <!-- section-card PER instrument, up to 8 stacked cards causing real    -->
    <!-- scroll fragmentation; same underlying $topInstrumentCards/         -->
    <!-- $remainingInstrumentCount computation, only the presentation        -->
    <!-- changed). Kept separate from (not merged into) Stage 4: Stage 4 is -->
    <!-- tightly scoped to MT5-verified broker-reconciled trades only        -->
    <!-- (currently empty for every user in this DB, see CLAUDE.md), so     -->
    <!-- folding a per-instrument breakdown of ALL trades into that card     -->
    <!-- would mix two different data sources under one "broker              -->
    <!-- reconciliation" heading. Not a duplicate of the Instrument P&L      -->
    <!-- chart above (that chart shows relative $ contribution only, no win  -->
    <!-- rate/avg risk/classification detail) or Asset Class Edge Detection  -->
    <!-- below (that groups into ~6 fixed categories; this lists real        -->
    <!-- per-instrument rows, top 8 by |P&L|, same ranking the chart above   -->
    <!-- already uses).                                                      -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($accountView !== 'comparison' && !empty($topInstrumentCards)):
        $tipMaxAbsPnl = max(1, ...array_map(fn($r) => abs((float)$r['total_pnl']), $topInstrumentCards));
    ?>
    <div class="section-card">
        <h3 title="<?= htmlspecialchars(reportsCoach('instrument.performance')) ?>">Instrument Performance, <?= htmlspecialchars(ucfirst($period)) ?> ⓘ</h3>
        <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:.75rem">Top <?= count($topInstrumentCards) ?> by |P&amp;L| this <?= htmlspecialchars($fsPeriodNoun ?? periodNoun($period)) ?>, click a row for classification detail.</p>
        <div style="overflow-x:auto">
        <table class="instr-perf-table">
            <thead><tr><th>Instrument</th><th>Trades</th><th>Win Rate</th><th>Avg Risk %</th><th>Over-Risk</th><th>P&amp;L</th><th></th></tr></thead>
            <tbody>
            <?php foreach ($topInstrumentCards as $tipIdx => $tic):
                $ticPnl = round((float)$tic['total_pnl'], 2);
                $tipRowId = 'tipDetail' . $tipIdx;
                $tipBarPct = max(4, round((abs($ticPnl) / $tipMaxAbsPnl) * 100, 1));
            ?>
                <tr class="instr-perf-row" onclick="toggleInstrPerfRow('<?= $tipRowId ?>', this)">
                    <td style="font-family:var(--font-sans);font-weight:600;color:var(--foreground)"><?= htmlspecialchars($tic['instrument']) ?></td>
                    <td><?= (int)$tic['trades'] ?></td>
                    <td><?= $tic['win_rate'] ?>%</td>
                    <td><?= number_format((float)($tic['avg_risk_pct'] ?? 0), 2) ?>%</td>
                    <td class="<?= (int)$tic['over_risk_count'] > 0 ? 'negative' : '' ?>"><?= (int)$tic['over_risk_count'] ?></td>
                    <td class="instr-perf-bar-cell">
                        <div class="<?= pnlClass($ticPnl) ?>" style="font-family:var(--font-mono);font-size:.78rem;margin-bottom:.2rem"><?= pnlFormat($ticPnl) ?></div>
                        <div class="instr-perf-bar-track"><div class="instr-perf-bar-fill" style="width:<?= $tipBarPct ?>%;background:<?= $ticPnl >= 0 ? 'var(--positive)' : 'var(--negative)' ?>"></div></div>
                    </td>
                    <td style="text-align:right;color:var(--foreground-dim)"><span class="tip-chevron">▾</span></td>
                </tr>
                <tr class="instr-perf-detail" id="<?= $tipRowId ?>">
                    <td colspan="7" style="padding:.75rem .6rem 1rem">
                        <?php if (!empty($tic['classifications'])): ?>
                        <div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:.6rem">
                            <?php foreach ($tic['classifications'] as $ticc): ?>
                            <span class="pill pill-neutral" style="font-size:.65rem"><?= htmlspecialchars(ucwords(str_replace('_', ' ', $ticc['trade_classification'] ?? 'unclassified'))) ?> · <?= (int)$ticc['count'] ?></span>
                            <?php endforeach; ?>
                        </div>
                        <?php endif; ?>
                        <a href="<?= journalTradesFilterUrl(['instrument' => $tic['instrument'], 'account_type' => match ($accountView) { 'prop' => 'prop_firm', 'live' => 'personal', default => null }, 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link">View trades →</a>
                    </td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
        </div>
        <?php if ($remainingInstrumentCount > 0): ?>
        <p style="font-size:.72rem;color:var(--foreground-dim);margin-top:.6rem">+<?= $remainingInstrumentCount ?> more instrument(s) traded this <?= htmlspecialchars($fsPeriodNoun ?? periodNoun($period)) ?>, not shown individually.</p>
        <?php endif; ?>
    </div>
    <?php endif; ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Prop Firm Analytics, NEW top-level accordion. Aggregated across  -->
    <!-- every prop-firm account; never lists all accounts individually.  -->
    <!-- Shown only under the Prop Firms account_view tab.                -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($accountView === 'prop'): ?>
    <div class="accordion-group" id="accordion-prop-firm-analytics">
        <button type="button" class="accordion-group-header" onclick="toggleAccordionGroup('accordion-prop-firm-analytics')">
            <span class="accordion-group-title"><span class="accordion-icon">🏦</span>Prop Firm Analytics</span>
            <span class="accordion-group-summary"><?php if ($propFirmAgg): ?><span><strong><?= $propFirmAgg['total_accounts'] ?></strong> accounts</span><span><strong><?= $propFirmAgg['breached_accounts'] ?></strong> breached</span><span><strong><?= $propFirmAgg['passed_accounts'] ?></strong> passed</span><?php endif; ?></span>
            <span class="accordion-chevron">▾</span>
        </button>
        <div class="accordion-group-body">
        <?php if (!$propFirmAgg): ?>
        <div class="empty-state"><p>No prop firm accounts found yet.</p></div>
        <?php else: ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('portfolio.combined_pnl')) ?>">Portfolio Summary ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Combined across all <?= $propFirmAgg['total_accounts'] ?> prop firm account(s)</p>
            <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.5"><?= htmlspecialchars(ProgressiveInsightsEngine::portfolio($period, 'prop firm', $propFirmAgg['total_accounts'], $propFirmAgg['combined_pnl'], $propFirmAgg['breached_accounts'])) ?></p>
            <div class="stat-grid">
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('portfolio.total_accounts')) ?>">Total Accounts ⓘ</p><p class="value"><?= $propFirmAgg['total_accounts'] ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('portfolio.combined_pnl')) ?>">Combined P&amp;L ⓘ</p><p class="value <?= pnlClass($propFirmAgg['combined_pnl']) ?>"><?= pnlFormat($propFirmAgg['combined_pnl']) ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('portfolio.active')) ?>">Active ⓘ</p><p class="value"><?= $propFirmAgg['active_accounts'] ?></p></div>
                <?php
                // Never a bare, unexplained "0", every count tile here keeps its
                // number (so it stays comparable at a glance with the nonzero
                // tiles around it) and gets an immediately-responsive hover
                // tooltip when it's zero, applied consistently across this whole
                // card and the By Challenge Type table below rather than mixing
                // that approach with text-replacement on some tiles and not others.
                $breachedTip = ($propFirmAgg['breached_accounts'] === 0 ? "Congratulations, no breached accounts yet. Strong risk discipline so far.\n\n" : '') . reportsCoach('portfolio.breached');
                $passedTip   = ($propFirmAgg['passed_accounts']   === 0 ? "No accounts have passed a challenge yet, keep progressing and this will populate.\n\n" : '') . reportsCoach('portfolio.passed');
                $fundedTip   = ($propFirmAgg['funded_accounts']   === 0 ? "No funded accounts yet, passing every phase of a challenge unlocks this.\n\n" : '') . reportsCoach('portfolio.funded');
                $instantTip  = $propFirmAgg['instant_funding_accounts'] === 0 ? 'No instant-funding accounts, that challenge type skips the evaluation phases entirely.' : '';
                ?>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($breachedTip) ?>">Breached ⓘ</p><p class="value <?= $propFirmAgg['breached_accounts'] > 0 ? 'negative' : 'positive' ?>"><?php if ($propFirmAgg['breached_accounts'] > 0): ?><a href="<?= journalCoachingHubUrl('breached') ?>" style="color:inherit;text-decoration:none"><?= $propFirmAgg['breached_accounts'] ?></a><?php else: ?><?= $propFirmAgg['breached_accounts'] ?><?php endif; ?> <span style="font-size:.65rem;color:var(--foreground-dim)">(<?= $propFirmAgg['breach_rate_pct'] ?>%)</span></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($passedTip) ?>">Passed ⓘ</p><p class="value <?= $propFirmAgg['passed_accounts'] > 0 ? 'positive' : '' ?>"><?php if ($propFirmAgg['passed_accounts'] > 0): ?><a href="<?= journalCoachingHubUrl('passed') ?>" style="color:inherit;text-decoration:none"><?= $propFirmAgg['passed_accounts'] ?></a><?php else: ?><?= $propFirmAgg['passed_accounts'] ?><?php endif; ?> <span style="font-size:.65rem;color:var(--foreground-dim)">(<?= $propFirmAgg['pass_rate_pct'] ?>%)</span></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($fundedTip) ?>">Funded ⓘ</p><p class="value <?= $propFirmAgg['funded_accounts'] > 0 ? 'positive' : '' ?>"><?= $propFirmAgg['funded_accounts'] ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($instantTip) ?>">Instant Funding<?= $instantTip ? ' ⓘ' : '' ?></p><p class="value"><?= $propFirmAgg['instant_funding_accounts'] ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(($propFirmAgg['avg_days_to_breach'] === null ? "Congratulations, no accounts have breached yet.\n\n" : '') . reportsCoach('portfolio.avg_days_to_breach')) ?>">Avg Days to Breach ⓘ</p><p class="value"><?= $propFirmAgg['avg_days_to_breach'] !== null ? $propFirmAgg['avg_days_to_breach'] : 'No breached accounts yet' ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(($propFirmAgg['avg_days_to_pass'] === null ? "No accounts have passed a challenge yet, keep progressing and this will populate.\n\n" : '') . reportsCoach('portfolio.avg_days_to_pass')) ?>">Avg Days to Pass ⓘ</p><p class="value"><?= $propFirmAgg['avg_days_to_pass'] !== null ? $propFirmAgg['avg_days_to_pass'] : 'No passed accounts yet' ?></p></div>
            </div>
            <?php if ($propFirmAgg['breached_with_date'] > 0 || $propFirmAgg['passed_with_date'] > 0): ?>
            <p style="font-size:.68rem;color:var(--foreground-dim);margin-top:.75rem;line-height:1.5">
                Based on <?= $propFirmAgg['breached_with_date'] ?> of <?= $propFirmAgg['breached_accounts'] ?> breached account(s) with a recorded breach date<?php if ($propFirmAgg['breached_same_day'] > 0): ?> (<?= $propFirmAgg['breached_same_day'] ?> breached the same day the account was created)<?php endif; ?>,
                and <?= $propFirmAgg['passed_with_date'] ?> of <?= $propFirmAgg['passed_accounts'] ?> passed account(s) with a recorded pass date<?php if ($propFirmAgg['passed_same_day'] > 0): ?> (<?= $propFirmAgg['passed_same_day'] ?> passed the same day)<?php endif; ?>.
                Same-day events pull these averages toward zero, treat them as directional, not precise, until more accounts have real elapsed time recorded.
            </p>
            <?php endif; ?>
        </div>

        <!-- Boundary Analysis, relocated here from its own top-level section
             (2026-07-27 refinement pass; see CLAUDE.md) since breach/pass
             boundaries are conceptually a Prop Firm Analytics concern, not a
             page-level section of their own. Calculations unchanged (same
             $boundaryAnalytics computed above, same lifecycle_state fix from
             the prior session), only the presentation changed, from a flat
             6-tile stat-grid to a compact Pass/Breach split bar + a separate
             Near-Miss callout (near-miss accounts are still-active, not a
             terminal state, so mixing them into the same bar as
             Breached/Passed would misrepresent them as a third outcome). -->
        <?php if (!empty($boundaryAnalytics) && $boundaryAnalytics['total_challenges'] > 0):
            $baTotal = $boundaryAnalytics['total_challenges'];
            $baBreachPct = round($boundaryAnalytics['breach_count'] / $baTotal * 100, 1);
            $baPassPct = round($boundaryAnalytics['pass_count'] / $baTotal * 100, 1);
            $baOtherPct = max(0, round(100 - $baBreachPct - $baPassPct, 1));
        ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('portfolio.breached')) ?>">Boundary Analysis ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Pass / Breach distribution across all <?= $baTotal ?> challenge(s)</p>

            <div class="boundary-split-bar">
                <?php if ($boundaryAnalytics['breach_count'] > 0): ?><div class="boundary-split-seg" style="width:<?= $baBreachPct ?>%;background:var(--negative)" title="Breached: <?= $boundaryAnalytics['breach_count'] ?> (<?= $baBreachPct ?>%)"><?= $baBreachPct >= 8 ? $baBreachPct . '%' : '' ?></div><?php endif; ?>
                <?php if ($boundaryAnalytics['pass_count'] > 0): ?><div class="boundary-split-seg" style="width:<?= $baPassPct ?>%;background:var(--positive)" title="Passed: <?= $boundaryAnalytics['pass_count'] ?> (<?= $baPassPct ?>%)"><?= $baPassPct >= 8 ? $baPassPct . '%' : '' ?></div><?php endif; ?>
                <?php if ($baOtherPct > 0): ?><div class="boundary-split-seg" style="width:<?= $baOtherPct ?>%;background:var(--border);color:var(--foreground-dim)" title="Still active / other: <?= $baOtherPct ?>%"><?= $baOtherPct >= 8 ? $baOtherPct . '%' : '' ?></div><?php endif; ?>
            </div>
            <div class="boundary-split-legend">
                <a href="<?= journalCoachingHubUrl('breached') ?>" style="text-decoration:none;color:inherit"><span class="dot" style="background:var(--negative)"></span>Breached (<?= $boundaryAnalytics['breach_count'] ?>) →</a>
                <a href="<?= journalCoachingHubUrl('passed') ?>" style="text-decoration:none;color:inherit"><span class="dot" style="background:var(--positive)"></span>Passed (<?= $boundaryAnalytics['pass_count'] ?>) →</a>
                <?php if ($baOtherPct > 0): ?><span><span class="dot" style="background:var(--border)"></span>Still active / other</span><?php endif; ?>
            </div>

            <div class="stat-grid" style="margin-bottom:1rem">
                <?php $nearMissTip = ($boundaryAnalytics['near_miss_count'] === 0 ? "Congratulations, no accounts are currently sitting close to their breach line.\n\n" : '') . reportsCoach('boundary.near_miss'); ?>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($nearMissTip) ?>">Near-Miss (&lt;10% from breach) ⓘ</p><p class="value <?= $boundaryAnalytics['near_miss_count'] > 0 ? 'negative' : '' ?>"><?= $boundaryAnalytics['near_miss_count'] ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('boundary.breach_severity')) ?>">Avg Breach Severity ⓘ</p><p class="value <?= $boundaryAnalytics['avg_breach_severity_pct'] !== null ? 'negative' : '' ?>"><?= $boundaryAnalytics['avg_breach_severity_pct'] !== null ? '+' . number_format($boundaryAnalytics['avg_breach_severity_pct'], 1) . '%' : '-' ?></p></div>
                <div class="stat-tile"><p class="label">Avg Breach Payload</p><p class="value"><?= $boundaryAnalytics['avg_breach_payload'] !== null ? pnlFormat(-abs($boundaryAnalytics['avg_breach_payload'])) : '-' ?></p></div>
                <div class="stat-tile"><p class="label">Avg Excess at Pass</p><p class="value"><?= $boundaryAnalytics['avg_excess_at_pass'] !== null ? pnlFormat(abs($boundaryAnalytics['avg_excess_at_pass'])) : '-' ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('boundary.pass_margin')) ?>">Typical Pass Margin ⓘ</p><p class="value <?= $boundaryAnalytics['avg_pass_excess_pct'] !== null ? 'positive' : '' ?>"><?= $boundaryAnalytics['avg_pass_excess_pct'] !== null ? '+' . number_format($boundaryAnalytics['avg_pass_excess_pct'], 1) . '%' : '-' ?></p></div>
                <?php if ($boundaryAnalytics['near_miss_count'] > 0 && $boundaryAnalytics['avg_near_miss_distance_pct'] !== null): ?>
                <div class="stat-tile"><p class="label">Avg Near-Miss Distance</p><p class="value negative"><?= number_format($boundaryAnalytics['avg_near_miss_distance_pct'], 1) ?>%</p></div>
                <?php endif; ?>
            </div>
            <?php if ($boundaryAnalytics['pass_count'] > 0): ?>
            <p style="font-size:.68rem;color:var(--foreground-dim);margin-bottom:.85rem;line-height:1.5">Avg Excess at Pass / Typical Pass Margin are based on <?= $boundaryAnalytics['pass_excess_sample_count'] ?> of <?= $boundaryAnalytics['pass_count'] ?> passed account(s) still showing their pass-moment balance, the rest have since progressed to a new phase or funded status, so no reliable pass-moment snapshot remains for them.</p>
            <?php endif; ?>
            <?php if (!empty($boundaryAnalytics['breach_reason_tally'])): ?>
            <div style="margin-bottom:1rem">
                <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:.5rem;text-transform:uppercase;letter-spacing:.04em" title="<?= htmlspecialchars(reportsCoach('boundary.behavior_patterns')) ?>">Behavior Patterns Leading to Breaches ⓘ</p>
                <ul class="fs-stage-list">
                    <?php $baReasonTotal = array_sum($boundaryAnalytics['breach_reason_tally']); $baReasonShown = 0; foreach ($boundaryAnalytics['breach_reason_tally'] as $baReason => $baReasonCt): if ($baReasonShown >= 5) break; $baReasonShown++; ?>
                    <li><span><?= htmlspecialchars($baReason) ?></span><span><?= $baReasonCt ?> (<?= round($baReasonCt / $baReasonTotal * 100, 1) ?>%)</span></li>
                    <?php endforeach; ?>
                </ul>
            </div>
            <?php endif; ?>
            <p style="font-size:.8rem;color:var(--foreground-muted);line-height:1.5;margin:0"><strong style="color:var(--foreground)">Commentary:</strong> <?= htmlspecialchars(ProgressiveInsightsEngine::boundaryAnalytics($period, $boundaryAnalytics)) ?></p>
        </div>
        <?php endif; ?>

        <?php if (!empty($propFirmAgg['by_type'])): ?>
        <div class="section-card">
            <h3>By Challenge Type</h3>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Challenge Type</th><th>Accounts</th><th>Breached</th><th>Passed</th><th>Pass Rate</th></tr></thead>
                    <tbody>
                    <?php foreach ($propFirmAgg['by_type'] as $bt):
                        $btAccounts   = (int)$bt['accounts'];
                        $btBreached   = (int)$bt['breached'];
                        $btPassed     = (int)$bt['passed'];
                        $btPassRate   = $btAccounts > 0 ? round($btPassed / $btAccounts * 100, 1) : 0;
                        $btTypeLabel  = ucwords(str_replace('_', ' ', $bt['challenge_type'] ?? 'this type'));
                        // Same tooltip-on-zero convention as the Portfolio Summary
                        // tiles above, applied consistently rather than mixed with
                        // text-replacement within this table.
                        $btBreachedTip = $btBreached === 0 ? "Congratulations, no {$btTypeLabel} accounts have breached yet." : '';
                        $btPassedTip   = $btPassed   === 0 ? "No {$btTypeLabel} accounts have passed yet, keep progressing and this will populate." : '';
                    ?>
                        <tr>
                            <td style="font-family:var(--font-sans)"><?= htmlspecialchars($btTypeLabel) ?></td>
                            <td><?= $btAccounts ?></td>
                            <td class="<?= $btBreached > 0 ? 'negative' : '' ?>" title="<?= htmlspecialchars($btBreachedTip) ?>"><?= $btBreached ?><?= $btBreachedTip ? ' ⓘ' : '' ?></td>
                            <td class="<?= $btPassed > 0 ? 'positive' : '' ?>" title="<?= htmlspecialchars($btPassedTip) ?>"><?= $btPassed ?><?= $btPassedTip ? ' ⓘ' : '' ?></td>
                            <td><?= $btPassRate ?>%</td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>
        <?php endif; ?>
        <?php if (!empty($propFirmAgg['best'])): ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.top_performing')) ?>">Top Performing Accounts ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Best <?= count($propFirmAgg['best']) ?> by P&amp;L</p>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Account</th><th>Status</th><th>P&amp;L</th><th></th></tr></thead>
                    <tbody>
                    <?php foreach ($propFirmAgg['best'] as $pb): ?>
                        <tr>
                            <td style="font-family:var(--font-sans)" title="<?= htmlspecialchars($pb['rank_reason'] ?? '') ?>"><?= htmlspecialchars($pb['label']) ?> ⓘ</td>
                            <td><?= htmlspecialchars(ucwords(str_replace('_', ' ', $pb['lifecycle_state'] ?? '-'))) ?></td>
                            <td class="<?= pnlClass((float)$pb['pnl']) ?>"><?= pnlFormat((float)$pb['pnl']) ?></td>
                            <td><a href="<?= journalAccountUrl($pb['challenge_type'], $pb['lifecycle_state'], $pb['id']) ?>" class="drill-link">View trades →</a></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>
        <?php endif; ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.needing_attention')) ?>">Accounts Needing Attention ⓘ</h3>
            <?php if (!empty($propFirmAgg['needs_attention'])): ?>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">At or past half of a drawdown or daily-loss limit, <?= count($propFirmAgg['needs_attention']) ?> account(s)</p>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Account</th><th>Status</th><th>Why it's flagged</th><th></th></tr></thead>
                    <tbody>
                    <?php foreach ($propFirmAgg['needs_attention'] as $pw): ?>
                        <tr>
                            <td style="font-family:var(--font-sans)"><?= htmlspecialchars($pw['label']) ?></td>
                            <td><?= htmlspecialchars(ucwords(str_replace('_', ' ', $pw['lifecycle_state'] ?? '-'))) ?></td>
                            <td class="negative"><?= htmlspecialchars($pw['reason']) ?></td>
                            <td><a href="<?= journalAccountUrl($pw['challenge_type'], $pw['lifecycle_state'], $pw['id']) ?>" class="drill-link">View trades →</a></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <?php else: ?>
            <div class="empty-state"><p>All accounts are within safe drawdown and daily-loss limits right now, nothing needs attention.</p></div>
            <?php endif; ?>
        </div>
        <?php if (!empty($propFirmAgg['most_breached'])): ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.most_breached')) ?>">Most Severely Breached ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Worst <?= count($propFirmAgg['most_breached']) ?> breach(es), ranked by real account result at breach</p>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Account</th><th title="Trading P&amp;L within the currently-selected reporting period, capped at the challenge's breach boundary, the same rule used everywhere on this page.">P&amp;L (period, capped) ⓘ</th><th title="The real, uncapped account result at the moment of breach, includes any loss beyond the firm's allowed limit.">Total Breach Payload ⓘ</th><th title="The maximum loss this account's own rules allowed before triggering a breach.">Breach Level ⓘ</th><th></th></tr></thead>
                    <tbody>
                    <?php foreach ($propFirmAgg['most_breached'] as $pmb): ?>
                        <tr>
                            <td style="font-family:var(--font-sans)"><?= htmlspecialchars($pmb['label']) ?></td>
                            <td class="<?= pnlClass((float)$pmb['pnl']) ?>"><?= pnlFormat((float)$pmb['pnl']) ?></td>
                            <td class="negative"><?= pnlFormat((float)$pmb['breach_payload']) ?></td>
                            <td><?= $pmb['breach_level'] !== null ? '$' . number_format($pmb['breach_level'], 2) : '-' ?></td>
                            <td><a href="<?= journalAccountUrl($pmb['challenge_type'], $pmb['lifecycle_state'], $pmb['id']) ?>" class="drill-link">View trades →</a></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>
        <?php endif; ?>
        <?php endif; ?>
        </div>
    </div>
    <?php endif; ?>

    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Live Account Analytics, NEW top-level accordion. Aggregated     -->
    <!-- across every live account; never lists all accounts individually.-->
    <!-- Shown only under the Live Accounts account_view tab.             -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($accountView === 'live'): ?>
    <div class="accordion-group" id="accordion-live-account-analytics">
        <button type="button" class="accordion-group-header" onclick="toggleAccordionGroup('accordion-live-account-analytics')">
            <span class="accordion-group-title"><span class="accordion-icon">💼</span>Live Account Analytics</span>
            <span class="accordion-group-summary"><?php if ($liveAccountAgg): ?><span><strong><?= $liveAccountAgg['total_accounts'] ?></strong> accounts</span><span>Combined <strong class="<?= pnlClass($liveAccountAgg['combined_pnl']) ?>"><?= pnlFormat($liveAccountAgg['combined_pnl']) ?></strong></span><?php endif; ?></span>
            <span class="accordion-chevron">▾</span>
        </button>
        <div class="accordion-group-body">
        <?php if (!$liveAccountAgg): ?>
        <div class="empty-state"><p>No live accounts found yet.</p></div>
        <?php else: ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('portfolio.combined_pnl_live')) ?>">Portfolio Summary ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Combined across all <?= $liveAccountAgg['total_accounts'] ?> live account(s)</p>
            <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.5"><?= htmlspecialchars(ProgressiveInsightsEngine::portfolio($period, 'live', $liveAccountAgg['total_accounts'], $liveAccountAgg['combined_pnl'], $liveAccountAgg['accounts_closed_at_loss'])) ?></p>
            <div class="stat-grid">
                <div class="stat-tile"><p class="label">Total Accounts</p><p class="value"><?= $liveAccountAgg['total_accounts'] ?></p></div>
                <div class="stat-tile"><p class="label">Combined P&amp;L</p><p class="value <?= pnlClass($liveAccountAgg['combined_pnl']) ?>"><?= pnlFormat($liveAccountAgg['combined_pnl']) ?></p></div>
                <div class="stat-tile"><p class="label">Total Withdrawals</p><p class="value">$<?= number_format($liveAccountAgg['total_withdrawal_value'], 2) ?></p></div>
                <?php
                // Same tooltip-on-zero convention as the Prop Firm Portfolio
                // Summary card, applied consistently rather than mixed with
                // text-replacement within this card.
                $withdrawTip = $liveAccountAgg['accounts_with_withdrawals'] === 0 ? 'No withdrawals recorded yet on any live account.' : '';
                $lossTip     = $liveAccountAgg['accounts_closed_at_loss']   === 0 ? 'Congratulations, no accounts have closed at a loss. Clean record so far.' : '';
                $profitTip   = $liveAccountAgg['accounts_meaningful_profit'] === 0 ? 'No account has cleared $500 in profit yet, this will populate as your accounts grow.' : '';
                ?>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($withdrawTip) ?>">Accounts w/ Withdrawals<?= $withdrawTip ? ' ⓘ' : '' ?></p><p class="value"><?= $liveAccountAgg['accounts_with_withdrawals'] ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($lossTip) ?>">Closed at a Loss<?= $lossTip ? ' ⓘ' : '' ?></p><p class="value <?= $liveAccountAgg['accounts_closed_at_loss'] > 0 ? 'negative' : 'positive' ?>"><?= $liveAccountAgg['accounts_closed_at_loss'] ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($profitTip) ?>">$500+ Profit Accounts<?= $profitTip ? ' ⓘ' : '' ?></p><p class="value <?= $liveAccountAgg['accounts_meaningful_profit'] > 0 ? 'positive' : '' ?>"><?= $liveAccountAgg['accounts_meaningful_profit'] ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= $liveAccountAgg['avg_survival_days'] === null ? 'No accounts have closed yet.' : '' ?>">Avg Survival<?= $liveAccountAgg['avg_survival_days'] === null ? ' ⓘ' : '' ?></p><p class="value"><?= $liveAccountAgg['avg_survival_days'] !== null ? $liveAccountAgg['avg_survival_days'] . ' days' : 'No closed accounts yet' ?></p></div>
            </div>
        </div>

        <!-- Capital Flow, new 2026-07-27 enrichment. Cash-movement facts
             only (deposits/withdrawals, all-time lifecycle facts, same
             convention as Prop Firm's breach/pass counts), deliberately
             separate from Combined P&L above, which is (and stays) summed
             purely from closed-trade broker_pnl/projected_pnl. Never mix
             the two: a deposit is money the trader added, not something
             they made trading, and this section is the one place that
             money is tracked and visualized. -->
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('capital.net_flow')) ?>">Capital Flow ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Deposits &amp; withdrawals across all <?= $liveAccountAgg['total_accounts'] ?> live account(s), cash movement, not trading performance</p>
            <div class="stat-grid" style="margin-bottom:1rem">
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('capital.deposits')) ?>">Total Deposits ⓘ</p><p class="value">$<?= number_format($liveAccountAgg['total_deposits'], 2) ?></p></div>
                <div class="stat-tile"><p class="label" title="Capital added after each account's initial funding (net_deposits), excludes the initial deposit itself.">Additional Capital Added ⓘ</p><p class="value">$<?= number_format($liveAccountAgg['total_additional_deposits'], 2) ?></p></div>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars(reportsCoach('capital.withdrawals')) ?>">Total Withdrawals ⓘ</p><p class="value">$<?= number_format($liveAccountAgg['total_withdrawal_value'], 2) ?></p></div>
                <div class="stat-tile"><p class="label" title="Total deposits minus total withdrawals, how much more cash has gone into these accounts than has come out.">Net Capital Flow ⓘ</p><p class="value <?= $liveAccountAgg['net_capital_flow'] >= 0 ? 'positive' : 'negative' ?>"><?= $liveAccountAgg['net_capital_flow'] >= 0 ? '+' : '' ?>$<?= number_format($liveAccountAgg['net_capital_flow'], 2) ?></p></div>
                <?php $noWithdrawTip = $liveAccountAgg['accounts_no_withdrawals'] === $liveAccountAgg['total_accounts'] ? 'No live account has ever withdrawn, all capital added has stayed in the market.' : ''; ?>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($noWithdrawTip) ?>">Accounts w/ No Withdrawals<?= $noWithdrawTip ? ' ⓘ' : '' ?></p><p class="value"><?= $liveAccountAgg['accounts_no_withdrawals'] ?></p></div>
                <?php $noDepositTip = $liveAccountAgg['accounts_no_additional_deposits'] === $liveAccountAgg['total_accounts'] ? 'No live account has had capital added beyond its initial deposit yet.' : ''; ?>
                <div class="stat-tile"><p class="label" title="<?= htmlspecialchars($noDepositTip) ?>">Accounts w/ No Additional Deposits<?= $noDepositTip ? ' ⓘ' : '' ?></p><p class="value"><?= $liveAccountAgg['accounts_no_additional_deposits'] ?></p></div>
            </div>
            <div class="chart-wrap" style="max-width:520px"><canvas id="capitalFlowChart"></canvas></div>
            <p style="font-size:.68rem;color:var(--foreground-dim);margin-top:.75rem;line-height:1.5">Deposits and withdrawals are portfolio events, not trading results, Combined P&amp;L above is computed purely from closed-trade outcomes and never includes cash added or withdrawn.</p>
        </div>
        <?php if (!empty($liveAccountAgg['best'])): ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.top_performing_live')) ?>">Top Performing Accounts ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Best <?= count($liveAccountAgg['best']) ?> by P&amp;L, accounts with no real trading activity this period are excluded</p>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Account</th><th>P&amp;L</th><th>Status</th><th></th></tr></thead>
                    <tbody>
                    <?php foreach ($liveAccountAgg['best'] as $lb):
                        $lbClosed = ($lb['lifecycle_state'] ?? '') === 'closed';
                        $lbStatusLabel = $lbClosed
                            ? 'Closed, Performance Before Closure'
                            : htmlspecialchars(ucwords(str_replace('_', ' ', $lb['lifecycle_state'] ?? '-')));
                        $lbTip = $lbClosed ? 'This account has since been closed, the P&amp;L shown reflects its trading performance while it was still active, not a current/ongoing result.' : '';
                    ?>
                        <tr>
                            <td style="font-family:var(--font-sans)"><?= htmlspecialchars($lb['label']) ?></td>
                            <td class="<?= pnlClass((float)$lb['pnl']) ?>"><?= pnlFormat((float)$lb['pnl']) ?></td>
                            <td<?= $lbTip ? ' title="' . htmlspecialchars($lbTip) . '"' : '' ?>><?= $lbStatusLabel ?><?= $lbTip ? ' ⓘ' : '' ?></td>
                            <td><a href="<?= journalAccountUrl(null, $lb['lifecycle_state'], $lb['id']) ?>" class="drill-link">View trades →</a></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>
        <?php endif; ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.needing_attention_live')) ?>">Accounts Needing Attention ⓘ</h3>
            <?php if (!empty($liveAccountAgg['needs_attention'])): ?>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Active accounts in real drawdown from peak, <?= count($liveAccountAgg['needs_attention']) ?> account(s). Closed accounts can't be traded, so they're never flagged here, see Closed Account Retrospective below instead.</p>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Account</th><th>Status</th><th>Why it's flagged</th><th></th></tr></thead>
                    <tbody>
                    <?php foreach ($liveAccountAgg['needs_attention'] as $lw): ?>
                        <tr>
                            <td style="font-family:var(--font-sans)"><?= htmlspecialchars($lw['label']) ?></td>
                            <td><?= htmlspecialchars(ucwords(str_replace('_', ' ', $lw['lifecycle_state'] ?? '-'))) ?></td>
                            <td class="negative"><?= htmlspecialchars($lw['reason']) ?></td>
                            <td><a href="<?= journalAccountUrl(null, $lw['lifecycle_state'], $lw['id']) ?>" class="drill-link">View trades →</a></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <?php else: ?>
            <div class="empty-state"><p>No active accounts are in real drawdown right now, nothing needs attention.</p></div>
            <?php endif; ?>
        </div>
        <?php if (!empty($liveAccountAgg['closed_retrospective'])): ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.closed_retrospective')) ?>">Closed Account Retrospective ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Historical insight from <?= count($liveAccountAgg['closed_retrospective']) ?> closed account(s) with real trading history, retrospective coaching, not a current action item</p>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Account</th><th title="<?= htmlspecialchars(reportsCoach('retrospective.active_duration')) ?>">Active Duration ⓘ</th><th>Trading P&amp;L (all-time)</th><th>Withdrawn Before Closure</th><th>Net Result</th><th>Why It Ended</th></tr></thead>
                    <tbody>
                    <?php foreach ($liveAccountAgg['closed_retrospective'] as $lc): ?>
                        <tr>
                            <td style="font-family:var(--font-sans)"><?= htmlspecialchars($lc['label']) ?></td>
                            <td><?= $lc['survival_days'] !== null ? htmlspecialchars(formatAccountDuration($lc['survival_days'])) : '-' ?> <span style="color:var(--foreground-dim);font-size:.72rem">(<?= $lc['closed_trade_count'] ?> trade<?= $lc['closed_trade_count'] === 1 ? '' : 's' ?>)</span></td>
                            <td class="<?= pnlClass($lc['alltime_pnl']) ?>"><?= pnlFormat($lc['alltime_pnl']) ?></td>
                            <td>$<?= number_format($lc['withdrawals'], 2) ?></td>
                            <td class="<?= pnlClass($lc['net_result']) ?>"><?= $lc['net_result'] >= 0 ? '+' : '' ?>$<?= number_format($lc['net_result'], 2) ?></td>
                            <td><?= htmlspecialchars($lc['reason']) ?></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>
        <?php endif; ?>
        <?php if (!empty($liveAccountAgg['longest_surviving'])): ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.longest_surviving')) ?>">Longest-Surviving Accounts ⓘ</h3>
            <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.04em">Top <?= count($liveAccountAgg['longest_surviving']) ?> by days active, requires at least 30 days survived AND at least one real closed trade; "existed" alone no longer qualifies</p>
            <div style="overflow-x:auto">
                <table class="setup-table">
                    <thead><tr><th>Account</th><th>Days Active</th><th title="Closed trades ÷ months survived, a rough gauge of how consistently the account was actually traded, not just how long it existed.">Trading Consistency ⓘ</th><th>P&amp;L</th><th></th></tr></thead>
                    <tbody>
                    <?php foreach ($liveAccountAgg['longest_surviving'] as $ls): ?>
                        <tr>
                            <td style="font-family:var(--font-sans)"><?= htmlspecialchars($ls['label']) ?></td>
                            <td><?= (int)$ls['survival_days'] ?></td>
                            <td><?= number_format((float)$ls['trades_per_month'], 1) ?> trades/mo <span style="color:var(--foreground-dim);font-size:.72rem">(<?= (int)$ls['closed_trade_count'] ?> total)</span></td>
                            <td class="<?= pnlClass((float)$ls['pnl']) ?>"><?= pnlFormat((float)$ls['pnl']) ?></td>
                            <td><a href="<?= journalAccountUrl(null, $ls['lifecycle_state'], $ls['id']) ?>" class="drill-link">View trades →</a></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>
        <?php else: ?>
        <div class="section-card">
            <h3 title="<?= htmlspecialchars(reportsCoach('leaderboard.longest_surviving')) ?>">Longest-Surviving Accounts ⓘ</h3>
            <div class="empty-state"><p>No account has both survived 30+ days and shown real trading activity yet, this will populate as your accounts build history.</p></div>
        </div>
        <?php endif; ?>
        <?php endif; ?>
        </div>
    </div>
    <?php endif; ?>

    <div class="accordion-group" id="accordion-advanced-statistics">
        <button type="button" class="accordion-group-header" onclick="toggleAccordionGroup('accordion-advanced-statistics')">
            <span class="accordion-group-title"><span class="accordion-icon">📐</span>Advanced Statistics</span>
            <span class="accordion-group-summary"><?php if ($profitFactor !== null): ?><span>Profit Factor <strong><?= number_format($profitFactor, 2) ?></strong></span><?php endif; ?><?php if (isset($sharpeRatio) && $sharpeRatio !== null): ?><span>Sharpe <strong><?= number_format($sharpeRatio, 2) ?></strong></span><?php endif; ?></span>
            <span class="accordion-chevron">▾</span>
        </button>
        <div class="accordion-group-body">
    <?php if ($totalTrades > 0): ?>
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Performance Analysis (Pro+, depth ≥ 2) -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($depth >= 2): ?>
        <?php if ($isProOrAbove): ?>
        <div class="section-card">
            <h3>Performance Analysis</h3>
            <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.5"><?= htmlspecialchars(ProgressiveInsightsEngine::risk($period, $expectancy, $profitFactor, $overRiskCount, $totalTrades)) ?></p>
            <?php
            $paPfReason = $profitFactor !== null ? '' : ' Clean record so far, Profit Factor needs at least one losing trade to compare gains against.';
            $paAvgWinReason  = $averageWin  ? '' : ' No wins yet this period.';
            $paAvgLossReason = $averageLoss ? '' : ' No losses yet this period.';
            ?>
            <div class="stat-grid">
                <div class="stat-tile advanced-metric">
                    <p class="label" title="<?= htmlspecialchars($metricDefinitions['expectancy']) ?>">Expectancy ⓘ</p>
                    <p class="value <?= pnlClass($expectancy ?? 0) ?>"><?= $expectancy !== null ? pnlFormat($expectancy) : '-' ?></p>
                    <p class="sub">Average $ per trade</p>
                </div>
                <div class="stat-tile advanced-metric">
                    <p class="label" title="<?= htmlspecialchars($metricDefinitions['profitFactor'] . $paPfReason) ?>">Profit Factor ⓘ</p>
                    <p class="value"><?= $profitFactor !== null ? number_format($profitFactor, 2) : 'No losses yet' ?></p>
                    <p class="sub">Gross profit ÷ gross loss</p>
                </div>
                <div class="stat-tile advanced-metric">
                    <p class="label" title="<?= htmlspecialchars($metricDefinitions['averageWin'] . $paAvgWinReason) ?>">Average Winner ⓘ</p>
                    <p class="value positive"><?= $averageWin ? pnlFormat($averageWin) : 'No wins yet' ?></p>
                </div>
                <div class="stat-tile advanced-metric">
                    <p class="label" title="<?= htmlspecialchars($metricDefinitions['averageLoss'] . $paAvgLossReason) ?>">Average Loser ⓘ</p>
                    <p class="value negative"><?= $averageLoss ? pnlFormat($averageLoss) : 'No losses yet' ?></p>
                </div>
                <!-- Over-risking tile: value + drill-link on its own line below -->
                <div class="stat-tile advanced-metric">
                    <p class="label" title="<?= htmlspecialchars($metricDefinitions['overRisking']) ?>">Over-Risking ⓘ</p>
                    <p class="value <?= $overRiskCount > 0 ? 'negative' : 'positive' ?>"><?= $overRiskCount ?> trades</p>
                    <!-- drill-link: tile-bottom placement -->
                    <a href="<?= journalTradesFilterUrl(['min_risk_pct' => $declaredMaxRiskPct, 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link drill-link-tile">View trades →</a>
                </div>
            </div>
            <?php if ($overRiskCount > 0): ?>
            <div class="insight-text"><?= $overRiskCount ?> trade<?= $overRiskCount > 1 ? 's' : '' ?> exceeded your declared risk limit this period.</div>
            <?php endif; ?>
        </div>
        <?php else: ?>
        <div class="locked-card">
            <div class="lock-icon">🔒</div>
            <h3>Performance Analysis</h3>
            <p>Upgrade to <strong>Pro</strong> to unlock expectancy, profit factor, average win/loss metrics, and over-risking detection.</p>
            <a href="/traderalign/pricing" class="btn btn-primary btn-sm">View Plans →</a>
        </div>
        <?php endif; ?>
    <?php endif; ?>
    <?php endif; ?>
    <?php if ($totalTrades > 0): ?>
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Drawdown Analysis (Pro+, depth ≥ 3) -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($depth >= 3): ?>
        <?php if ($isProOrAbove && $maximumDrawdown !== null): ?>
        <div class="section-card">
            <h3>Drawdown Analysis</h3>
            <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.5"><?= htmlspecialchars(ProgressiveInsightsEngine::drawdown($period, $maximumDrawdown, $drawdownFrequency, $totalTrades)) ?></p>
            <div class="stat-grid">
                <div class="stat-tile advanced-metric">
                    <p class="label" title="<?= htmlspecialchars($metricDefinitions['maximumDrawdown']) ?>">Maximum Drawdown ⓘ</p>
                    <p class="value negative"><?= pnlFormat((float)$maximumDrawdown) ?></p>
                    <p class="sub">Largest peak-to-trough decline</p>
                </div>
                <div class="stat-tile advanced-metric">
                    <p class="label" title="<?= htmlspecialchars($metricDefinitions['drawdownFrequency']) ?>">Drawdown Frequency ⓘ</p>
                    <p class="value"><?= $drawdownFrequency ?></p>
                    <p class="sub">Times P&amp;L dipped below -$100</p>
                </div>
            </div>
        </div>
        <?php elseif (!$isProOrAbove): ?>
        <div class="locked-card">
            <div class="lock-icon">🔒</div>
            <h3>Drawdown Analysis</h3>
            <p>Upgrade to <strong>Pro</strong> to access maximum drawdown tracking and drawdown frequency analysis.</p>
            <a href="/traderalign/pricing" class="btn btn-primary btn-sm">View Plans →</a>
        </div>
        <?php else: ?>
        <!-- $isProOrAbove is true but $maximumDrawdown is null, the card
             used to silently vanish here with no explanation at all, worse
             than a bare placeholder dash since there was no feedback whatsoever. This
             only means cumulative P&L never dipped below the period's
             starting point, not that the metric is broken. -->
        <div class="section-card">
            <h3>Drawdown Analysis</h3>
            <div class="empty-state"><p>Congratulations, no drawdown recorded this <?= htmlspecialchars(periodNoun($period)) ?>. Cumulative P&amp;L never dipped below where it started.</p></div>
        </div>
        <?php endif; ?>
    <?php endif; ?>
    <?php endif; ?>
    <?php if ($totalTrades > 0): ?>
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Professional Metrics (Pro+, depth ≥ 4) -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($depth >= 4): ?>
        <?php if ($isProOrAbove): ?>
        <div class="section-card">
            <h3>Professional Metrics</h3>
            <p style="font-size:.75rem;color:var(--foreground-dim);margin-bottom:1rem;line-height:1.5">Institutional-grade risk and return metrics. Hover over any label for an explanation.</p>
            <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1rem;line-height:1.5"><?= htmlspecialchars(ProgressiveInsightsEngine::professionalMetrics($sharpeRatio, $sortinoRatio, $totalTrades)) ?></p>
            <?php
            $cagrReason    = $cagr !== null ? '' : ' No active account found to calculate starting balance from.';
            // Sortino needs at least 2 losing trades to compute a reliable
            // downside deviation. Wording distinguishes genuinely zero losses
            // (a good outcome worth acknowledging) from merely not-enough-yet
            // (1 loss), neither should read as "trade worse to unlock this".
            $sortinoReason = $sortinoRatio !== null ? '' : ($totalLosses === 0
                ? ' No losing trades yet, nice work. Sortino Ratio (which measures downside risk) will show once there is some downside to measure.'
                : ' Sortino Ratio needs at least 2 losing trades to calculate downside deviation reliably, keep going, this will populate soon.');
            $recoveryReason = $recoveryFactor !== null ? '' : ' No drawdown recorded this period, nice, and Recovery Factor will show once there is a drawdown to measure recovery from.';
            $calmarReason   = $calmarRatio !== null ? '' : ' Needs a recorded drawdown and a calculable CAGR, both will populate as your track record builds.';
            ?>
            <div class="stat-grid">
                <div class="stat-tile advanced-metric"><p class="label" title="<?= htmlspecialchars($metricDefinitions['cagr'] . $cagrReason) ?>">CAGR (Annual Return) ⓘ</p><p class="value <?= pnlClass($cagr ?? 0) ?>"><?= $cagr !== null ? number_format($cagr, 1) . '%' : '-' ?></p></div>
                <div class="stat-tile advanced-metric"><p class="label" title="<?= htmlspecialchars(reportsCoach('advstat.sharpe')) ?>">Sharpe Ratio ⓘ</p><p class="value"><?= $sharpeRatio !== null ? number_format($sharpeRatio, 2) : '-' ?></p></div>
                <div class="stat-tile advanced-metric"><p class="label" title="<?= htmlspecialchars(($sortinoReason !== '' ? trim($sortinoReason) . "\n\n" : '') . reportsCoach('advstat.sortino')) ?>">Sortino Ratio ⓘ</p><p class="value"><?= $sortinoRatio !== null ? number_format($sortinoRatio, 2) : '-' ?></p></div>
                <div class="stat-tile advanced-metric"><p class="label" title="<?= htmlspecialchars(($recoveryReason !== '' ? trim($recoveryReason) . "\n\n" : '') . reportsCoach('advstat.recovery')) ?>">Recovery Factor ⓘ</p><p class="value"><?= $recoveryFactor !== null ? number_format($recoveryFactor, 2) : '-' ?></p></div>
                <div class="stat-tile advanced-metric"><p class="label" title="<?= htmlspecialchars(($calmarReason !== '' ? trim($calmarReason) . "\n\n" : '') . reportsCoach('advstat.calmar')) ?>">Calmar Ratio ⓘ</p><p class="value"><?= $calmarRatio !== null ? number_format($calmarRatio, 2) : '-' ?></p></div>
            </div>
        </div>
        <?php else: ?>
        <div class="locked-card">
            <div class="lock-icon">🔒</div>
            <h3>Professional Metrics</h3>
            <p>Upgrade to <strong>Pro</strong> to unlock CAGR, Sharpe Ratio, Sortino Ratio, Recovery Factor, and Calmar Ratio.</p>
            <a href="/traderalign/pricing" class="btn btn-primary btn-sm">View Plans →</a>
        </div>
        <?php endif; ?>
    <?php endif; ?>
    <?php endif; ?>
    <!-- Account Overview / Challenge Progress / Lifecycle Journey, removed
         from the main report body. These three cards each showed one
         arbitrarily-selected account's individual numbers ($activeAccount,
         defaulting to the first account found) with no indication of why
         that account was picked, nonsensical in an aggregate report for a
         trader with many accounts. Every other section on this page already
         aggregates across all accounts in the active Prop Firms/Live
         Accounts view; these three were the only holdouts. Per-account
         drill-down (full account detail, an account selector scoped to the
         accounts that actually contributed trades this period, a "View
         trades" link into Journal for just that period's trades) will be
         rebuilt properly inside the 4-Stage Performance Audit section
         instead, not done in this pass. getAccountMetricsForPeriod()/
         getChallengeMetrics()/getLifecycleJourney() calls above and
         $accountMetrics/$challengeMetrics/$lifecycleJourney/$activeAccount
         are left computed but otherwise unused, same "orphaned, confirmed
         unreachable, left in place" pattern already used elsewhere in this
         file (see $executionScore), cheap to keep, and the eventual
         4-Stage per-account rebuild will likely want the same calls. -->
    <!-- Comparison tab content (the old Prop vs Live table, then its
         placeholder replacement) removed entirely per explicit instruction,
         this whole render path is now unreachable anyway since it sits
         inside the $accountView !== 'comparison' wrapper added above. The
         Comparison tab shows only the period toggle until a real
         replacement is designed as a separate task. The backing
         $comparisonProp/$comparisonLive queries above are left in place
         (cheap, harmless) since that redesign will likely need the same
         per-account-type metrics. -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <!-- Analytics Across Strategies (Elite/Ultimate only), always the    -->
    <!-- final subsection of Advanced Statistics, no reveal-gate.          -->
    <!-- ════════════════════════════════════════════════════════════════ -->
    <?php if ($isEliteOrUltimate && !empty($crossStrategy)): ?>
    <div id="analytics-across-strategies-wrapper" style="margin-top:0">
    <div class="section-card" style="border-left:3px solid var(--primary); margin-top:1.25rem" id="analytics-across-strategies">
        <h3 style="font-size:1.15rem">📊 Analytics Across Strategies</h3>
        <p style="font-size:.75rem;color:var(--foreground-dim);margin-bottom:.25rem;text-transform:uppercase;letter-spacing:.05em;font-weight:500">Portfolio-Level Analysis</p>
        <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1.25rem;line-height:1.4">Portfolio-level comparison across all <?= count($crossStrategy) ?> active strategies.</p>
        <p style="font-size:.8rem;color:var(--foreground-muted);margin-bottom:1.25rem;line-height:1.4"><?= htmlspecialchars(ProgressiveInsightsEngine::crossStrategy($period, $crossStrategy)) ?></p>

        <!-- Portfolio Snapshot -->
        <h4 style="font-size:.85rem;font-weight:600;color:var(--foreground);margin-bottom:.75rem">Portfolio Snapshot</h4>
        <div class="stat-grid" style="margin-bottom:1.5rem">
            <div class="stat-tile"><p class="label">Strategies Analyzed</p><p class="value"><?= count($crossStrategy) ?></p></div>
            <?php
            $bestPerforming = null;
            foreach ($crossStrategy as $cs) { if ($cs['trades'] >= 10 && ($bestPerforming === null || $cs['total_pnl'] > $bestPerforming['total_pnl'])) $bestPerforming = $cs; }
            $highestWinRate = null;
            foreach ($crossStrategy as $cs) { if ($cs['trades'] >= 10 && ($highestWinRate === null || $cs['win_rate'] > $highestWinRate['win_rate'])) $highestWinRate = $cs; }
            $largestContributor = null;
            foreach ($crossStrategy as $cs) { if ($cs['trades'] >= 5 && ($largestContributor === null || $cs['percentage_of_profit'] > $largestContributor['percentage_of_profit'])) $largestContributor = $cs; }
            ?>
            <?php if ($bestPerforming): ?>
            <div class="stat-tile">
                <p class="label">Best Performing</p>
                <p class="value" style="font-size:1rem;font-family:var(--font-sans)"><?= htmlspecialchars($bestPerforming['name']) ?></p>
                <p class="sub">Net P&amp;L: <span class="<?= pnlClass($bestPerforming['total_pnl']) ?>"><?= pnlFormat($bestPerforming['total_pnl']) ?></span></p>
            </div>
            <?php endif; ?>
            <?php if ($highestWinRate): ?>
            <div class="stat-tile">
                <p class="label">Highest Win Rate</p>
                <p class="value" style="font-size:1rem;font-family:var(--font-sans)"><?= htmlspecialchars($highestWinRate['name']) ?></p>
                <p class="sub"><?= $highestWinRate['win_rate'] ?>% (min 10 trades)</p>
                <!-- drill-link: tile-bottom placement, filtered by that strategy version -->
                <a href="<?= journalTradesFilterUrl(['strategy_version_id' => $highestWinRate['version_id'], 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link drill-link-tile">View trades →</a>
            </div>
            <?php endif; ?>
            <?php if ($largestContributor): ?>
            <div class="stat-tile">
                <p class="label">Largest Profit Contributor</p>
                <p class="value" style="font-size:1rem;font-family:var(--font-sans)"><?= htmlspecialchars($largestContributor['name']) ?></p>
                <p class="sub"><?= $largestContributor['percentage_of_profit'] ?>% of portfolio profits</p>
            </div>
            <?php endif; ?>
            <?php if (count($crossStrategy) >= 2 && $concentrationPercentage > 0): ?>
            <div class="stat-tile">
                <p class="label">Portfolio Concentration</p>
                <p class="value"><?= $concentrationPercentage ?>%</p>
                <p class="sub">of profits from top 2 of <?= count($crossStrategy) ?> strategies</p>
            </div>
            <?php endif; ?>
        </div>

        <!-- Strategy Compliance, buckets a strategy's status_label (assigned
             per-strategy above) into one of three groups. Subtext spells out
             exactly which labels land in each bucket so a user doesn't have to
             guess what qualifies, same grouping as $greenStatuses/
             $amberStatuses/$orangeStatuses/$redStatuses above. -->
        <h4 style="font-size:.85rem;font-weight:600;color:var(--foreground);margin-bottom:.75rem">Strategy Compliance</h4>
        <div class="stat-grid" style="margin-bottom:1rem">
            <div class="stat-tile">
                <p class="label" title="On Track, At Capacity, or New, no rule violations detected.">On Track ⓘ</p>
                <p class="value" style="color:var(--positive)"><?= $onTrackCount ?></p>
                <p class="sub">on track, at capacity, or new, no rule violations</p>
            </div>
            <div class="stat-tile">
                <p class="label" title="Overactive, Streak Breach, Risk Breach, Underused, or Near Limit, a declared rule was broken or is about to be.">Warnings ⓘ</p>
                <p class="value" style="color:var(--warning)"><?= $warningCount ?></p>
                <p class="sub">breaking or nearing a declared rule, see Strategy Comparison below</p>
            </div>
            <div class="stat-tile">
                <p class="label" title="Not Yet Traded, or Inactive (no trades in 30+ days).">Inactive ⓘ</p>
                <p class="value" style="color:var(--negative)"><?= $inactiveCount ?></p>
                <p class="sub">no trades yet, or none in the last 30+ days</p>
            </div>
        </div>

        <?php
        // Scalability cap (item 3, 2026-07-27 finishing pass), $crossStrategy
        // has no LIMIT anywhere upstream (every active strategy version with
        // ≥1 trade this period, plus every untraded active strategy) and
        // previously fed the main comparison table, the Net P&L chart (whose
        // own height scaled with count($crossStrategy), unbounded), and the
        // Contribution Analysis tile grid with zero cap, fine for a handful
        // of strategies, not for a user with years of strategy iteration.
        // Capped at the top 20 by |P&L| (already the query's own sort
        // intent), consistent with this file's established instrument-list
        // cap size; "Show full detail" below stays uncapped since it's an
        // opt-in collapsed panel, not always-visible.
        $crossStrategyDisplay = $crossStrategy;
        usort($crossStrategyDisplay, fn($a, $b) => abs((float)$b['total_pnl']) <=> abs((float)$a['total_pnl']));
        $crossStrategyOverflow = max(0, count($crossStrategyDisplay) - 20);
        $crossStrategyDisplay = array_slice($crossStrategyDisplay, 0, 20);

        // Real change-event counts per strategy, replaces the old "vN"
        // column (business rules §13). One bulk query, not N+1.
        $csChangeCounts = [];
        if (!empty($crossStrategy)) {
            $csIds = array_column($crossStrategy, 'strategy_id');
            $csPlaceholders = implode(',', array_fill(0, count($csIds), '?'));
            $csCountRows = $db->pdo()->prepare("SELECT strategy_id, COUNT(*) AS n FROM strategy_change_events WHERE strategy_id IN ($csPlaceholders) GROUP BY strategy_id");
            $csCountRows->execute($csIds);
            foreach ($csCountRows->fetchAll(PDO::FETCH_ASSOC) as $r) { $csChangeCounts[$r['strategy_id']] = (int)$r['n']; }
        }
        ?>
        <!-- Strategy Comparison table, with "View all detail" collapse inside this card -->
        <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem;flex-wrap:wrap;gap:.5rem">
            <h4 style="font-size:.85rem;font-weight:600;color:var(--foreground);margin:0">Strategy Comparison<?= $crossStrategyOverflow > 0 ? ' (top 20 of ' . count($crossStrategy) . ' by |P&L|, see Show full detail for the rest)' : '' ?></h4>
            <!-- "View all strategies" is now a collapse toggle inside this card -->
            <button class="versions-toggle" onclick="togglePanel('strategyDetailPanel', this)">Show full detail ▾</button>
        </div>
        <div style="overflow-x:auto;margin-bottom:1.5rem">
            <table class="setup-table">
                <thead><tr><th>Strategy</th><th>Changes</th><th>Trades</th><th>Win Rate</th><th>Net P&amp;L</th><th>Profit Factor</th><th>Expectancy</th><th>Avg Reward</th><th>Status</th><th></th></tr></thead>
                <tbody>
                    <?php foreach ($crossStrategyDisplay as $cs): ?>
                    <tr>
                        <td><strong><?= htmlspecialchars($cs['name']) ?></strong></td>
                        <td><?= (int)($csChangeCounts[$cs['strategy_id']] ?? 0) ?></td>
                        <td><?= $cs['trades'] ?></td>
                        <td><?= $cs['win_rate'] ?>%</td>
                        <td class="<?= pnlClass($cs['total_pnl']) ?>"><?= pnlFormat($cs['total_pnl']) ?></td>
                        <td title="<?= $cs['profit_factor'] === null ? 'No losses yet on this strategy' : '' ?>"><?= $cs['profit_factor'] !== null ? number_format($cs['profit_factor'], 2) : 'No losses yet' ?></td>
                        <td class="<?= pnlClass($cs['expectancy']) ?>"><?= pnlFormat($cs['expectancy']) ?></td>
                        <td><?= $cs['average_reward'] ?>R</td>
                        <td>
                            <span class="pill <?= $statusPillClasses[$cs['status_label']] ?? 'pill-neutral' ?>"><?= $cs['status_label'] ?></span>
                            <?php if ($cs['status_context']): ?><div style="font-size:.65rem;color:var(--foreground-dim);margin-top:3px"><?= htmlspecialchars($cs['status_context']) ?></div><?php endif; ?>
                        </td>
                        <!-- drill-link: trailing table-cell per row, filtered by version -->
                        <td><a href="<?= journalTradesFilterUrl(['strategy_version_id' => $cs['version_id'], 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link">View trades →</a></td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>

        <!-- Expandable full-detail panel (replaces the old floating link) -->
        <div id="strategyDetailPanel" class="versions-panel">
            <div style="background:var(--background);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem;margin-bottom:1rem">
                <p style="font-size:.75rem;color:var(--foreground-dim);margin-bottom:.75rem">Extended metrics for all active strategies, risk, activity, and compliance context.</p>
                <div style="overflow-x:auto">
                    <table class="setup-table">
                        <thead><tr><th>Strategy</th><th>Avg Risk %</th><th>Avg Reward</th><th>Peak Daily Trades</th><th>Trades This Week</th><th>Losing Streak</th><th>Risk Breaches</th><th>Last Trade</th></tr></thead>
                        <tbody>
                            <?php foreach ($crossStrategy as $cs): ?>
                            <tr>
                                <td><strong><?= htmlspecialchars($cs['name']) ?></strong></td>
                                <td><?= round((float)($cs['average_risk'] ?? 0), 2) ?>%</td>
                                <td><?= $cs['average_reward'] ?>R</td>
                                <td><?= $cs['peak_daily_trades'] ?? '-' ?></td>
                                <td><?= $cs['trades_this_week'] ?? '-' ?></td>
                                <td><?= $cs['current_losing_streak'] ?? '-' ?></td>
                                <td><?php if (!empty($cs['risk_breaches'])): ?><a href="<?= journalTradesFilterUrl(['strategy_version_id' => $cs['version_id'], 'min_risk_pct' => ($cs['max_risk_per_trade_pct'] ?? 0) + RISK_BREACH_TOLERANCE_PCT, 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link"><?= $cs['risk_breaches'] ?> →</a><?php else: ?><?= $cs['risk_breaches'] ?? '-' ?><?php endif; ?></td>
                                <td style="color:<?= $cs['last_trade_color'] ?>"><?= htmlspecialchars($cs['last_trade_display']) ?></td>
                            </tr>
                            <?php endforeach; ?>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>

        <!-- ════════════════════════════════════════════════════════════════ -->
        <!-- Strategy Evolution subsection -->
        <!-- ════════════════════════════════════════════════════════════════ -->
        <?php if (!empty($strategyEvolutionData)): ?>
        <h4 style="font-size:.85rem;font-weight:600;color:var(--foreground);margin-bottom:.25rem">Strategy Evolution</h4>
        <p style="font-size:.72rem;color:var(--foreground-dim);margin-bottom:1rem;text-transform:uppercase;letter-spacing:.05em">Original declaration vs current declaration, for strategies with at least one recorded change</p>

        <?php foreach ($strategyEvolutionData as $evo): ?>
        <div style="background:var(--background);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem;margin-bottom:1rem">
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;flex-wrap:wrap;gap:.5rem">
                <strong style="font-size:.9rem;color:var(--foreground)"><?= htmlspecialchars($evo['strategy_name']) ?></strong>
                <span style="font-size:.72rem;color:var(--foreground-dim)"><?= (int)$evo['change_count'] ?> change<?= $evo['change_count'] === 1 ? '' : 's' ?> recorded</span>
            </div>
            <?php if ($evo['has_comparison']): ?>
            <div style="overflow-x:auto;margin-bottom:.75rem">
                <table class="setup-table">
                    <thead><tr><th>Metric</th><th>Original Declaration</th><th>Current Declaration</th><th>Gap</th></tr></thead>
                    <tbody>
                        <?php foreach ($evo['metrics'] as $m):
                            // Determine gap colour: strip symbols, check sign
                            $rawGap = str_replace(['+', '$', '%', ','], '', $m['gap']);
                            if (is_numeric($rawGap)) { $gapClass = (float)$rawGap > 0 ? 'positive' : ((float)$rawGap < 0 ? 'negative' : ''); }
                            else { $gapClass = ''; }
                        ?>
                        <tr>
                            <td style="font-family:var(--font-sans);font-weight:600"><?= htmlspecialchars($m['label']) ?></td>
                            <td><?= htmlspecialchars((string)$m['original']) ?></td>
                            <td><?= htmlspecialchars((string)$m['current']) ?></td>
                            <td class="<?= $gapClass ?>"><?= htmlspecialchars((string)$m['gap']) ?></td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <?php endif; ?>
            <div class="insight-text" style="font-size:.72rem;padding:.5rem .75rem;margin-bottom:.75rem">
                <?= htmlspecialchars($evo['verdict']) ?>
            </div>

            <!-- "View Change History", the strategy's real recorded changes, field by field -->
            <button class="versions-toggle" onclick="togglePanel('evo-changes-<?= $evo['strategy_id'] ?>', this)">View Change History ▾</button>
            <div id="evo-changes-<?= $evo['strategy_id'] ?>" class="versions-panel">
                <p style="font-size:.75rem;color:var(--foreground-muted);margin:.6rem 0"><?= htmlspecialchars($evo['narration']) ?></p>
                <?php foreach (array_reverse($evo['change_events']) as $ev): ?>
                <div style="padding:.5rem 0;border-top:1px solid var(--border)">
                    <div style="font-size:.68rem;font-weight:700;color:var(--foreground-dim);margin-bottom:.25rem"><?= htmlspecialchars(date('M j, Y', strtotime($ev['changed_at']))) ?></div>
                    <?php foreach ($ev['fields'] as $f): ?>
                    <div style="font-size:.72rem;color:var(--foreground-muted);padding:.1rem 0"><strong style="color:var(--foreground)"><?= htmlspecialchars(StrategyChangeHistory::fieldLabel($f['field_name'])) ?>:</strong> <?= htmlspecialchars(StrategyChangeHistory::displayValue($f['previous_value'])) ?> → <?= htmlspecialchars(StrategyChangeHistory::displayValue($f['new_value'])) ?></div>
                    <?php endforeach; ?>
                </div>
                <?php endforeach; ?>
            </div>
        </div>
        <?php endforeach; ?>
        <?php endif; ?>

        <!-- Net P&L by Strategy chart -->
        <h4 style="font-size:.85rem;font-weight:600;color:var(--foreground);margin-bottom:.5rem">Net Profit &amp; Loss by Strategy<?= $crossStrategyOverflow > 0 ? ' (top 20 by |P&L|)' : '' ?></h4>
        <div style="margin-bottom:1.5rem">
            <div class="chart-wrap" style="min-height:<?= count($crossStrategyDisplay) * 40 + 20 ?>px;max-height:350px">
                <canvas id="crossStrategyPnlChart"></canvas>
            </div>
        </div>

        <!-- Contribution Analysis -->
        <h4 style="font-size:.85rem;font-weight:600;color:var(--foreground);margin-bottom:.75rem">Contribution Analysis<?= $crossStrategyOverflow > 0 ? ' (top 20 by |P&L|)' : '' ?></h4>
        <div class="stat-grid" style="margin-bottom:1.25rem">
            <?php foreach ($crossStrategyDisplay as $cs): ?>
            <div class="stat-tile">
                <p class="label"><?= htmlspecialchars($cs['name']) ?></p>
                <p class="value <?= pnlClass($cs['total_pnl']) ?>" style="font-size:1rem"><?= pnlFormat($cs['total_pnl']) ?></p>
                <p class="sub"><?= $cs['percentage_of_profit'] >= 0 ? '+' : '' ?><?= $cs['percentage_of_profit'] ?>% of profits · <?= $cs['percentage_of_trades'] ?>% of trades<?php if ($cs['losses'] > 0): ?> · <?= $cs['percentage_of_losses'] ?>% of losses<?php endif; ?></p>
                <!-- drill-link: tile-bottom, per strategy version -->
                <a href="<?= journalTradesFilterUrl(['strategy_version_id' => $cs['version_id'], 'from' => $startDateOnly, 'to' => $endDateOnly]) ?>" class="drill-link drill-link-tile">View trades →</a>
            </div>
            <?php endforeach; ?>
        </div>

        <!-- Strategy Concentration, capped list, remainder summarised numerically -->
        <?php if (count($crossStrategy) >= 2 && $concentrationPercentage > 0): ?>
        <h4 style="font-size:.85rem;font-weight:600;color:var(--foreground);margin-bottom:.5rem">Strategy Concentration</h4>
        <?php
        // Build contribution rows, sort descending, cap display at 5
        $sortedContrib = $crossStrategy;
        usort($sortedContrib, fn($a, $b) => $b['total_pnl'] <=> $a['total_pnl']);
        $displayCap   = 5;
        $displayed    = array_slice($sortedContrib, 0, $displayCap);
        $remaining    = array_slice($sortedContrib, $displayCap);
        $remainingPct = count($remaining) > 0 ? round(array_sum(array_column($remaining, 'percentage_of_profit')), 1) : 0;
        ?>
        <div class="insight-text" style="border-left:3px solid var(--primary);margin-bottom:0">
            <strong><?= $concentrationPercentage ?>%</strong> of your total profits came from your top 2 strategies.
            <div style="margin-top:.5rem">
                <?php foreach ($displayed as $dc): ?>
                <div style="display:flex;justify-content:space-between;font-size:.75rem;padding:.2rem 0;border-bottom:1px solid var(--border)">
                    <span><strong><?= htmlspecialchars($dc['name']) ?></strong></span>
                    <span class="<?= pnlClass($dc['total_pnl']) ?>"><?= $dc['percentage_of_profit'] >= 0 ? '+' : '' ?><?= $dc['percentage_of_profit'] ?>%</span>
                </div>
                <?php endforeach; ?>
                <?php if (!empty($remaining)): ?>
                <div style="font-size:.72rem;color:var(--foreground-dim);padding-top:.35rem">
                    ...and <?= count($remaining) ?> more <?= count($remaining) === 1 ? 'strategy' : 'strategies' ?> contributing the remaining <?= $remainingPct >= 0 ? '+' : '' ?><?= $remainingPct ?>%.
                </div>
                <?php endif; ?>
            </div>
            <?php if ($concentrationPercentage >= 70): ?>
            <p style="margin-top:.5rem;font-size:.75rem">These strategies are currently driving the majority of your portfolio performance.</p>
            <?php elseif ($concentrationPercentage >= 50): ?>
            <p style="margin-top:.5rem;font-size:.75rem">Your performance is moderately concentrated.</p>
            <?php else: ?>
            <p style="margin-top:.5rem;font-size:.75rem">Your profits are distributed across multiple strategies.</p>
            <?php endif; ?>
        </div>
        <?php endif; ?>
    </div><!-- /.section-card analytics -->
    </div><!-- /#analytics-across-strategies-wrapper -->

    <script>
    if (typeof Chart !== 'undefined') {
        (function() {
            var canvas = document.getElementById('crossStrategyPnlChart');
            if (!canvas) return;
            var L = <?= json_encode(array_map(fn($cs) => $cs['name'], $crossStrategyDisplay)) ?>;
            var D = <?= json_encode(array_map(fn($cs) => $cs['total_pnl'], $crossStrategyDisplay)) ?>;
            var B = <?= json_encode(array_map(fn($cs) => $cs['total_pnl'] >= 0 ? '#26A69A' : '#EF5350', $crossStrategyDisplay)) ?>;
            var dL = L.map(function(n) { return n.length > 28 ? n.substring(0, 27) + '\u2026' : n; });
            new Chart(canvas.getContext('2d'), {
                type: 'bar',
                data: { labels: dL, datasets: [{ data: D, backgroundColor: B, borderRadius: 4, borderSkipped: false, barPercentage: 0.8 }] },
                options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { color: '#2A2E39' } }, y: { grid: { display: false } } } }
            });
        })();
    }
    </script>
    <?php endif; ?>
        </div>
    </div>
    <?php endif; /* $accountView !== 'comparison' */ ?>

    <hr style="border:none;border-top:1px solid var(--border);margin:2rem 0">

</main>

<script>
/* ── Shared collapse/expand for all version panels ── */
function togglePanel(id, btn) {
    var panel = document.getElementById(id);
    if (!panel) return;
    var open = panel.classList.toggle('open');
    if (btn) btn.textContent = btn.textContent.replace(open ? '▾' : '▴', open ? '▴' : '▾');
}

/* ── Progressive disclosure: top-level accordion groups (Trade Analysis, 4-Stage Performance Audit, Prop Firm/Live Account Analytics, Advanced Statistics) ── */
function toggleAccordionGroup(id) {
    var group = document.getElementById(id);
    if (!group) return;
    group.classList.toggle('open');
}

/* ── 4-Stage Summary: "Show More" reveals the standalone Intent vs Reality
   card beneath the whole section (2026-07-27, previously expanded
   secondary metrics inside the stage cards themselves; see CLAUDE.md). ── */
function toggleFsSecondary(btn) {
    var card = document.getElementById('fsGapCard');
    if (!card) return;
    var open = card.classList.toggle('open');
    btn.textContent = open ? 'Show Less ▴' : 'Show More ▾';
}

/* ── Instrument Performance: click a row to expand its classification
   detail + "View trades" link, one row open at a time isn't required, so
   each toggles independently. ── */
function toggleInstrPerfRow(id, rowEl) {
    var detail = document.getElementById(id);
    if (!detail) return;
    var open = detail.classList.toggle('open');
    var chevron = rowEl.querySelector('.tip-chevron');
    if (chevron) chevron.textContent = open ? '▴' : '▾';
}

/* ── Charts ── */
<?php if ($totalTrades > 0): ?>
function makeCharts() {
    if (typeof Chart === 'undefined') { setTimeout(makeCharts, 100); return; }
    Chart.defaults.color        = '#B2B5BE';
    Chart.defaults.borderColor  = '#2A2E39';
    Chart.defaults.font.family  = 'Inter, system-ui, sans-serif';
    Chart.defaults.font.size    = 11;

    <?php if (!empty($strategyChartLabels)): ?>
    (function() {
        var c = document.getElementById('strategyPnlChart');
        if (!c) return;
        new Chart(c, {
            type: 'bar',
            data: { labels: <?= json_encode($strategyChartLabels) ?>, datasets: [{ data: <?= json_encode($strategyChartData) ?>, backgroundColor: <?= json_encode($strategyChartBg) ?>, borderRadius: 4, borderSkipped: false, barPercentage: 0.8 }] },
            options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: '#2A2E39' } } } }
        });
    })();
    <?php endif; ?>

    <?php if (!empty($instrumentChartRows)): ?>
    (function() {
        var c = document.getElementById('instrumentChart');
        if (!c) return;
        new Chart(c, {
            type: 'bar',
            data: { labels: <?= json_encode($instrumentLabels) ?>, datasets: [{ data: <?= json_encode($instrumentChartDataArr) ?>, backgroundColor: <?= json_encode($instrumentChartBackgrounds) ?>, borderRadius: 4 }] },
            options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: '#2A2E39' } } } }
        });
    })();
    <?php endif; ?>

    <?php if ($accountView === 'live' && !empty($liveAccountAgg)): ?>
    (function() {
        var c = document.getElementById('capitalFlowChart');
        if (!c) return;
        new Chart(c, {
            type: 'bar',
            data: {
                labels: ['Total Deposits', 'Total Withdrawals', 'Net Trading P&L', 'Current Balance'],
                datasets: [{
                    data: [<?= $liveAccountAgg['total_deposits'] ?>, <?= $liveAccountAgg['total_withdrawal_value'] ?>, <?= $liveAccountAgg['combined_pnl'] ?>, <?= $liveAccountAgg['total_current_balance'] ?>],
                    backgroundColor: ['#60A5FA', '#EF5350', <?= $liveAccountAgg['combined_pnl'] >= 0 ? "'#26A69A'" : "'#EF5350'" ?>, '#E09B3A'],
                    borderRadius: 4
                }]
            },
            options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', plugins: { legend: { display: false } }, scales: { x: { grid: { color: '#2A2E39' } }, y: { grid: { display: false } } } }
        });
    })();
    <?php endif; ?>

    <?php if (!empty($classificationRows)):
        // Hex equivalents of $classificationBarColors' CSS custom properties —
        // canvas fill colors can't resolve var(--positive) the way inline
        // element styles can, so this mirrors assets/tokens.css's real values
        // rather than introducing a new palette.
        $classificationPieHex = [
            'disciplined_win' => '#26A69A', 'elevated_win'  => '#26A69A',
            'breakeven'       => '#E09B3A', 'controlled_loss' => '#E09B3A',
            'loss_high_risk'  => '#F97316', 'tilt_warning'    => '#F97316',
                'revenge_trade'   => '#EF5350',
        ];
        $classificationPieLabels = []; $classificationPieData = []; $classificationPieColors = [];
        foreach ($classificationRows as $cpr) {
            $cpc = $cpr['trade_classification'] ?? '';
            $classificationPieLabels[] = ucwords(str_replace('_', ' ', $cpc));
            $classificationPieData[]   = (int)$cpr['count'];
            $classificationPieColors[] = $classificationPieHex[$cpc] ?? '#B2B5BE';
        }
    ?>
    (function() {
        var c = document.getElementById('classificationPieChart');
        if (!c) return;
        new Chart(c, {
            type: 'pie',
            data: { labels: <?= json_encode($classificationPieLabels) ?>, datasets: [{ data: <?= json_encode($classificationPieData) ?>, backgroundColor: <?= json_encode($classificationPieColors) ?>, borderColor: '#131722', borderWidth: 2 }] },
            options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 10, font: { size: 10 } } } } }
        });
    })();
    <?php endif; ?>

    <?php if ($accountView !== 'comparison' && $fsGradeTotal > 0): ?>
    (function() {
        var c = document.getElementById('fsGradePieChart');
        if (!c) return;
        // Chart-only short labels (2-4 words, e.g. "Disciplined Wins"), kept
        // deliberately separate from the full coaching text used by the
        // legend rows' `title=` tooltips just below this chart. A Chart.js
        // hover tooltip needs to identify the slice without obscuring the
        // visualization; the full paragraph belongs on the legend/labels,
        // not inside the chart itself (2026-07-27 item 1 finishing pass).
        var fsGradeChartLabelMap = <?= json_encode($fsGradeChartLabel) ?>;
        new Chart(c, {
            type: 'pie',
            data: {
                labels: <?= json_encode(array_values(array_filter(['A+', 'A', 'B', 'C'], fn($g) => $fsGradeCounts[$g] > 0))) ?>,
                datasets: [{
                    data: <?= json_encode(array_values(array_filter($fsGradeCounts, fn($v) => $v > 0))) ?>,
                    backgroundColor: <?= json_encode(array_values(array_intersect_key($fsGradeColors, array_filter($fsGradeCounts, fn($v) => $v > 0)))) ?>,
                    borderColor: '#131722', borderWidth: 2
                }]
            },
            options: {
                responsive: true, maintainAspectRatio: false,
                plugins: {
                    legend: { display: false },
                    tooltip: {
                        callbacks: {
                            // Short chart-native tooltip only: grade + a
                            // 2-4 word name, then the count/share. No
                            // paragraph-length coaching text here, that
                            // stays on the legend rows below the chart.
                            title: function(items) { return items[0].label + ', ' + (fsGradeChartLabelMap[items[0].label] || ''); },
                            label: function(item) { return item.parsed + ' trade(s), ' + (item.dataset.data.reduce((a,b)=>a+b,0) ? Math.round(item.parsed / item.dataset.data.reduce((a,b)=>a+b,0) * 100) : 0) + '%'; }
                        }
                    }
                }
            }
        });
    })();
    <?php endif; ?>
}
makeCharts();
<?php endif; ?>

</script>

</body>
</html>