<?php
/**
 * JOURNAL
 * ─────────────────────────────────────────────────────────────
 * ARCHITECTURE REDESIGN (2026-07-31): Journal is now exactly two
 * workspaces — Active Journeys (live account/trade management + coaching;
 * ?workspace=active, the default) and Trading Archive (historical,
 * evidence-backed learning from finished accounts; ?workspace=archive).
 * The old Journal Coaching / Journal Trades split is gone.
 *
 * Active Journeys is the former "Right Now" cockpit (renderRightNow(),
 * still using its historical `rn` internal function/variable prefix —
 * renamed only in what the trader sees, not in ~60 internal helper names,
 * since that would be pure churn with no behavioral value) with the old
 * separate Journal Trades filterable list folded in as its "Trade
 * Journal" section (renderTradeJournalSection(), built on
 * src/JournalTradesQuery.php — the same query/filter/row-render source of
 * truth the api/journal/trades-more.php "Show More" endpoint already
 * used, extended with a free-text `q` search filter this pass). The old
 * zone=active/completed/live placeholder shells (2026-07-29 cleanup pass,
 * "being redesigned — check back soon") are deleted, not redirected past
 * — they were never real pages, just dead nav.
 *
 * Trading Archive is a real, data-grounded navigation shell this pass
 * (type × lens × program tabs, real account counts) — its actual insights
 * engine (phase difficulty scoring, autopsy analysis, evidence-linked
 * trade drill-through) is deliberately deferred to a dedicated follow-up
 * session; see renderTradingArchive()'s own doc comment.
 *
 * Journal Trades' single-trade view (Trade Details / 4-Stage) remains the
 * approved empty shell from the 2026-07-29 cleanup pass, untouched by
 * this redesign — reachable via ?trade_id=X from either workspace.
 */

$userId = $auth->requireAuth();

// ── Validation Workflow — Journal is shared, workflow-SCOPED
// infrastructure, not a main-workflow-only page (2026-08-18 correction) ──
// SUPERSEDED: the 2026-08-16 blanket ValidationService::gateMainWorkflowOnly()
// redirect this replaced was over-broad. Its own stated reason — "the
// ACCOUNT-level queries feeding this page's account picker
// (rnFetchActivePropAccounts()/rnFetchActiveLiveAccounts()) were never
// gated at all" — was a real, correctly-diagnosed bug, but the fix for an
// unscoped QUERY is to scope the query, not to remove the whole page from
// one workflow. That real gap is now closed directly at the two functions
// (both filter on trading_accounts.workflow_context — see their own
// comments below) rather than worked around by blocking the page.
//
// Per explicit product decision: BOTH workflows get Journal, each strictly
// scoped to its own accounts/trades — never a separate substitute page.
// This restores the design this file's own 2026-08-12 workflow-isolation
// audit had already reasoned through and built for (see the
// `$journalWorkflowState === 'validation'` fallback/guards below, which
// were live-verified working before the 2026-08-16 gate made them
// unreachable dead code): Trading Archive (?workspace=archive) — a
// main-workflow-only, phase-progression/promotion-history concept with no
// validation equivalent — still gracefully falls back to Active Journeys
// for a validating user (its own in-page tab is hidden too), but Active
// Journeys itself (Journal's real, sidebar-linked default view) now
// renders normally for both workflows, showing only that workflow's own
// accounts and trades. journal-validation.php (Validation Tracker) is
// unaffected and still exists as its own separate progress-tracking page —
// this change does not remove or replace it, only stops Journal itself
// from bouncing a validating user away.
$journalWorkflowState = ValidationService::getWorkflowState($userId, $db);

require_once __DIR__ . '/../src/JournalTradesQuery.php';
require_once __DIR__ . '/../api/analytics/engine.php';
require_once __DIR__ . '/../api/analytics/edge_health_engine.php';
require_once __DIR__ . '/../src/JourneySegmentation.php';
require_once __DIR__ . '/../src/PropFirmRuleEngine.php';
require_once __DIR__ . '/../src/RiskPanelEngine.php';
require_once __DIR__ . '/../src/AccountSummaryView.php';
require_once __DIR__ . '/../src/TradingArchiveEngine.php';
require_once __DIR__ . '/../src/StrategyChangeHistory.php';

/**
 * Redirects every pre-redesign URL shape (?view=coaching|trades, bare
 * ?id=X from Dashboard's trade link, ?zone=...&category=...) to its
 * equivalent under the new ?workspace=active|archive scheme, before any
 * other routing runs. Nothing that used to link into Journal should ever
 * 404 or land on a stale shell.
 */
function legacyJournalRedirect(): void {
    if (isset($_GET['workspace'])) return; // already on the new scheme

    $view = $_GET['view'] ?? (isset($_GET['zone']) ? 'coaching' : null);
    if ($view === null && !isset($_GET['id'])) return; // nothing legacy to redirect

    $carry = $_GET;
    unset($carry['view'], $carry['zone'], $carry['category'], $carry['account']);

    // Bare ?id=X (Dashboard's trade link) or ?view=coaching&id=X — both
    // redirected straight to the trade view before this redesign too.
    if (!empty($_GET['id'])) {
        unset($carry['id']);
        header('Location: ' . '/traderalign/journal?' . http_build_query(array_merge($carry, ['workspace' => 'active', 'trade_id' => $_GET['id']])));
        exit;
    }

    if ($view === 'trades') {
        // Every existing filter param (instrument/account_id/account_type/
        // session/from/to/classification/strategy_version_id/status/
        // min_risk_pct/trade_id) carries through unchanged — the merged
        // Trade Journal section reads the exact same param names.
        header('Location: ' . '/traderalign/journal?' . http_build_query(array_merge($carry, ['workspace' => 'active'])));
        exit;
    }

    if ($view === 'coaching') {
        $zone = $_GET['zone'] ?? 'now';
        $category = $_GET['category'] ?? null;
        if ($zone === 'completed') {
            $lens = $category === 'passed' ? 'success' : 'failure';
            $params = array_merge($carry, ['workspace' => 'archive', 'archive_lens' => $lens, 'archive_type' => 'prop']);
            if (!empty($_GET['account'])) $params['account_id'] = $_GET['account'];
            header('Location: ' . '/traderalign/journal?' . http_build_query($params));
            exit;
        }
        $params = array_merge($carry, ['workspace' => 'active']);
        if ($zone === 'live') $params['rn_type'] = 'live';
        if ($zone === 'active' && $category && $category !== 'unclassified') {
            $params['rn_type'] = 'prop';
            $params['rn_challenge'] = $category;
        }
        header('Location: ' . '/traderalign/journal?' . http_build_query($params));
        exit;
    }
}
legacyJournalRedirect();

// ─────────────────────────────────────────────────────────────
// Top-level workspace switcher.
// ─────────────────────────────────────────────────────────────
$workspace = $_GET['workspace'] ?? 'active';
if (!in_array($workspace, ['active', 'archive'], true)) $workspace = 'active';

// 2026-08-18 correction: the 2026-08-12 workflow-isolation audit's own
// fallback here (workspace=archive silently forced back to 'active' while
// validating) was the wrong fix for a real problem. Its stated reason —
// TradingArchiveEngine's queries had no workflow-awareness at all — was a
// real, correctly-diagnosed gap, but the fix for unscoped DATA is to scope
// the data, not to hide the whole page for one workflow. That gap is now
// closed directly inside TradingArchiveEngine (gatherPropSegments()/
// gatherLiveSegments()/gatherAccountSegmentsById() all filter on
// trading_accounts.workflow_context and the real workflow's is_validation
// value, the same pattern rnFetchActivePropAccounts()/validationScopeSql()
// already use elsewhere in this file) — Trading Archive is real, shared
// Journal functionality, exactly like Active Journeys, and must stay
// reachable and fully functional in both workflows, each seeing only its
// own accounts/trades.

// activeJourneysUrl()/archiveUrl() moved to src/RenderHelpers.php 2026-08-18
// (required globally by index.php, so dashboard.php's alert-linking can
// reuse them too) — still available here under the same names, just
// defined once instead of twice.

/**
 * Ground-truth account-state derivation, from the real confirmed
 * lifecycle_state values (see CLAUDE.md's Lifecycle Rule) — still needed
 * here purely to compute the navigation tabs' account counts.
 */
function deriveState($lifecycleState) {
    $l = (string)($lifecycleState ?? '');
    if ($l === 'breached') return 'breached';
    if (str_contains($l, '_complete')) return 'passed';
    if ($l === 'funded_active') return 'funded';
    if (str_contains($l, 'phase_') && str_contains($l, '_active')) return 'active_challenge';
    if ($l === 'closed') return 'closed';
    return 'active';
}

/**
 * Ground-truth classification using ONLY confirmed columns:
 * ca.challenge_type and ca.lifecycle_state (via challenge_lifecycle
 * alias). No live account has a challenge_type — that's how live and
 * prop-firm accounts are told apart. Used only to build nav-tab counts
 * now; no per-account content is rendered from this anymore.
 */
function classifyAccount($acc) {
    $challengeType = $acc['challenge_type'] ?? null;
    if (empty($challengeType)) {
        return ['zone' => 'live', 'category' => 'live'];
    }
    $state = deriveState($acc['challenge_lifecycle'] ?? null);
    return match($state) {
        'breached'         => ['zone' => 'completed', 'category' => 'breached'],
        'passed'           => ['zone' => 'completed', 'category' => 'passed'],
        'closed'           => ['zone' => 'completed', 'category' => 'closed'], // no nav tab renders this anymore; kept classifiable rather than silently dropped
        'funded'           => ['zone' => 'active', 'category' => 'funded'],
        'active_challenge' => ['zone' => 'active', 'category' => $challengeType],
        default            => ['zone' => 'active', 'category' => 'unclassified'],
    };
}

function countIn($byZoneCategory, $zone, $category) {
    return count($byZoneCategory[$zone][$category] ?? []);
}

// ════════════════════════════════════════════════════════════════════
// RIGHT NOW — real, production implementation of the approved prototype
// (2026-07-29). Everything below this block through renderRightNow()
// backs ONLY the "now" zone; every other zone/category in Journal
// Coaching is still the empty nav-only shell from the prior cleanup pass.
//
// DISCLOSED DEVIATION FROM THE PROTOTYPE, confirmed against the real
// schema/data before writing any of this: the prototype's mockup shows
// "Phase 1 – Attempt 1", "Phase 2 – Attempt 2", "Funded – Funded" as if
// they were three separate accounts. They are not, in this app's real
// data model — api/accounts/create.php's 'promote' mode advances phase
// IN PLACE on the SAME trading_accounts/challenge_accounts row
// (confirmed by reading that file and by querying real accounts, e.g.
// "aau 5K - Attempt 1" goes phase_1 -> phase_2 -> funded_active while its
// own attempt_number stays 1 the whole time). attempt_number instead
// differentiates separate challenge PURCHASES (re-attempts after a
// breach), not phase steps within one purchase. So every phase card
// below reconstructs a real segment of ONE account's own history — never
// fabricating separate "Attempt 2"/"Attempt 3" labels that don't exist
// for that account. The account's own real label is used consistently
// across all of its phase cards instead.
//
// SEGMENTATION (rewritten 2026-07-29, second pass — real bug found and
// fixed, reported by Kevin as a data-segmentation issue, not a UI one).
// Trades are partitioned into Phase 1 / Phase 2 / Phase 3 / Funded
// buckets using each trade's OWN real phase tag (trades.prop_firm_funded
// / prop_firm_phase / prop_phase_at_entry), NOT by matching trade dates
// against lifecycle_events date windows. That date-window approach was
// tried first and found unreliable on real data: most accounts have
// sparse or zero lifecycle_events, and even when present they can be
// logged with real lag relative to when a trade was actually taken
// under that phase (confirmed on a real account: a trade tagged
// phase_2 was captured a full hour before its own PHASE_2_STARTED
// event was logged) — both caused real trades to silently vanish from
// every phase's table. This partitioning (plus the Instant Funding /
// funded-state corrections layered on top of it) now lives entirely in
// JourneySegmentation (src/JourneySegmentation.php, 2026-08-06
// extraction — see that file's own header for the full account of what
// was found and why the fix works, and for the class-privacy reasoning
// behind moving it out of this file). Every stage renders through the
// exact same
// renderRnTradeTable() component — same columns, same layout, same
// Show More/Show Less behavior — only the dataset passed to it differs.
// ════════════════════════════════════════════════════════════════════

function rnChallengeTypeLabel($type) {
    return match ($type) {
        'one_step'        => '1 Step Challenge',
        'two_step'         => '2 Step Challenge',
        'three_step'       => '3 Step Challenge',
        'instant_funding'  => 'Instant Funding',
        default            => 'Challenge',
    };
}

function rnFormatDuration($minutes) {
    if ($minutes === null) return '-';
    $minutes = (float)$minutes;
    if ($minutes < 1) return '<1m';
    if ($minutes < 60) return round($minutes) . 'm';
    $h = floor($minutes / 60);
    $m = round($minutes - $h * 60);
    return $h . 'h ' . str_pad((string)$m, 2, '0', STR_PAD_LEFT) . 'm';
}

/** Pip/point delta between two prices for a given instrument — a real, computed value (not fabricated), using the same forex/JPY/other heuristic already established for pip-value math elsewhere in this app. */
function rnPipsOrPoints($instrument, $priceDiff) {
    $instrument = strtolower((string)$instrument);
    if ($instrument === '' || $priceDiff === null) return null;
    if (str_contains($instrument, 'jpy')) return round($priceDiff * 100, 1);
    if (preg_match('/^[a-z]{6}$/', $instrument)) return round($priceDiff * 10000, 1);
    return round($priceDiff, 2); // metals/indices/commodities: raw points, not "pips"
}

/**
 * Item 8 (2026-08-04): a real "Show More"/"Show Less" inline truncation for
 * Trader Notes, shared by both the Trades table (renderRnTradeTable()'s
 * notes cell) and the single-trade 4-Stage Lifecycle (Block 3's Trader
 * Notes row) — one PHP helper for the initial server render, mirrored by
 * one JS helper (rnBuildNoteHtml() near the end of this file) for the
 * Trades table's client-side re-render after a note is saved/edited. Short
 * enough notes render as plain nl2br'd text (byte-identical to before);
 * longer notes collapse to the first RN_NOTE_TRUNCATE_LIMIT characters with
 * a Show More toggle that reveals the rest inline — never a modal, never
 * a page-layout-breaking wall of text.
 */
const RN_NOTE_TRUNCATE_LIMIT = 220;
/** Item 8: reasonable maximum length for a trade note — ~300 words at a typical ~6 chars/word (incl. space). Enforced both client-side (this constant feeds the textarea's maxlength) and server-side (api/journal/update-note.php). */
const RN_NOTE_MAX_LENGTH = 1800;
function rnTruncatedNoteHtml(string $rawNotes, int $limit = RN_NOTE_TRUNCATE_LIMIT): string {
    $rawNotes = trim($rawNotes);
    $fullHtml = nl2br(htmlspecialchars($rawNotes), false);
    if (mb_strlen($rawNotes) <= $limit) return $fullHtml;
    $shortHtml = nl2br(htmlspecialchars(mb_substr($rawNotes, 0, $limit) . '…'), false);
    return '<span class="rn-note-wrap" data-expanded="0">'
        . '<span class="rn-note-short">' . $shortHtml . '</span>'
        . '<span class="rn-note-full" style="display:none">' . $fullHtml . '</span>'
        . ' <button type="button" class="tl4-expand-btn rn-note-more" onclick="rnToggleNoteText(this)">Show More</button>'
        . '<button type="button" class="tl4-expand-btn rn-note-less" style="display:none" onclick="rnToggleNoteText(this)">Show Less</button>'
        . '</span>';
}

/** Real trade_classification values, humanized for the STATE column. */
function rnStateInfo($classification) {
    return match ($classification) {
        'disciplined_win' => ['Disciplined', 'rn-state-good'],
        'controlled_loss'  => ['Controlled', 'rn-state-neutral'],
        'breakeven'        => ['Breakeven', 'rn-state-neutral'],
        'elevated_win'     => ['Elevated', 'rn-state-warn'],
        'loss_high_risk'   => ['High Risk', 'rn-state-bad'],
        'tilt_warning'     => ['⚠ Tilt Warning', 'rn-state-bad'],
        'revenge_trade'    => ['⚠ Revenge', 'rn-state-bad'],
        default            => ['-', 'rn-state-neutral'],
    };
}

/** Qualitative tag for a 0-100 consistency score, wording matched to the approved prototype ("Needs Improvement", not Reports' own "Weak"/"Poor" wording — different page, different mockup, same underlying score). Orphaned as of 2026-08-18: its last 4 call sites (the Active Coaching / Funded / Live card "Execution Consistency" shield tiles) were all deleted as a real, reported metric-contradiction problem (this EdgeHealthEngine score could independently read "Excellent" while the account was mid-drawdown or on a losing streak). Left in place, unused, per this file's established orphaned-code convention. */
function rnScoreTag($score) {
    if ($score === null) return ['-', 'rn-score-neutral'];
    if ($score >= 80) return ['Excellent', 'rn-score-good'];
    if ($score >= 65) return ['Good', 'rn-score-good'];
    if ($score >= 50) return ['Fair', 'rn-score-warn'];
    return ['Needs Improvement', 'rn-score-bad'];
}

/**
 * Item 3 — every score on this page needs a self-explaining tooltip: what it
 * measures, how it's calculated, what raises it, what lowers it. The
 * All-Time and Last-30-Days tiles are the SAME underlying metric
 * (EdgeHealthEngine's Execution Consistency factor — the identical
 * consistency_score field getPeriodFactors() returns, only scoped to a
 * different date range each time) — not two different metrics, so the
 * tooltip says so explicitly rather than implying otherwise. Both tiles are
 * now labeled "Execution Consistency" (matching the Full Coaching
 * Analysis workspace's own name for this exact factor) with the time
 * window as a visible qualifier, not a name a trader has to guess applies.
 */
/** Item 3 — "what/how/increases/decreases" tooltip for each of EdgeHealthEngine's 4 weighted factor scores shown in Full Coaching Analysis. Real weights/mechanics, not generic copy — mirrors the engine's own documented weighting (Risk of Ruin 35%, Edge Quality 30%, Execution Consistency 20%, Discipline Trend 15%). */
function rnEdgeFactorTooltip(string $key): string {
    return match ($key) {
        'risk_of_ruin' => "Weighted 35% of your overall Edge Health. Estimates the probability of a catastrophic account decline continuing at your current win rate, average risk, and position sizing. It rises when risk per trade is small and consistent relative to your edge; it falls when position sizing is aggressive relative to how often you actually win.",
        'edge_quality' => "Weighted 30% of your overall Edge Health. Measures whether your trades produce a real statistical edge: win rate combined with average reward-to-risk. It rises with a higher win rate and/or a better average reward relative to risk; it falls when wins are too small, too rare, or too dependent on outsized single trades to be reliable.",
        // 2026-08-18: the "Consistency tiles above" this used to point to
        // (the Active Coaching / Funded / Live card shield tiles) were all
        // deleted as a real, reported metric-contradiction problem — this
        // factor card inside Full Coaching Analysis is now the ONLY place
        // this specific EdgeHealthEngine score is shown anywhere on the
        // page, so the tooltip is self-contained rather than pointing at
        // tiles that no longer exist.
        'consistency' => "Weighted 20% of your overall Edge Health. Measures how uniform your risk-per-trade and your win/loss outcomes have been across this account's real trade history, scored 0-100. It rises when position sizing stays steady trade-to-trade and outcomes cluster predictably; it falls when risk swings wildly or results are erratic.",
        'discipline' => "Weighted 15% of your overall Edge Health. Compares your expectancy in the first half of this trade sequence against the second half. It rises when your results are improving over time (later trades outperforming earlier ones); it falls when results are deteriorating trade-to-trade.",
        default => '',
    };
}

/** Orphaned as of 2026-08-18 — see rnScoreTag()'s own doc comment. Left in place, unused. */
function rnConsistencyTooltip(bool $allTime): string {
    $scope = $allTime
        ? "this account's ENTIRE trade history (every closed trade, all-time)"
        : "only this account's last 30 days of closed trades";
    $other = $allTime ? 'a separate Last 30 Days reading' : 'a separate All-Time reading';
    return "Measures how uniform your risk-per-trade and your win/loss outcomes have been, scored 0-100, the same Execution Consistency factor used in Full Coaching Analysis, here scored over {$scope}. It rises when position sizing stays steady trade-to-trade and outcomes cluster predictably; it falls when risk swings wildly or results are erratic. This tile and {$other} elsewhere on this page are the SAME metric at two different time windows, not two different scores.";
}

// ────────────────────────────────────────────────────────────────────
// PRODUCTION-POLISH PASS (2026-07-29) — Current Goal, checklist-driven
// coaching, challenge-program filter, and per-stage completion messages.
// Everything below reuses the existing lifecycle/phase-segmentation
// architecture untouched — these are additive helpers feeding the same
// phase/segment cards, never a parallel data model.
// ────────────────────────────────────────────────────────────────────

/** The account's one real active goal (if any) — see account_goals table, migration_add_account_goals.sql. */
function rnFetchActiveGoal(string $accountId, string $userId, $db): ?array {
    $rows = $db->raw(
        "SELECT * FROM account_goals WHERE account_id = :aid AND user_id = :uid AND status = 'active' ORDER BY created_at DESC LIMIT 1",
        [':aid' => $accountId, ':uid' => $userId]
    );
    return $rows[0] ?? null;
}

/**
 * Real, always-recomputed progress math for a goal — never persisted,
 * never hardcoded. $currentBalance/$avgWin/$remainingToTarget all come
 * from the same live trade-derived figures the rest of the page already
 * computes (funded card's total_profit, live segment's account_metrics).
 */
function rnGoalProgress(array $goal, float $currentBalance, ?float $avgWin): array {
    $target = (float)$goal['target_balance'];
    $start = (float)$goal['starting_balance'];
    $span = $target - $start;
    $gained = $currentBalance - $start;
    $remaining = max(0.0, $target - $currentBalance);
    $achieved = $currentBalance >= $target;
    $pct = $achieved ? 100.0 : ($span > 0 ? max(0.0, min(100.0, ($gained / $span) * 100)) : 0.0);
    $expectedWinsNeeded = $achieved ? 0 : (($avgWin !== null && $avgWin > 0) ? (int)ceil($remaining / $avgWin) : null);
    return [
        'target' => $target, 'start' => $start, 'remaining' => $remaining,
        'pct' => $pct, 'achieved' => $achieved, 'gained' => $gained,
        'expected_wins_needed' => $expectedWinsNeeded,
    ];
}

/**
 * Real goal coaching, built from this account's own numbers (win rate,
 * streak, classification counts, consistency score) rather than generic
 * "keep going" copy — the same signals renderRnCoachingWorkspace() already
 * surfaces for "Behaviours to Avoid", reused here rather than reinvented.
 *
 * Items 15/16 (2026-07-29 mission-alignment pass): keys renamed and reordered
 * to match the approved priority — the trader should see what needs
 * attention FIRST, not last. Rendered order in renderRnGoalSection() is now
 * Highest Priority -> Keep Doing -> Focus On Improving -> Doing Well ->
 * Consistency Impact. Wording softened throughout ("Stop X" -> "Reduce X" /
 * "Build in X") per the explicit instruction that tone stay constructive,
 * never harsh, while remaining direct about what to change.
 */
function rnGoalCoachingText(array $progress, array $ctx): array {
    $winRate = $ctx['win_rate'] ?? null;
    $streak = $ctx['streak'] ?? 0;
    $disciplined = $ctx['disciplined_count'] ?? 0;
    $tilt = $ctx['tilt_count'] ?? 0;
    $revenge = $ctx['revenge_count'] ?? 0;
    $highRiskLoss = $ctx['high_risk_loss_count'] ?? 0;
    $consistency = $ctx['consistency'] ?? null;
    $classifiedTotal = $ctx['classified_total'] ?? 0;

    if ($progress['achieved']) {
        return [
            'highest_priority' => "Nothing to flag: the goal is met. The only real risk now is loosening discipline because the pressure to reach a number is gone.",
            'keep_doing' => "Keep sizing and executing exactly as you have been. Reaching a goal is the moment discipline is most likely to slip, so don't let it.",
            'focus_improve' => "Nothing to improve on right now. If you set a new goal from here, treat it with the same discipline that got you this one.",
            'doing_well' => "You've reached this goal: the balance is at or above your \${$progress['target']} target. That's the outcome, not luck; whatever process got you here is worth writing down before it fades from memory.",
            'consistency_impact' => "This goal was reached without needing a personality change. Repeat the process, not the outcome.",
        ];
    }

    $badTrades = $tilt + $revenge + $highRiskLoss;
    $goodRatio = $classifiedTotal > 0 ? $disciplined / $classifiedTotal : null;

    $doingWell = match (true) {
        $streak > 1 => "You're on a {$streak}-trade winning streak right now: whatever setup and sizing produced that is exactly what's funding this goal.",
        $goodRatio !== null && $goodRatio >= 0.5 => "The majority of your classified trades ({$disciplined} of {$classifiedTotal}) are Disciplined Wins: you're executing your plan, not just getting lucky.",
        $winRate !== null && $winRate >= 50 => "Your win rate is holding at " . number_format($winRate, 1) . "%, and that alone is enough to reach this goal if size stays consistent.",
        default => "Every closed trade toward this goal, win or lose, is real data. Keep journaling every one; the pattern will become clear with more trades.",
    };

    // Item 6 — whichever branch actually fires above is the exact same
    // classification a "View N Trades" filter chip needs to point at, so
    // the flag key/count are captured here rather than re-derived from the
    // rendered text later (which would mean pattern-matching prose).
    $flagKey = match (true) {
        $revenge > 0 => 'revenge_trade', $tilt > 0 => 'tilt_warning', $highRiskLoss > 0 => 'loss_high_risk', default => null,
    };
    $flagCount = match ($flagKey) { 'revenge_trade' => $revenge, 'tilt_warning' => $tilt, 'loss_high_risk' => $highRiskLoss, default => 0 };

    $highestPriority = match (true) {
        $revenge > 0 => "Revenge trading has shown up {$revenge} time" . ($revenge === 1 ? '' : 's') . " on this account: sizing up specifically to win back a loss is the single behaviour most likely to push this goal further away, not closer.",
        $tilt > 0 => "{$tilt} trade" . ($tilt === 1 ? '' : 's') . " on this account carried a Tilt Warning: trading straight through a losing stretch without pausing tends to compound losses right when the goal needs protecting most.",
        $highRiskLoss > 0 => "{$highRiskLoss} loss" . ($highRiskLoss === 1 ? '' : 'es') . " exceeded the planned max loss on this account. Oversized losses erase progress faster than wins can rebuild it.",
        $consistency !== null && $consistency < 50 => "Consistency Score is sitting at " . round($consistency) . "/100. Uneven execution from trade to trade is the main thing limiting progress right now, more than the setups themselves.",
        default => "No clear constraint detected in your recent trading. Progress toward this goal is mostly a function of time and sample size from here.",
    };

    $keepDoing = match (true) {
        $disciplined > 0 => "Keep repeating whatever produced your {$disciplined} Disciplined Win" . ($disciplined === 1 ? '' : 's') . ": same setup criteria, same entry trigger, same risk, every time.",
        $winRate !== null && $winRate >= 40 => "Keep taking the trade when the full setup confirms, and pass when it doesn't. That discipline is what compounds into this goal.",
        default => "Keep journaling every trade with a classification: the data needed to coach this goal properly builds with every closed trade.",
    };

    $focusImprove = match (true) {
        $revenge > 0 => "Reduce sizing up after a loss. The single highest-leverage change for this goal is treating every trade at the same risk, regardless of what the last one did.",
        $tilt > 0 => "Build in a deliberate pause before taking the next trade after a loss. That's what breaks the tilt pattern this account has shown.",
        $highRiskLoss > 0 => "Avoid adjusting size or stop-loss mid-trade. Set both before entry and leave them. The losses that exceed plan are the ones eating into this goal.",
        default => "Nothing counterproductive detected in this account's recent trades. The main risk from here is complacency, not a specific bad habit.",
    };

    $winsNote = $progress['expected_wins_needed'] !== null
        ? "at your current average win, roughly {$progress['expected_wins_needed']} more winning trade" . ($progress['expected_wins_needed'] === 1 ? '' : 's') . " (assuming losses stay in line with plan) would close the remaining \$" . number_format($progress['remaining'], 2)
        : "the average win isn't established yet on this account; once a few more winners close, this goal will show a real expected-wins estimate";
    $consistencyText = $consistency !== null
        ? "Consistency Score is " . round($consistency) . "/100 right now: " . ($consistency >= 65
            ? "that level of consistency is a real asset toward this goal; {$winsNote}."
            : "raising that score (fewer tilt/revenge trades, sizing that matches the plan) is the fastest lever to reach this goal sooner; {$winsNote}.")
        : "Consistency Score isn't available yet on this account; {$winsNote}.";

    return [
        'highest_priority' => $highestPriority,
        'keep_doing' => $keepDoing,
        'focus_improve' => $focusImprove,
        'doing_well' => $doingWell,
        'consistency_impact' => $consistencyText,
        'flag_classification' => $flagKey, 'flag_count' => $flagCount,
        'keep_classification' => $disciplined > 0 ? 'disciplined_win' : null, 'keep_count' => $disciplined,
    ];
}

/** Item 7 — the "Why this risk?" footer sentence, generated from rnComputeRiskRecommendation()'s real reasons array (never a canned drawdown/no-drawdown binary) plus an explicit note whenever the recommendation is overriding a declared risk that would be unsafe right now. */
function rnRiskWhyText(array $riskRec): string {
    $reasonText = !empty($riskRec['reasons']) ? implode('; ', $riskRec['reasons']) : "no drawdown or behavioral risk factors are currently active on this account";
    $text = "Recommended risk is " . number_format($riskRec['safe_risk'], 2) . "% per trade right now because {$reasonText}.";
    if ($riskRec['overriding_declared']) {
        $text .= " Your strategy declares " . number_format($riskRec['declared'], 2) . "% per trade, which is higher than what current account preservation calls for, so this recommendation temporarily overrides it. This isn't a change to your declared strategy, just a smaller size until the account's condition improves.";
    }
    return $text;
}

/** Item 7 — the main "Coaching Recommendation" card text, driven by the real tier rnComputeRiskRecommendation() computed, not a healthy/drawdown binary. */
function rnRiskRecText(array $riskRec): string {
    return match ($riskRec['tier']) {
        'critical' => "This account is in a critical state right now. Trade at the smallest size that still lets you execute your edge, and treat NOT breaching as the priority over recovering ground quickly.",
        'serious'  => "This account is in serious drawdown. Cut size back meaningfully; a string of small, controlled wins rebuilds both balance and consistency, a single oversized trade rebuilds neither.",
        'moderate' => "This account is in a moderate drawdown. Trim risk back from here. Recovery is a process built from consistent execution, not one large trade.",
        default    => "This account is within a healthy range right now. Keep sizing disciplined and consistent with your plan.",
    };
}

/** Item 7 — last up-to-10 CLOSED trades (oldest→newest) from a phase/segment's own already-fetched rows, reduced to just the two fields rnComputeRiskRecommendation() needs. No new query — reuses whichever rows the caller already fetched for its own trade table. */
function rnRecentTradesForRisk(array $rows): array {
    $slice = array_slice($rows, -10);
    return array_map(fn($r) => ['classification' => $r['trade']['trade_classification'] ?? null, 'pnl' => $r['pnl'] ?? 0.0], $slice);
}

/**
 * Item 7 — Recommended Risk overhaul. The prior version recommended
 * whatever risk% the trader's strategy declared, halved when technically
 * "in drawdown" (any balance below account_size/peak) — so a strategy that
 * declared 10% still got recommended straight back, even on an account
 * already sitting at $4,950 of a $5,000 challenge, one bad trade from
 * breaching. That's the declared config repeated back, not coaching.
 *
 * Real account-preservation state now DRIVES the number; the declared
 * strategy risk is only ever a ceiling on top of it, never the primary
 * input:
 *   1. Tier from how much of the account's drawdown budget is already
 *      consumed ($ddUsedRatio: current drawdown / max allowed drawdown for
 *      prop, or current drawdown-from-peak / a 20%-of-equity informal
 *      ceiling for live accounts, which have no declared limit) — healthy
 *      (<25% used) recommends 1%, moderate (<50%) 0.5%, serious (<75%)
 *      0.25%, critical (>=75%, or genuinely near breach) 0.1%.
 *   2. That tier is cut further (never raised) by two independent
 *      behavioral red flags, each real and drawn from this account's own
 *      last up-to-10 CLOSED trades in the current phase/segment — a
 *      losing streak of 3+ (drawdown VELOCITY: still actively bleeding,
 *      not just historically down), and 2+ tilt/revenge/high-risk-loss
 *      classifications in that same recent window (behavioral
 *      deterioration). Each halves the tier's number again (floored at
 *      0.1%) rather than being ignored just because the drawdown tier
 *      alone looked survivable.
 *   3. The declared strategy risk is applied LAST, as a ceiling only —
 *      never used to push the number up past what account preservation
 *      calls for. When the declared value is higher than what's safe
 *      right now, the recommendation explicitly says it's overriding the
 *      declared number rather than silently substituting a smaller one.
 */
function rnComputeRiskRecommendation(string $accountId, string $userId, $db, array $riskCtx): array {
    $declaredRisk = null;
    try {
        // 2026-08-12 workflow-isolation audit: accounts are shared/universal,
        // but this "most recently attached strategy" lookup had no
        // is_validation filter at all — on an account with both real and
        // validation trades, whichever happened to be captured last could
        // surface a validation strategy's declared risk% as this coaching
        // tile's "declared" ceiling for a Main-workflow viewer, or vice
        // versa. Active Journeys renders in both workflows unconditionally,
        // so this must scope to whichever workflow is CURRENTLY active, not
        // whichever trade happened to be most recent overall.
        $rnRiskRecCond = validationScopeSql(ValidationService::getWorkflowState($userId, $db), 't');
        $svRow = $db->raw(
            "SELECT sv.max_risk_per_trade_pct FROM trades t
             JOIN strategy_versions sv ON sv.id = t.strategy_version_id
             WHERE t.account_id = :aid AND t.user_id = :uid AND t.strategy_version_id IS NOT NULL AND {$rnRiskRecCond}
             ORDER BY t.captured_at DESC LIMIT 1",
            [':aid' => $accountId, ':uid' => $userId]
        );
        $declaredRisk = !empty($svRow) ? (float)$svRow[0]['max_risk_per_trade_pct'] : null;
    } catch (\Throwable $e) { $declaredRisk = null; }

    $ddUsedRatio = max(0.0, (float)($riskCtx['dd_used_ratio'] ?? 0.0));
    $streak = (int)($riskCtx['streak'] ?? 0);
    $nearFailure = !empty($riskCtx['near_failure']);
    $recentTrades = $riskCtx['recent_trades'] ?? []; // up to last 10, oldest→newest, each ['classification'=>?, 'pnl'=>float]
    // §1.4 fix (2026-08-10) — default true so every OTHER existing caller
    // (this function has always had exactly one caller that can genuinely
    // fail to fetch real drawdown data — the prop-firm branch above, when
    // AnalyticsEngine::getChallengeMetrics() throws) keeps its exact prior
    // behavior; only that one caller now opts into disclosing the gap.
    $ddDataAvailable = (bool)($riskCtx['dd_data_available'] ?? true);

    $tier = match (true) {
        $nearFailure || $ddUsedRatio >= 0.75 => 'critical',
        $ddUsedRatio >= 0.50 => 'serious',
        $ddUsedRatio >= 0.25 => 'moderate',
        default => 'healthy',
    };
    $tierBase = match ($tier) { 'critical' => 0.10, 'serious' => 0.25, 'moderate' => 0.50, default => 1.00 };

    // Real, recent (last up-to-10 closed trades on this exact phase/segment)
    // behavioral signals — never a lifetime average, since a trader who's
    // cleaned up their act in the last month shouldn't be sized down for
    // mistakes made a year ago, and vice versa.
    $recentBadCount = 0;
    foreach (array_slice($recentTrades, -10) as $rt) {
        if (in_array($rt['classification'] ?? null, ['tilt_warning', 'revenge_trade', 'loss_high_risk'], true)) $recentBadCount++;
    }
    $streakFlag = $streak <= -3;
    $behaviorFlag = $recentBadCount >= 2;

    $multiplier = 1.0;
    $reasons = [];
    // §1.4 fix (2026-08-10) — the 'healthy' tier is the only one $ddUsedRatio
    // can silently reach with zero real data behind it (a missing/failed
    // getChallengeMetrics() call defaults the ratio to 0.0, the same value a
    // genuinely-confirmed 0%-drawdown account would produce) — every other
    // tier requires a real nonzero ratio to have been computed in the first
    // place, so they can't be reached by missing data alone.
    $reasons[] = match (true) {
        $tier === 'critical' && $nearFailure => "the account is close to its breach/failure boundary right now",
        $tier === 'critical' => "this account has already used " . round($ddUsedRatio * 100) . "% or more of its drawdown budget",
        $tier === 'serious'  => "this account has used roughly " . round($ddUsedRatio * 100) . "% of its drawdown budget, so recovery from here needs smaller, steadier trades",
        $tier === 'moderate' => "this account has used roughly " . round($ddUsedRatio * 100) . "% of its drawdown budget, a moderate, not yet critical, level of drawdown",
        !$ddDataAvailable    => "real drawdown data for this account isn't available right now, so this defaults to a standard, conservative risk level until it is",
        default              => "this account's drawdown is currently well within a healthy range",
    };
    if ($streakFlag) { $multiplier *= 0.5; $reasons[] = "it's currently on a " . abs($streak) . "-trade losing streak, which on its own calls for smaller size until it breaks"; }
    if ($behaviorFlag) { $multiplier *= 0.5; $reasons[] = "{$recentBadCount} of the last " . min(10, count($recentTrades)) . " closed trades were flagged Tilt/Revenge/Oversized-Loss, a real, recent behavioral warning sign"; }

    $safeRisk = round(max(0.10, $tierBase * $multiplier), 2);
    $overridingDeclared = $declaredRisk !== null && $declaredRisk > $safeRisk + 0.001;
    $recommendedRisk = $declaredRisk !== null ? min($declaredRisk, $safeRisk) : $safeRisk;

    return [
        'declared' => $declaredRisk, 'recommended' => $recommendedRisk, 'safe_risk' => $safeRisk,
        'tier' => $tier, 'dd_used_ratio' => $ddUsedRatio, 'streak_flag' => $streakFlag,
        'behavior_flag' => $behaviorFlag, 'recent_bad_count' => $recentBadCount,
        'overriding_declared' => $overridingDeclared, 'reasons' => $reasons,
        'in_drawdown' => $tier !== 'healthy',
        'dd_data_available' => $ddDataAvailable,
    ];
}

// ════════════════════════════════════════════════════════════════════
// PSYCHOLOGY MEETS STRATEGY (2026-07-29, mission-alignment pass, items
// 2-10) — TraderAlign's behavioural intelligence layer. Right Now exists to
// prepare the trader for the NEXT trade, not to re-explain the past (that's
// Reports/Analytics' job) — this is the one section built specifically to
// answer "what should I do differently before my next trade?" using the
// trader's own real evidence.
//
// ARCHITECTURAL BOUNDARY (item 8), enforced throughout this whole block:
// Calculator = intention, but ONLY its Pre-Trade Psychology fields
// (mood_before / energy_before / confidence_before, all 1-5, captured by
// calculator-source.html's mood/energy/confidence dots — 0/unset saves as
// NULL, confirmed by reading the calculator source directly) plus Strategy
// Checklist completion (checklist_results.ticked_pct, a genuinely separate
// pre-trade preparation confirmation, not a "calculator trade value").
// Everything else that describes what actually happened (trade_
// classification, the account's own already-established effective P&L
// cascade) comes from Drafts/MT5 — real outcomes, never
// projected_pnl/max_potential_profit/position size used as a stand-in for
// quality. This block never reads risk_pct, max_potential_loss/profit,
// lots_per_position, or any other purely-provisional calculator field.
//
// A genuine, disclosed data-model limitation: this schema has no record of
// a trade the user considered and DIDN'T take, so item 9's "chose not to
// trade" honesty scenario cannot be evaluated from real data — no such
// event exists to query. Handled by only ever rewarding honesty that's
// actually observable (a recorded mood/confidence that didn't match a
// negative outcome, real variance in what's recorded) rather than
// fabricating a scenario this schema can't represent.
// ════════════════════════════════════════════════════════════════════

/** Item 3 — psychology completion state for one trade: all three of mood/energy/confidence recorded, some, or none. */
function rnMoodCompletionState(?int $mood, ?int $energy, ?int $confidence): string {
    $filled = (int)($mood !== null) + (int)($energy !== null) + (int)($confidence !== null);
    if ($filled === 3) return 'complete';
    if ($filled === 0) return 'missing';
    return 'partial';
}

/** Item 4 — checklist completion state for one trade, from checklist_results.ticked_pct (a real 0-100 value, never null once a checklist row exists — a missing row is treated the same as skipped). */
function rnChecklistCompletionState($tickedPct): string {
    if ($tickedPct === null || $tickedPct === '') return 'skipped';
    $pct = (float)$tickedPct;
    if ($pct >= 100) return 'complete';
    if ($pct <= 0) return 'skipped';
    return 'partial';
}

/** Item 3 — mood/energy/confidence completion coaching. Never shame; always education. 'complete' isn't called (the main psychology narrative already covers it — "analyze normally" per the brief). */
function rnMoodCompletionCoach(string $state): string {
    return match ($state) {
        'partial' => "You only partially recorded your psychological state before this trade. Completing all three inputs (mood, energy, confidence) gives coaching a fuller picture of your decision-making.",
        'missing' => "You entered this trade without recording your mindset. Honest self-awareness before trading helps reveal behavioural patterns that are difficult to recognise afterwards.",
        default => '',
    };
}

/**
 * Item 4 — Strategy Checklist completion coaching, all three states,
 * educational not judgmental. Deliberately describes only what was
 * actually recorded (checklist confirmation in the app) — never implies
 * the trader's real strategy was or wasn't followed, since that isn't
 * something this schema records. See rnChecklistCompletionState()'s own
 * doc comment: a missing checklist row means no confirmation was
 * recorded, not that the trader skipped their own process.
 */
function rnChecklistCompletionCoach(string $state): string {
    return match ($state) {
        'complete' => "You confirmed every item on your pre-trade checklist before this trade.",
        'partial' => "Part of your pre-trade checklist was confirmed, but some items were left unchecked. Partial preparation often becomes the weakest link in otherwise good trades.",
        'skipped' => "No pre-trade checklist confirmation was recorded for this trade. Recording it next time gives you a clearer read on your own preparation.",
        default => '',
    };
}

/** Money helper for narrative sentences (distinct from the table-convention pnlFormat(), which omits the $ since its own column header already says "USD"). */
function rnMoneySigned(float $v): string {
    return ($v >= 0 ? '+$' : '-$') . number_format(abs($v), 2);
}

/**
 * Item 8 (refinement pass) — a real, visual Declared-Psychology-vs-
 * Actual-Execution comparison for one trade, since the flow strip's plain
 * "Not Recorded / Skipped / Disciplined / +$172.80" labels read as generic
 * status text rather than an actual comparison. Declared Psychology is the
 * average of whichever of mood/energy/confidence (1-5 each) were recorded,
 * rescaled to 0-100%; Actual Execution Quality is a 0-100% mapping of the
 * real sp_classify_trade outcome (disciplined_win highest, revenge_trade
 * lowest). Both are real, already-available fields — no new computation
 * beyond the rescale/mapping itself.
 */
function rnPsychExecutionGauge(array $t): array {
    $vals = array_values(array_filter([$t['mood_before'], $t['energy_before'], $t['confidence_before']], fn($v) => $v !== null));
    $psychPct = !empty($vals) ? (array_sum(array_map('intval', $vals)) / count($vals) - 1) / 4 * 100 : null;
    $execMap = ['disciplined_win' => 100, 'controlled_loss' => 85, 'elevated_win' => 70, 'breakeven' => 60, 'loss_high_risk' => 25, 'tilt_warning' => 15, 'revenge_trade' => 0];
    $classification = $t['trade_classification'] ?? null;
    $execPct = $classification !== null && isset($execMap[$classification]) ? (float)$execMap[$classification] : null;

    if ($psychPct === null) {
        $gapText = "Mood, energy, and confidence weren't recorded before this trade, so there's nothing yet to compare execution quality against. Recording all three next time unlocks this comparison.";
    } elseif ($execPct === null) {
        $gapText = "This trade hasn't been classified, so there's nothing yet to compare your reported psychology against.";
    } else {
        $gap = $execPct - $psychPct;
        if ($gap <= -25) {
            $gapText = "You reported strong psychology going into this trade, but actual execution quality came in well below that, a real gap worth examining before your next trade, not just a bad outcome.";
        } elseif ($gap >= 25) {
            $gapText = "Actual execution quality came in well above what you reported going in. Your process carried this trade further than your own confidence gave it credit for.";
        } else {
            $gapText = "Your reported psychology and your actual execution quality were roughly aligned on this trade. Your self-assessment is tracking reality.";
        }
    }
    return ['psych_pct' => $psychPct, 'exec_pct' => $execPct, 'gap_text' => $gapText];
}

/**
 * Item 5 — "Psychology Meets Strategy": Psychology -> Strategy -> Execution
 * -> Result -> Lesson, for one trade (the most recent closed trade on this
 * account). Confidence bucketed low/moderate/high; checklist state and
 * classification read directly from real data — every branch below maps to
 * one of the brief's own worked examples, with a still-personalized,
 * real-numbers fallback for every other combination.
 */
function rnPsychMeetsStrategyText(array $t, string $moodState, string $checklistState): string {
    $confidence = $t['confidence_before'] !== null ? (int)$t['confidence_before'] : null;
    $classification = $t['trade_classification'] ?? null;
    $pnl = (float)($t['effective_pnl'] ?? 0);
    $isWin = $pnl > 0.005;
    $isTiltish = in_array($classification, ['tilt_warning', 'revenge_trade'], true);
    $isDisciplined = in_array($classification, ['disciplined_win', 'controlled_loss'], true);
    $pnlText = rnMoneySigned($pnl);
    $confLabel = $confidence === null ? null : ($confidence <= 2 ? 'low' : ($confidence === 3 ? 'moderate' : 'high'));

    if ($isTiltish && ($moodState === 'missing' || $checklistState === 'skipped')) {
        $gap = ($moodState === 'missing' && $checklistState === 'skipped')
            ? 'psychological preparation and without checklist confirmation'
            : ($moodState === 'missing' ? 'psychological preparation' : 'checklist confirmation');
        $flag = $classification === 'revenge_trade' ? 'a Revenge Trade' : 'a Tilt Warning';
        return "This trade began without {$gap}. It closed {$pnlText} and was flagged as {$flag}. Your own trading history shows these situations consistently reduce execution quality.";
    }
    if ($confLabel === 'high' && $checklistState === 'skipped') {
        return "Your confidence was high before this trade, but your preparation checklist was skipped. It closed {$pnlText}. Confidence should strengthen preparation, not replace it.";
    }
    if ($confLabel === 'low' && $checklistState === 'complete' && $isDisciplined) {
        return "Although your confidence was low before entry, your process remained disciplined: every checklist item was confirmed. The trade closed {$pnlText}. Your strategy proved stronger than your emotions today.";
    }
    if ($confLabel === 'high' && $checklistState === 'complete' && $isDisciplined) {
        return "Your mindset and your preparation were aligned: high confidence, a fully-confirmed checklist, and a disciplined result ({$pnlText}). These are the conditions under which your trading consistently performs best.";
    }
    if ($confLabel === 'low' && !$isWin) {
        return "Your confidence was low before this trade, and it closed {$pnlText}. Whether that low confidence was accurate or the process just needed more trust, it's worth revisiting what specifically felt uncertain before entry.";
    }

    $confPart = $confLabel === null ? "Confidence wasn't recorded before this trade" : "Confidence was {$confLabel} before this trade";
    $checklistPart = match ($checklistState) {
        'complete' => 'your checklist was fully confirmed',
        'partial'  => 'only part of your checklist was confirmed',
        default    => 'your checklist was skipped',
    };
    $outcomePart = $isWin ? "it closed a win of {$pnlText}" : "it closed a loss of {$pnlText}";
    return "{$confPart}, and {$checklistPart}; {$outcomePart}.";
}

/**
 * Item 6 — psychology compared against this account's OWN historical
 * behaviour (never a generic benchmark). Every comparison is gated behind
 * a real minimum sample size on both sides of the split (n>=3 each), the
 * same n-aware discipline already established elsewhere in this app
 * (EdgeHealthEngine's tiers, Reports' small-sample gating) — below that,
 * the comparison is silently omitted rather than drawn from too few trades
 * to mean anything.
 */
function rnPsychHistoricalComparison(array $trades): array {
    $lines = [];
    $withEnergy = array_values(array_filter($trades, fn($t) => $t['energy_before'] !== null));
    $withConfidence = array_values(array_filter($trades, fn($t) => $t['confidence_before'] !== null));
    $allPnls = array_column($trades, 'effective_pnl');
    $overallAvg = count($allPnls) > 0 ? array_sum($allPnls) / count($allPnls) : null;

    if (count($withEnergy) >= 6) {
        $lowEnergy = array_values(array_filter($withEnergy, fn($t) => (int)$t['energy_before'] < 3));
        $otherEnergy = array_values(array_filter($withEnergy, fn($t) => (int)$t['energy_before'] >= 3));
        if (count($lowEnergy) >= 3 && count($otherEnergy) >= 3) {
            $lowAvg = array_sum(array_column($lowEnergy, 'effective_pnl')) / count($lowEnergy);
            $otherAvg = array_sum(array_column($otherEnergy, 'effective_pnl')) / count($otherEnergy);
            if ($lowAvg < $otherAvg - 0.01) {
                $lines[] = "Trades entered with Energy below 3 have historically averaged " . rnMoneySigned($lowAvg) . " per trade, versus " . rnMoneySigned($otherAvg) . " when energy was 3 or higher (" . count($lowEnergy) . " vs " . count($otherEnergy) . " trades). Low energy is a real, measurable drag on this account's results.";
            }
        }
    }

    if (count($withConfidence) >= 6) {
        $highConf = array_values(array_filter($withConfidence, fn($t) => (int)$t['confidence_before'] >= 4));
        if (count($highConf) >= 3 && $overallAvg !== null) {
            $highAvg = array_sum(array_column($highConf, 'effective_pnl')) / count($highConf);
            if ($highAvg < $overallAvg - 0.01) {
                $lines[] = "Your highest-confidence trades (" . count($highConf) . ") currently average " . rnMoneySigned($highAvg) . " per trade, underperforming your overall average of " . rnMoneySigned($overallAvg) . ". Confidence alone isn't predicting quality on this account yet.";
            }
        }
        $disciplined = array_values(array_filter($withConfidence, fn($t) => ($t['trade_classification'] ?? null) === 'disciplined_win'));
        if (count($disciplined) >= 3) {
            $counts = [];
            foreach ($disciplined as $t) { $c = (int)$t['confidence_before']; $counts[$c] = ($counts[$c] ?? 0) + 1; }
            arsort($counts);
            $modeConf = (int)array_key_first($counts);
            if ($modeConf <= 3) {
                $lines[] = "Your disciplined winning trades most commonly began at a confidence level of {$modeConf}/5, not the maximum. Moderate confidence, not certainty, is what's actually behind your best execution.";
            }
        }
    }
    return $lines;
}

/** Item 7 — Strategy Checklist completion compared against this account's own real win rate. Gated at n>=3 on both sides (complete vs. not) — below that, returns null rather than drawing a conclusion from too thin a sample. */
function rnChecklistHistoricalComparison(array $trades): ?array {
    $withChecklist = array_values(array_filter($trades, fn($t) => $t['ticked_pct'] !== null && $t['ticked_pct'] !== ''));
    $complete = array_values(array_filter($withChecklist, fn($t) => (float)$t['ticked_pct'] >= 100));
    $incomplete = array_values(array_filter($withChecklist, fn($t) => (float)$t['ticked_pct'] < 100));
    if (count($complete) < 3 || count($incomplete) < 3) return null;
    $winRateOf = function (array $rows) {
        return count($rows) > 0 ? round(count(array_filter($rows, fn($t) => (float)$t['effective_pnl'] > 0)) / count($rows) * 100, 1) : 0.0;
    };
    $completeWr = $winRateOf($complete);
    $incompleteWr = $winRateOf($incomplete);
    $coach = $completeWr >= $incompleteWr
        ? "Your own trading history clearly shows higher performance whenever your checklist is fully completed ({$completeWr}% win rate over " . count($complete) . " trades) versus when it isn't ({$incompleteWr}% over " . count($incomplete) . " trades). Continue trusting the process you've already proven."
        : "Checklist completion hasn't yet translated into a clear win-rate edge on this account ({$completeWr}% completed vs. {$incompleteWr}% not). Keep completing it anyway; sample size is still building, and discipline compounds over more trades than this.";
    return ['complete_n' => count($complete), 'complete_wr' => $completeWr, 'incomplete_n' => count($incomplete), 'incomplete_wr' => $incompleteWr, 'coach' => $coach];
}

/**
 * Item 9 — reward honest self-awareness, never a perfect score. Detects the
 * one scenario this schema CAN actually evidence: a recorded mood/
 * confidence that stayed calm/positive even though the trade still turned
 * into a flagged outcome — proof the number wasn't just optimism, and proof
 * the trader is reporting honestly rather than only when it flatters them.
 * A second, independent signal (real variance in what's recorded, not a
 * flat run of 5s) is used when no mismatch exists. (This schema has no
 * record of a trade considered and not taken, so the brief's "chose not to
 * trade" scenario can't be evaluated here — nothing is fabricated for it.)
 */
function rnHonestyInsight(array $trades): ?string {
    $mismatches = array_values(array_filter($trades, function ($t) {
        $conf = $t['confidence_before'] !== null ? (int)$t['confidence_before'] : null;
        $mood = $t['mood_before'] !== null ? (int)$t['mood_before'] : null;
        $badOutcome = in_array($t['trade_classification'] ?? null, ['tilt_warning', 'revenge_trade', 'loss_high_risk'], true);
        return $badOutcome && (($conf !== null && $conf >= 4) || ($mood !== null && $mood >= 4));
    }));
    if (count($mismatches) > 0) {
        $n = count($mismatches);
        return "On {$n} occasion" . ($n === 1 ? '' : 's') . ", you recorded a calm mood or confident mindset before a trade that still turned into a flagged outcome. Recording the honest pre-trade number even when the trade went wrong afterward is exactly the self-awareness that makes this coaching useful. Perfect scores every time would tell us nothing.";
    }
    $recorded = array_values(array_filter($trades, fn($t) => $t['confidence_before'] !== null));
    if (count($recorded) >= 6) {
        $distinct = count(array_unique(array_column($recorded, 'confidence_before')));
        if ($distinct >= 3) {
            return "Your recorded confidence varies meaningfully from trade to trade rather than defaulting to the same number every time. That honesty is what lets this coaching find real patterns instead of noise.";
        }
    }
    return null;
}

/** Fetches this account's own closed trades with ONLY the fields this section is architecturally allowed to read (item 8) — Pre-Trade Psychology, Strategy Checklist completion, and the account's own already-established real-outcome P&L cascade. Never max_potential_profit/loss, risk_pct, or any other provisional calculator value. */
function rnFetchAccountPsychologyTrades(string $accountId, string $userId, $db, string $accountType): array {
    // validationScopeSql() computed locally, not read from an outer-scope
    // variable — PHP function bodies don't inherit the calling script's
    // top-level variables, only what's passed as a parameter. $userId/$db
    // are already parameters here, so this is self-contained and correct.
    $rnValidationCondT = validationScopeSql(ValidationService::getWorkflowState($userId, $db), 't');
    return $db->raw(
        "SELECT t.id, t.captured_at, t.closed_at, t.mood_before, t.energy_before, t.confidence_before,
                t.trade_classification,
                CASE WHEN :atype = 'prop_firm' THEN COALESCE(t.challenge_pnl, t.broker_pnl, t.projected_pnl, 0) ELSE COALESCE(t.broker_pnl, t.projected_pnl, 0) END AS effective_pnl,
                cr.ticked_pct
         FROM trades t
         LEFT JOIN checklist_results cr ON cr.trade_lifecycle_id = t.trade_lifecycle_id AND cr.user_id = t.user_id
         WHERE t.account_id = :aid AND t.user_id = :uid AND t.status = 'closed' AND t.deleted_at IS NULL
           AND {$rnValidationCondT}
         ORDER BY t.captured_at ASC",
        [':atype' => $accountType, ':aid' => $accountId, ':uid' => $userId]
    );
}

/**
 * Renders the "Psychology Meets Strategy" section (items 2-10) — Right
 * Now's behavioural intelligence layer. Built entirely from this account's
 * own real Pre-Trade Psychology / Strategy Checklist / outcome history;
 * renders nothing when the account has no closed trades to evidence yet
 * (an empty state here would just be noise ahead of the trader's first
 * trade, and the rest of Right Now already covers that case).
 */
function renderRnPsychologyStrategy(array $trades): void {
    if (empty($trades)) return;
    $last = end($trades);
    reset($trades);

    $moodState = rnMoodCompletionState(
        $last['mood_before'] !== null ? (int)$last['mood_before'] : null,
        $last['energy_before'] !== null ? (int)$last['energy_before'] : null,
        $last['confidence_before'] !== null ? (int)$last['confidence_before'] : null
    );
    $checklistState = rnChecklistCompletionState($last['ticked_pct'] ?? null);
    $lastText = rnPsychMeetsStrategyText($last, $moodState, $checklistState);
    $moodCoach = rnMoodCompletionCoach($moodState);
    $checklistCoach = rnChecklistCompletionCoach($checklistState);
    $historical = rnPsychHistoricalComparison($trades);
    $checklistHist = rnChecklistHistoricalComparison($trades);
    $honesty = rnHonestyInsight($trades);
    [$lastStateLabel, ] = rnStateInfo($last['trade_classification'] ?? null);
    $lastPnl = (float)($last['effective_pnl'] ?? 0);

    $psychStepVal = match ($moodState) { 'complete' => 'Recorded', 'partial' => 'Partially Recorded', default => 'Not Recorded' };
    // "Checklist" (not "Strategy") and "Not Recorded" (not "Skipped") —
    // 2026-08-18 correction: only checklist-confirmation status is
    // actually known from recorded data; whether the trader followed
    // their own strategy is not something this schema records, so the
    // flow strip must never claim "Strategy Skipped."
    $stratStepVal = match ($checklistState) { 'complete' => 'Confirmed', 'partial' => 'Partially Confirmed', default => 'Not Recorded' };
    $gauge = rnPsychExecutionGauge($last);
    ?>
    <div class="rn-psych">
        <div class="rn-psych-head">
            <div><span class="rn-psych-title">🧠 Psychology Meets Strategy</span></div>
            <span class="rn-psych-sub">Built from this account's own mindset, preparation, and outcome history. Never generic advice.</span>
        </div>

        <div class="rn-psych-flow">
            <div class="rn-psych-step"><div class="rn-psych-step-label">Psychology</div><div class="rn-psych-step-val"><?= htmlspecialchars($psychStepVal) ?></div></div>
            <div class="rn-psych-arrow">→</div>
            <div class="rn-psych-step"><div class="rn-psych-step-label">Checklist</div><div class="rn-psych-step-val"><?= htmlspecialchars($stratStepVal) ?></div></div>
            <div class="rn-psych-arrow">→</div>
            <div class="rn-psych-step"><div class="rn-psych-step-label">Execution</div><div class="rn-psych-step-val"><?= htmlspecialchars($lastStateLabel) ?></div></div>
            <div class="rn-psych-arrow">→</div>
            <div class="rn-psych-step"><div class="rn-psych-step-label">Result</div><div class="rn-psych-step-val <?= pnlClass($lastPnl) ?>"><?= rnMoneySigned($lastPnl) ?></div></div>
        </div>

        <?php // Item 8 — an actual visual comparison (declared psychology vs.
              // real execution quality), not just status labels, for this
              // same last trade the flow strip above and the lesson below
              // both describe. ?>
        <div class="rn-psych-gauge">
            <div class="rn-psych-gauge-row">
                <div class="rn-psych-gauge-label" title="Average of this trade's recorded Mood, Energy, and Confidence (1-5 each, self-reported at capture), rescaled to 0-100%.">Declared Psychology</div>
                <div class="rn-psych-gauge-track"><div class="rn-psych-gauge-fill rn-psych-gauge-fill-psych" style="width:<?= $gauge['psych_pct'] !== null ? round($gauge['psych_pct']) : 0 ?>%"></div></div>
                <div class="rn-psych-gauge-pct"><?= $gauge['psych_pct'] !== null ? round($gauge['psych_pct']) . '%' : 'Not recorded' ?></div>
            </div>
            <div class="rn-psych-gauge-row">
                <div class="rn-psych-gauge-label" title="This trade's real trade_classification outcome (Disciplined Win highest, Revenge Trade lowest), mapped to 0-100%.">Actual Execution Quality</div>
                <div class="rn-psych-gauge-track"><div class="rn-psych-gauge-fill rn-psych-gauge-fill-exec" style="width:<?= $gauge['exec_pct'] !== null ? round($gauge['exec_pct']) : 0 ?>%"></div></div>
                <div class="rn-psych-gauge-pct"><?= $gauge['exec_pct'] !== null ? round($gauge['exec_pct']) . '%' : 'Not classified' ?></div>
            </div>
            <p class="rn-psych-gauge-gap"><?= htmlspecialchars($gauge['gap_text']) ?></p>
        </div>

        <div class="rn-psych-lesson">
            <div class="rn-psych-lesson-label">Lesson from your last trade</div>
            <p><?= htmlspecialchars($lastText) ?></p>
        </div>

        <?php if ($moodState !== 'complete'): ?>
        <div class="rn-psych-note"><?= htmlspecialchars($moodCoach) ?></div>
        <?php endif; ?>
        <div class="rn-psych-note <?= $checklistState === 'complete' ? 'rn-psych-note-good' : '' ?>"><?= htmlspecialchars($checklistCoach) ?></div>

        <?php if (!empty($historical) || $checklistHist !== null || $honesty !== null): ?>
        <div class="rn-psych-evidence">
            <div class="rn-psych-evidence-title">What your own history shows</div>
            <ul>
                <?php foreach ($historical as $line): ?><li><?= htmlspecialchars($line) ?></li><?php endforeach; ?>
                <?php if ($checklistHist !== null): ?><li><?= htmlspecialchars($checklistHist['coach']) ?></li><?php endif; ?>
                <?php if ($honesty !== null): ?><li><?= htmlspecialchars($honesty) ?></li><?php endif; ?>
            </ul>
        </div>
        <?php endif; ?>
    </div>
    <?php
}

/** Distinct-tone phase-pass messaging (item 5) — never the same wording twice across Phase 1/2/3. */
function rnPhasePassedMessage(string $label): array {
    return match ($label) {
        'Phase 1' => ['title' => 'PHASE 1 PASSED', 'body' => "First milestone down. You've shown you can execute the plan under real evaluation pressure. That's exactly the habit to carry into Phase 2, not relax on. The accounts that get funded are the ones that keep treating every phase like the first."],
        'Phase 2' => ['title' => 'PHASE 2 PASSED - FUNDED', 'body' => "You've qualified for funding. From here the mindset has to shift: Phase 1 and 2 rewarded growing the account quickly, but a funded account rewards protecting it. Slow down, keep risk exactly where it's declared, and let consistency, not speed, do the work from here."],
        'Phase 3' => ['title' => 'PHASE 3 PASSED - FUNDED', 'body' => "Final evaluation stage cleared. The same shift applies now, more than ever: funding is earned by staying in the game, not by pushing harder. Protect this balance the way you'd protect real capital, because from here on, it is."],
        default => ['title' => strtoupper($label) . ' PASSED', 'body' => "Congratulations on passing {$label}. Carry the same discipline that got you here into the next stage."],
    };
}

/** Distinct-tone phase-breach messaging (item 5). */
function rnPhaseBreachedMessage(string $label): array {
    return match ($label) {
        'Phase 1' => ['title' => 'PHASE 1 BREACHED', 'body' => "This attempt breached its drawdown limit during the first evaluation stage. Before starting again, look honestly at what happened here. Most Phase 1 breaches trace back to oversized risk on a small number of trades, not a bad run of setups."],
        'Phase 2' => ['title' => 'PHASE 2 BREACHED', 'body' => "Phase 1 was already proven. This breach happened closer to funding, which is the harder one to take. Worth checking whether risk crept up as the target got closer; that's the most common pattern this late in an evaluation."],
        'Phase 3' => ['title' => 'PHASE 3 BREACHED', 'body' => "A breach this late in the evaluation usually isn't about skill. It's risk discipline slipping under pressure. Review the trades right before the breach closely before the next attempt."],
        default => ['title' => strtoupper($label) . ' BREACHED', 'body' => "This account breached its drawdown limit during {$label} and is no longer tradeable."],
    };
}

/** Distinct-tone funded-account breach message — the costliest place for this to happen, so the framing is deliberately different from an evaluation-phase breach. */
function rnFundedBreachedMessage(): array {
    return ['title' => 'FUNDED ACCOUNT BREACHED', 'body' => "This is the most costly stage for a breach to happen. Real capital and real payouts were on the line, not just a pass/fail evaluation. Before requesting a new account, review the trades leading up to this breach specifically for size creep: funded breaches are disproportionately caused by risk that grew after a string of wins, not after losses."];
}

/** All checklist_items in a given category (setup/entry/execution/risk), preserving declared position order. */
function rnChecklistCategoryItems(array $items, string $category): array {
    return array_values(array_filter($items, fn($i) => ($i['category'] ?? '') === $category));
}

/** Joins a strategy's own declared checklist item text into one readable clause — never a hardcoded description, always the trader's own words. */
function rnJoinChecklistText(array $items): string {
    $texts = [];
    foreach ($items as $i) {
        $t = trim((string)($i['polished_text'] ?: $i['raw_text']));
        $t = rtrim($t, '. ');
        if ($t !== '') $texts[] = $t;
    }
    $texts = array_values(array_unique($texts));
    if (empty($texts)) return '';
    if (count($texts) === 1) return $texts[0];
    $last = array_pop($texts);
    return implode(', ', $texts) . ' and ' . $last;
}

/** Item 2 — Setup coaching, generated from the strategy's own declared setup checklist items, never hardcoded generic prep advice. */
function rnCoachingSetupText(array $checklist): string {
    $items = rnChecklistCategoryItems($checklist, 'setup');
    $joined = rnJoinChecklistText($items);
    // §1.6 fix (2026-08-10) — the prior guard checked item COUNT
    // (empty($items)), not whether rnJoinChecklistText() actually produced
    // any usable text after trimming — a category with rows whose
    // raw_text/polished_text both trim to blank rendered "confirm: .
    // Don't move to entry..." (grammatically broken, confirms nothing).
    // A row with no real text is functionally identical to no row at all,
    // so both now fall to the same honest "nothing declared" message —
    // matching the pattern rnCoachingExecutionText() already used
    // correctly for its own $joined !== '' check.
    if (empty($items) || $joined === '') return "No setup criteria declared on this strategy yet. Add setup criteria in Strategy Builder to get real preparation coaching here.";
    return "Before you do anything else, confirm: {$joined}. Don't move to entry until every one of these is genuinely true. A setup that's \"close enough\" is the most common source of low-quality trades.";
}

/** Item 2 — Entry coaching, generated from the strategy's own declared entry triggers. */
function rnCoachingEntryText(array $checklist): string {
    $items = rnChecklistCategoryItems($checklist, 'entry');
    $joined = rnJoinChecklistText($items);
    // §1.6 fix (2026-08-10) — same blank-text guard as rnCoachingSetupText() above.
    if (empty($items) || $joined === '') return "No entry triggers declared on this strategy yet. Add entry triggers in Strategy Builder to get real execution coaching here.";
    return "Your declared entry trigger: {$joined}. Wait for this exact condition to be fully in place. Entering before it confirms is chasing, not executing your edge. Stay patient; the setup you already confirmed will still be there.";
}

/** Item 2 — Execution coaching. Branches on whether the strategy's own declared execution/entry text mentions pending vs. market execution — never assumed, always read from the strategy's own words. */
function rnCoachingExecutionText(array $checklist): string {
    $execItems = rnChecklistCategoryItems($checklist, 'execution');
    $entryItems = rnChecklistCategoryItems($checklist, 'entry');
    $blob = strtolower(implode(' ', array_map(fn($i) => (string)($i['polished_text'] ?: $i['raw_text']), array_merge($execItems, $entryItems))));
    $isPending = str_contains($blob, 'pending');
    $isMarket = str_contains($blob, 'market');
    $joined = rnJoinChecklistText($execItems);

    if (empty($execItems) && !$isPending && !$isMarket) {
        return "No execution steps declared on this strategy yet. Add execution steps in Strategy Builder to get real trade-placement coaching here.";
    }
    $base = $joined !== '' ? "Your declared execution steps: {$joined}. " : '';
    if ($isPending) {
        return $base . "This strategy executes with pending orders. Place the order at your confirmed level and let price come to you. Don't chase by moving the order closer, and don't cancel it early just because price is hesitating nearby.";
    }
    if ($isMarket) {
        return $base . "This strategy executes at market. Only pull the trigger once every setup and entry condition above is confirmed. A market order can't be un-sent, so the confirmation happens before the click, not after.";
    }
    return $base . "Execute exactly as declared above. Consistency in how you enter matters as much as when.";
}

/** Item 2 — Risk coaching. References BOTH the strategy's declared maximum AND the coaching engine's live recommendation, explaining any gap between them in plain terms. */
function rnCoachingRiskText(array $checklist, array $riskRec): string {
    $items = rnChecklistCategoryItems($checklist, 'risk');
    $declared = $riskRec['declared'];
    $recommended = $riskRec['recommended'];
    $recText = number_format($recommended, 2) . '%';
    if ($declared === null) {
        $txt = "No strategy risk limit is declared on this account's trades. The {$recText} figure below is based purely on this account's own current condition (drawdown used, recent streak, recent behavior), not a declared config value.";
    } elseif (abs($recommended - $declared) < 0.001) {
        $declaredText = number_format($declared, 2) . '%';
        $txt = "Your strategy allows up to {$declaredText} risk per trade, and this account's current condition supports trading at that full level. Nothing here is asking you to size down right now.";
    } else {
        $declaredText = number_format($declared, 2) . '%';
        $reason = !empty($riskRec['reasons']) ? implode('; ', $riskRec['reasons']) : "recent trading behaviour on this account suggests reducing exposure until consistency improves";
        $txt = "Although your strategy allows {$declaredText} risk per trade, current coaching recommends {$recText} on the next trade: {$reason}. Size to the recommended level, not the maximum allowed, until account condition improves and this figure rises back up on its own.";
    }
    // §1.6 fix (2026-08-10) — same blank-text guard as rnCoachingSetupText()/
    // rnCoachingEntryText() above: !empty($items) only checked row count,
    // not whether the joined text was actually usable — a risk-category row
    // with blank text appended "Your own declared risk rule: ." (a broken,
    // content-free trailing clause) to an otherwise-correct sentence.
    if (!empty($items)) {
        $joined = rnJoinChecklistText($items);
        if ($joined !== '') $txt .= " Your own declared risk rule: {$joined}.";
    }
    return $txt;
}

/** Item 4 — Challenge Program filter (Prop Firms only): 1 Step / 2 Step / 3 Step / Instant Funding. */
function renderRnChallengeFilter(string $rnType, string $active): void {
    if ($rnType !== 'prop') return;
    $options = ['all' => 'All Programs', 'one_step' => '1 Step', 'two_step' => '2 Step', 'three_step' => '3 Step', 'instant_funding' => 'Instant Funding'];
    ?>
    <div class="rn-challenge-filter">
        <?php foreach ($options as $val => $label): ?>
        <a href="<?= htmlspecialchars(rnUrl('prop', null, $val)) ?>" class="rn-challenge-tab <?= $active === $val ? 'active' : '' ?>"><?= htmlspecialchars($label) ?></a>
        <?php endforeach; ?>
    </div>
    <?php
}

/**
 * Real declared profit-target-%/max-drawdown-% for a given phase label,
 * read from challenge_accounts.rules (a static JSON blob covering every
 * phase's own rules, set once at account creation and never phase-
 * specific-ally overwritten) — same key-fallback order as
 * AnalyticsEngine::getChallengeMetrics(), reused rather than
 * reinvented, so a historical/passed phase's OWN real target can be
 * computed without needing that phase's current_balance snapshot (which
 * this schema only ever stores the CURRENT value of, not history).
 */
function rnPhaseTargets(array $rules, string $label): array {
    $key = match ($label) {
        'Phase 1' => 'phase_1', 'Phase 2' => 'phase_2', 'Phase 3' => 'phase_3',
        'Funded Account' => 'funded', default => 'phase_1',
    };
    // §1.2 fix (2026-08-10) — this cascade's fallback numbers (8/5/5,
    // 10% for max_dd) exist for a real reason (renderRnPhaseCard()'s
    // live progress-bar math needs SOME number for an in-progress
    // account rather than showing nothing — see this function's own
    // callers' doc comments), but were being handed to the UI with zero
    // way to tell a real, firm-declared value apart from a silent guess
    // — the exact bug class already fixed once elsewhere in this
    // codebase (TradingArchiveEngine::realDeclaredMaxDdPct()). Ported
    // here: same real-or-null candidate check, exposed as
    // '..._configured' booleans alongside the unchanged, still-always-
    // populated numeric values, so every consumer can keep using the
    // number for math while disclosing when it's a fallback, not fact.
    $realVal = static function (array $candidates): ?float {
        foreach ($candidates as $v) {
            if ($v !== null && $v !== '' && is_numeric($v)) return (float)$v;
        }
        return null;
    };
    if ($key === 'funded') {
        $profitDeclared = $realVal([$rules['funded_profit_target'] ?? null, $rules['profit_target_pct'] ?? null]);
        $ddDeclared = $realVal([$rules['funded_max_loss'] ?? null, $rules['max_dd_pct'] ?? null]);
        return [
            'profit_target_pct' => $profitDeclared ?? 0.0,
            'profit_target_configured' => $profitDeclared !== null,
            'max_dd_pct' => $ddDeclared ?? 10.0,
            'max_dd_configured' => $ddDeclared !== null,
        ];
    }
    $defaults = ['phase_1' => 8, 'phase_2' => 5, 'phase_3' => 5];
    $profitDeclared = $realVal([$rules["{$key}_profit_target"] ?? null, $rules["{$key}_target_pct"] ?? null, $rules['profit_target_pct'] ?? null, $rules['target_pct'] ?? null]);
    $ddDeclared = $realVal([$rules["{$key}_max_loss"] ?? null, $rules['max_dd_pct'] ?? null]);
    return [
        'profit_target_pct' => $profitDeclared ?? (float)$defaults[$key],
        'profit_target_configured' => $profitDeclared !== null,
        'max_dd_pct' => $ddDeclared ?? 10.0,
        'max_dd_configured' => $ddDeclared !== null,
    ];
}

/**
 * Journey segmentation (which real Phase/Funded bucket each of an
 * account's trades belongs to) now lives entirely in
 * src\JourneySegmentation.php (2026-08-06 extraction) — see that file's
 * own header for the full "why a class, not global functions" reasoning
 * (real PHP-enforced privacy: only JourneySegmentation::
 * partitionAccountJourney() is public, so a future change literally
 * cannot call the old uncorrected per-trade partitioning directly the
 * way a doc-comment-only convention could never guarantee). What used
 * to be three plain global functions here — rnTagTradePhase(),
 * rnPartitionTradesByPhase(), rnPartitionAccountJourney() — is gone from
 * this file entirely; rnBuildJourney() below now calls
 * JourneySegmentation::partitionAccountJourney() directly.
 */

/**
 * Attaches '_prior_loss_count' to every row (mutates $trades in place) —
 * the exact count of losses among the up-to-10 previous CLOSED trades on
 * this SAME account. Historical note (2026-08-18): this mirrored
 * sp_classify_trade's own v_consec_losses lookup and used to feed
 * rnClassificationTooltip()'s "why" explanation. sp_classify_trade has
 * since been retired — see src/TradeClassificationEngine.php — and
 * rnClassificationTooltip() now reads the real, persisted
 * trades.classification_reason JSON instead (computed once, at
 * classification time, by the actual tier logic — never re-derived here).
 * '_prior_loss_count' has no remaining consumer as of this rewrite; left
 * in place (additive-only, cheap, no DB query) rather than removed, in
 * case a future feature wants a plain "losses in the last 10" figure for
 * something unrelated to classification.
 */
function rnAttachPriorLossCounts(array &$trades): void {
    $n = count($trades);
    for ($i = 0; $i < $n; $i++) {
        $lookback = array_slice($trades, max(0, $i - 10), min(10, $i));
        $count = 0;
        foreach ($lookback as $prior) {
            if ($prior['effective_pnl'] !== null && (float)$prior['effective_pnl'] < 0) $count++;
        }
        $trades[$i]['_prior_loss_count'] = $count;
    }
}

/**
 * Real, per-trade explanation of a trade_classification value — reads
 * trades.classification_reason (persisted once, at classification time,
 * by src/TradeClassificationEngine.php — never re-derived here) instead
 * of re-deriving "why" from raw fields. Rewritten 2026-08-18 alongside
 * that engine's fix: classification now compares this trade's own
 * risk_pct against Active Coaching's tier-recommended risk % at the
 * moment it closed (not a planned-vs-actual $ check, not a raw "loss
 * somewhere in the last 10" streak count), so the explanation must match.
 * Falls back to a generic sentence for a trade classified before this
 * column existed (classification_reason NULL) rather than fabricating
 * numbers that were never recorded.
 */
function rnClassificationTooltip(?string $classification, float $pnl, array $t): string {
    $moneyPnl = ($pnl >= 0 ? '+$' : '-$') . number_format(abs($pnl), 2);
    $reason = !empty($t['classification_reason']) ? json_decode($t['classification_reason'], true) : null;

    $tierLabel = match ($reason['tier'] ?? null) {
        'tier1' => '1.00% (no drawdown)', 'tier2' => '0.50% (reduced sizing)',
        'tier3' => '0.25% (defensive sizing)', 'breached' => '0.00% (account breached)',
        default => null,
    };
    $actualRiskPct = $reason['actual_risk_pct'] ?? null;
    $recommendedPct = $reason['recommended_risk_pct'] ?? null;
    $precededBy = $reason['preceded_by'] ?? null;

    $riskLine = ($actualRiskPct !== null && $tierLabel !== null)
        ? "This trade risked {$actualRiskPct}% of the account, against a {$tierLabel} recommendation at the time. "
        : '';
    $precededLine = $precededBy === 'loss' ? 'It immediately followed a loss on this same phase. '
        : ($precededBy === 'win' ? 'It immediately followed a win on this same phase. ' : '');

    return match ($classification) {
        'disciplined_win' => "Closed {$moneyPnl}. {$riskLine}Risk stayed within your current drawdown tier, which is why it's marked Disciplined. Nothing to change here; this is the outcome to repeat.",
        'controlled_loss' => "Closed {$moneyPnl}. {$riskLine}Risk stayed within your current drawdown tier. A normal, well-managed loss: no behavior change needed, only continued discipline.",
        'breakeven' => "Closed within $0.01 of flat ({$moneyPnl}). Neither a real win nor a real loss, so it's excluded from win/loss classification.",
        'elevated_win' => "Closed {$moneyPnl}. {$riskLine}{$precededLine}A bigger win, but risked more than your current drawdown tier recommends. Worth checking it wasn't luck standing in for disciplined sizing.",
        'loss_high_risk' => "Closed {$moneyPnl}. {$riskLine}Risked more than your current drawdown tier recommends. To improve: size to the tier your Overall Loss Risk card is recommending right now, not what feels right in the moment.",
        'tilt_warning' => "Closed {$moneyPnl}. {$riskLine}{$precededLine}This is Fear of Trading: sized well below what your tier allows, right after a loss. Sizing down out of fear can protect one trade but slows real recovery. Improvement: after a loss, trade your normal tier size, not a fraction of it.",
        'revenge_trade' => "Closed {$moneyPnl}. {$riskLine}{$precededLine}This is Revenge Trading: risked more than your current tier recommends, right after a loss. Improvement: after any loss, step away or confirm you're still within your recommended tier before the next trade. This pattern is the single highest-value thing to eliminate.",
        default => "No classification recorded for this trade yet.",
    };
}

/** Terminal status for whichever bucket is the account's real current one — every earlier bucket is unconditionally 'passed' (a later bucket having trades proves it). */
function rnDeriveTerminalStatus(string $lifecycleState, string $label): string {
    if ($lifecycleState === 'breached') return 'breached';
    if ($lifecycleState === 'closed') return 'closed';
    if ($lifecycleState === 'funded_active' && $label === 'Funded Account') return 'funded_active';
    if (str_ends_with($lifecycleState, '_complete')) return 'passed'; // finished this stage, not yet promoted (e.g. FTMO 5K - Attempt 1)
    // Real bug fixed (2026-08-07) — an account can genuinely be promoted
    // (lifecycle_state already says phase_2_active/phase_3_active/
    // funded_active) while it has zero real trades yet in that new
    // phase, meaning the only real bucket JourneySegmentation ever built
    // is still labeled for an EARLIER phase. Falling straight through to
    // 'active' below then wrongly treated that earlier, already-finished
    // phase as if it were still the account's current one (confirmed
    // live: a freshly-promoted Phase 2 account, zero Phase 2 trades yet,
    // showed its real Phase 1 ending balance in the "Right Now" coaching
    // section instead of correctly showing nothing yet for a fresh phase
    // with no data). Comparing real phase rank catches this: if the
    // account's own live lifecycle_state is already ahead of this
    // bucket's label, the account has genuinely moved past it,
    // regardless of whether any trades exist yet for the new phase.
    $labelRank = match ($label) {
        'Phase 1' => 1, 'Phase 2' => 2, 'Phase 3' => 3, 'Funded Account' => 4, default => 0,
    };
    $stateRank = match (true) {
        $lifecycleState === 'funded_active' => 4,
        str_starts_with($lifecycleState, 'phase_3') => 3,
        str_starts_with($lifecycleState, 'phase_2') => 2,
        str_starts_with($lifecycleState, 'phase_1') => 1,
        default => 0,
    };
    if ($labelRank > 0 && $stateRank > $labelRank) return 'passed';
    return 'active';
}

/** Real "<phase> passed on" date, preferring this exact stage's own logged completion/promotion event over a trade-derived guess. */
function rnFindPassedOnDate($db, string $accountId, string $label): ?string {
    $eventTypes = match ($label) {
        'Phase 1' => ['PHASE_1_COMPLETED', 'PHASE_2_STARTED'],
        'Phase 2' => ['PHASE_2_COMPLETED', 'PHASE_3_STARTED', 'FUNDED_ACTIVATED'],
        'Phase 3' => ['FUNDED_ACTIVATED'],
        default => [],
    };
    if (empty($eventTypes)) return null;
    $placeholders = [];
    $params = [':aid' => $accountId];
    foreach ($eventTypes as $i => $et) { $placeholders[] = ":et{$i}"; $params[":et{$i}"] = $et; }
    $rows = $db->raw(
        "SELECT occurred_at FROM lifecycle_events WHERE account_id = :aid AND event_type IN (" . implode(',', $placeholders) . ") ORDER BY occurred_at ASC LIMIT 1",
        $params
    );
    return $rows[0]['occurred_at'] ?? null;
}

/** Real "<phase> started on" date, preferring this exact stage's own logged creation/promotion event over a trade-derived guess. */
function rnFindStartedOnDate($db, string $accountId, string $label): ?string {
    $eventTypes = match ($label) {
        'Phase 1' => ['ACCOUNT_CREATED'],
        'Phase 2' => ['PHASE_2_STARTED'],
        'Phase 3' => ['PHASE_3_STARTED'],
        'Funded Account' => ['FUNDED_ACTIVATED'],
        default => [],
    };
    if (empty($eventTypes)) return null;
    $placeholders = [];
    $params = [':aid' => $accountId];
    foreach ($eventTypes as $i => $et) { $placeholders[] = ":et{$i}"; $params[":et{$i}"] = $et; }
    $rows = $db->raw(
        "SELECT occurred_at FROM lifecycle_events WHERE account_id = :aid AND event_type IN (" . implode(',', $placeholders) . ") ORDER BY occurred_at ASC LIMIT 1",
        $params
    );
    return $rows[0]['occurred_at'] ?? null;
}

/**
 * Builds one phase/segment card's full data set from an already-
 * partitioned list of raw trade rows (running balance reconstructed
 * from account_size — Phase promotions reset
 * challenge_accounts.current_balance to account_size, confirmed in
 * api/accounts/create.php's 'promote' mode, so each phase's own balance
 * column genuinely restarts from account_size rather than continuing
 * the previous phase's ending balance), and this phase's own real
 * progress-to-pass numbers (profit_target_pct read from the account's
 * static rules JSON via rnPhaseTargets() — correct for a historical/
 * passed phase too, not just the live current one).
 */

/**
 * The ONE place Start Balance / Profit Target / Target Balance /
 * Maximum Drawdown / Breach Level (Minimum Equity) are derived together
 * — 2026-08-06, a real, reported bug: renderRnPhaseCard()'s own stats
 * line computed the real $4,600 breach FLOOR correctly (account_size −
 * max_drawdown_amount) but labeled it "Max Drawdown," which on that same
 * $5,000/8% account is actually $400. Those are two different values —
 * a dollar DELTA (how much can be lost before breaching) versus a
 * BALANCE LEVEL (the equity floor that delta implies) — and must never
 * be shown under the same label again. This function makes that
 * structurally impossible: every caller gets both, always correctly
 * separated and named.
 *
 * Deliberately takes already-resolved percentages, not raw rules — the
 * two real callers resolve "what percentage applies" differently
 * on purpose (renderRnPhaseCard() reuses rnPhaseTargets()'s existing
 * live-progress-bar behavior, which fills in a sane default percentage
 * for an in-progress account rather than showing nothing; Lessons From
 * Failure's breach view uses realDeclaredMaxDdPct()-style real-or-null
 * values, since a finished account's own record should never imply a
 * rule it didn't actually have) — this function only owns the one thing
 * both should share: turning a resolved percentage into the correct,
 * correctly-labeled dollar amounts, once.
 */
/**
 * Real bug found and fixed (2026-08-06, third refinement pass) — this
 * function's own Profit Target math previously required
 * `$profitTargetPct > 0` before treating the value as real, which
 * silently collapsed a genuinely RETRIEVED, genuinely DECLARED 0% target
 * (confirmed live: one real account's own challenge_accounts.rules JSON
 * literally stores `"phase_1_target_pct": 0`) into the exact same
 * "Not specified" state as a target that was never configured at all —
 * two completely different situations (real-zero vs. truly-absent)
 * reported identically. This was never a retrieval/mapping bug — the
 * value WAS being fetched correctly (confirmed via rnPhaseTargets()'s
 * own cascade, which never returns 0 for a genuinely missing key — its
 * phase_1/2/3 branches all fall back to a hardcoded 8/5/5 default, so a
 * resolved 0 can only mean the key was explicitly present and set to
 * 0) — it was a presentation bug that made real, retrieved data look
 * like missing data. Fixed by keeping a real 0 as a real 0 all the way
 * through (never silently promoted to null) and exposing a distinct
 * `profit_target_zero` flag so the render layer (rnProgramConfigCards()
 * below) can show an honest "No Profit Target Required" state instead
 * of either a misleading "$0.00 (0%)" or an equally misleading "Not
 * specified." `profit_target_pct === null` still means, and only ever
 * means, "this account's rules genuinely carry no usable value here."
 */
// §1.2 fix (2026-08-10) — two new optional trailing flags, defaulted
// `true` so every existing caller (the breach-terminal-view caller
// below already passes real-or-null values, and both live callers now
// thread rnPhaseTargets()'s own '..._configured' booleans through) keeps
// its exact prior behavior unless it opts into disclosing an estimate.
// When false, the percentage/dollar figure is still shown (the live
// progress-bar math genuinely needs a number) but flagged as an
// estimate rather than presented with the same confidence as a real,
// firm-declared rule — the exact gap CLAUDE.md documents this codebase
// already closing once for TradingArchiveEngine::realDeclaredMaxDdPct(),
// ported here.
// Maximum Loss Model (2026-08-17) — $ddReferenceBalance is the balance the
// Maximum Drawdown / Breach Level pair is actually anchored to: the same
// $accountSize for Fixed (unchanged, and the default when a caller omits
// this new optional param — every pre-existing call site keeps its exact
// prior behavior), but the account's own real PEAK balance for Trailing.
// Profit Target / Target Balance are deliberately NOT affected — those
// stay anchored to $accountSize regardless of model, matching
// PropFirmRuleEngine::compute()'s own profit-target formula, which is
// never model-dependent.
function rnProgramConfigSummary(float $accountSize, ?float $profitTargetPct, ?float $maxDdPct, bool $profitTargetConfigured = true, bool $maxDdConfigured = true, ?float $ddReferenceBalance = null): array {
    $ddRef = $ddReferenceBalance ?? $accountSize;
    $hasProfitTarget = $profitTargetPct !== null;
    $profitTargetDollar = $hasProfitTarget ? $accountSize * $profitTargetPct / 100 : null;
    $maxDrawdownDollar = ($maxDdPct !== null && $maxDdPct > 0) ? $ddRef * $maxDdPct / 100 : null;
    return [
        'start_balance' => $accountSize,
        'profit_target_pct' => $profitTargetPct,
        'profit_target_dollar' => $profitTargetDollar,
        'profit_target_zero' => $hasProfitTarget && $profitTargetPct <= 0,
        'profit_target_estimated' => $hasProfitTarget && !$profitTargetConfigured,
        'target_balance' => $profitTargetDollar !== null ? $accountSize + $profitTargetDollar : null,
        'max_dd_pct' => $maxDrawdownDollar !== null ? $maxDdPct : null,
        'max_drawdown_dollar' => $maxDrawdownDollar,
        'max_dd_estimated' => $maxDrawdownDollar !== null && !$maxDdConfigured,
        'breach_level' => $maxDrawdownDollar !== null ? $ddRef - $maxDrawdownDollar : null,
    ];
}

/** Maximum Loss Model (2026-08-17) — resolves the real $ddReferenceBalance to pass into rnProgramConfigSummary() from a phase/segment's own already-available rules + row-balance history: $accountSize for Fixed (or when there's nothing to walk), the highest balance point in $rows for Trailing (this segment/phase's own real high-water mark). Shared by every rnProgramConfigSummary() call site in this file so they can't independently disagree on how a Trailing account's peak is resolved. */
function rnDdReferenceBalance(array $rules, float $accountSize, array $rows): float {
    $model = strtolower(trim((string)($rules['max_loss_model'] ?? 'fixed')));
    if ($model !== 'trailing' || empty($rows)) return $accountSize;
    $balances = array_column($rows, 'balance');
    return $balances ? max($accountSize, max($balances)) : $accountSize;
}

/** Renders rnProgramConfigSummary()'s five values as one consistent, unambiguous stats-line fragment (no wrapping <div> — callers embed this inside their own .rn-phase-stats-line and append any further context, e.g. "Breached On"). Superseded by rnProgramConfigCards() below as of the 2026-08-06 refinement pass (the correct facts were already there, only their presentation — one long run-on line of text — needed to change) — kept in place, unused, per the additive-only rule rather than deleted. */
function rnProgramConfigLineHtml(array $cfg): string {
    $fmt = fn($v) => $v !== null ? '$' . number_format($v, 2) : 'Not specified';
    $pct = fn($v) => $v !== null ? ' (' . rtrim(rtrim(number_format($v, 2), '0'), '.') . '%)' : '';
    $html = 'Start Balance: ' . $fmt($cfg['start_balance']);
    $html .= ' &bull; Profit Target: ' . $fmt($cfg['profit_target_dollar']) . $pct($cfg['profit_target_pct']);
    $html .= ' &bull; Target Balance: ' . $fmt($cfg['target_balance']);
    $html .= ' &bull; Maximum Drawdown: ' . $fmt($cfg['max_drawdown_dollar']) . $pct($cfg['max_dd_pct']);
    $html .= ' &bull; Breach Level (Minimum Equity): ' . $fmt($cfg['breach_level']);
    return $html;
}

/**
 * Item 1 (2026-08-06, refinement pass) — the real, correctly-labeled facts
 * rnProgramConfigSummary() computes were being thrown onto the page as one
 * long run-on line of text ("Start Balance: $5,000.00 • Profit Target:
 * ..."), which read as an afterthought next to the rest of this page's
 * card-based design language. This renders the exact same values as a
 * small grid of scannable stat chips instead — same data, same "never
 * fabricate, never mislabel" rules rnProgramConfigSummary() already
 * enforces, only the presentation changed. $extra appends further
 * context chips after the five core ones (Final Balance, Passed On,
 * Breached On, Funded On, etc.) — callers no longer hand-concatenate
 * their own trailing string onto a shared line.
 */
function rnProgramConfigCards(array $cfg, array $extra = []): string {
    $fmt = fn($v) => $v !== null ? '$' . number_format($v, 2) : 'Not specified';
    $pctSuffix = fn($v) => $v !== null ? rtrim(rtrim(number_format($v, 2), '0'), '.') . '%' : null;
    $profitTargetZero = !empty($cfg['profit_target_zero']);
    // §1.2 fix (2026-08-10) — an estimated (fallback-default) figure is
    // still shown with real confidence in its VALUE (the progress math
    // needs it), but its sub-label now says so plainly instead of
    // reading identically to a real, firm-declared rule.
    $profitEstimated = !empty($cfg['profit_target_estimated']);
    $ddEstimated = !empty($cfg['max_dd_estimated']);
    $profitSub = $profitEstimated ? 'Estimated, not declared by this firm' : $pctSuffix($cfg['profit_target_pct']);
    $ddSub = $ddEstimated ? 'Estimated, not declared by this firm' : $pctSuffix($cfg['max_dd_pct']);

    // §1.3 fix (2026-08-10) — the zero-target chip previously always said
    // "Declared as 0% for this phase" whenever profit_target_pct resolved
    // to <=0, with no check against $profitEstimated — indistinguishable
    // from a REAL declared-zero rule (hasProfitTarget=true, configured=
    // true) even when this 0 was actually rnPhaseTargets()'s own
    // never-configured fallback for Funded (which defaults to 0.0, not
    // 8/5/5 like Phase 1-3 — see that function's own comment). Both now
    // get their own honest sub-label.
    $chips = [
        ['label' => 'Start Balance', 'value' => $fmt($cfg['start_balance']), 'sub' => null, 'tone' => null],
        $profitTargetZero
            ? ['label' => 'Profit Target', 'value' => 'No Target Required', 'sub' => $profitEstimated ? 'Not declared by this firm, estimated as 0%' : 'Declared as 0% for this phase', 'tone' => null]
            : ['label' => 'Profit Target', 'value' => $fmt($cfg['profit_target_dollar']), 'sub' => $profitSub, 'tone' => null],
        $profitTargetZero
            ? ['label' => 'Target Balance', 'value' => $fmt($cfg['target_balance']), 'sub' => $profitEstimated ? 'Same as Start Balance, based on an estimated target' : 'Same as Start Balance', 'tone' => 'positive']
            : ['label' => 'Target Balance', 'value' => $fmt($cfg['target_balance']), 'sub' => $profitEstimated ? 'Based on an estimated target' : null, 'tone' => 'positive'],
        ['label' => 'Maximum Drawdown', 'value' => $fmt($cfg['max_drawdown_dollar']), 'sub' => $ddSub, 'tone' => null],
        ['label' => 'Breach Level', 'value' => $fmt($cfg['breach_level']), 'sub' => $ddEstimated ? 'Minimum Equity, based on an estimated limit' : 'Minimum Equity', 'tone' => 'negative'],
    ];
    foreach ($extra as $e) { $chips[] = $e; }

    $html = '<div class="rn-config-grid">';
    foreach ($chips as $c) {
        $tone = $c['tone'] ?? null;
        $toneClass = $tone === 'positive' ? ' rn-config-chip-positive' : ($tone === 'negative' ? ' rn-config-chip-negative' : '');
        $html .= '<div class="rn-config-chip' . $toneClass . '">';
        $html .= '<div class="rn-config-chip-label">' . htmlspecialchars($c['label']) . '</div>';
        $html .= '<div class="rn-config-chip-value">' . htmlspecialchars($c['value']) . '</div>';
        if (!empty($c['sub'])) $html .= '<div class="rn-config-chip-sub">' . htmlspecialchars($c['sub']) . '</div>';
        $html .= '</div>';
    }
    $html .= '</div>';
    return $html;
}
/**
 * Each phase/segment's own trade table walks its "Balance" column fresh
 * from $accountSize — deliberate, and confirmed correct (2026-08-07):
 * real prop-firm evaluations genuinely reset per phase promotion (a
 * fresh Phase 2 starts at the full account size, ignoring Phase 1's
 * profit — verified live via challenge_accounts.current_balance's own
 * updated_at exactly matching a real promotion timestamp, snapping back
 * to account_size). An earlier pass on this same day briefly changed
 * this to a continuous, never-resetting whole-account walk (based on the
 * wrong assumption that trading_accounts.current_balance — which
 * genuinely never resets — was the correct model here) and was reverted
 * after live testing showed it broke real trade-sizing math one level
 * up: a freshly-promoted Phase 2 account showed its PRIOR phase's ending
 * balance instead of a fresh start. $startingBalance stays available
 * (defaults to $accountSize, i.e. every caller today) in case a future,
 * deliberately-scoped feature needs to override it — it is NOT currently
 * threaded across phases by any caller.
 */
function rnBuildPhaseCardFromRows(string $accountId, array $account, string $label, string $status, array $tradeRows, array $rules, ?float $startingBalance = null): array {
    $accountSize = (float)($account['account_size'] ?? $account['starting_balance'] ?? 0);
    $runningBalance = $startingBalance ?? $accountSize;
    $rows = [];
    foreach ($tradeRows as $t) {
        $pnl = $t['effective_pnl'] !== null ? (float)$t['effective_pnl'] : 0.0;
        $runningBalance += $pnl;
        $rows[] = ['trade' => $t, 'pnl' => $pnl, 'balance' => $runningBalance];
    }

    $winners = array_values(array_filter($rows, fn($r) => $r['pnl'] > 0));
    $winnerPnls = array_column($winners, 'pnl');
    $avgWin = count($winnerPnls) > 0 ? array_sum($winnerPnls) / count($winnerPnls) : null;
    $bestTrade = count($winnerPnls) > 0 ? max($winnerPnls) : null;
    $totalProfit = array_sum(array_column($rows, 'pnl'));
    $winRate = count($rows) > 0 ? round(count($winners) / count($rows) * 100, 2) : 0.0;

    // Current streak, counted backward from the end of this segment's own trades.
    $streak = 0;
    for ($i = count($rows) - 1; $i >= 0; $i--) {
        $p = $rows[$i]['pnl'];
        $sign = $p > 0 ? 1 : ($p < 0 ? -1 : 0);
        if ($i === count($rows) - 1) { $streak = $sign; continue; }
        if ($sign !== 0 && $sign === ($streak <=> 0)) $streak += $sign; else break;
    }

    $isCurrent = in_array($status, ['active', 'funded_active'], true);
    $targets = rnPhaseTargets($rules, $label);
    $profitNeeded = $accountSize * ($targets['profit_target_pct'] / 100);
    $remaining = max(0.0, $profitNeeded - $totalProfit);
    $progressPct = $profitNeeded > 0 ? max(0.0, ($totalProfit / $profitNeeded) * 100) : 100.0;
    $targetBalance = $accountSize + $profitNeeded;
    $maxDrawdownAmount = $accountSize * ($targets['max_dd_pct'] / 100);

    // Live-only drawdown-room numbers (genuinely "right now" state, not
    // reconstructible historically) — fetched only for whichever segment
    // is the account's real current one; feeds the Right Now coaching
    // section, not this card's own header stats.
    $challengeMetrics = null;
    if ($isCurrent) {
        try { $challengeMetrics = AnalyticsEngine::getChallengeMetrics($accountId); } catch (\Throwable $e) { $challengeMetrics = null; }
    }

    // §1.5 fix (2026-08-10) — a breached/closed attempt is just as much an
    // ENDED attempt as a passed one for the purpose of this metric; only
    // 'passed' was excluded before, so a breached/closed phase fell through
    // to the same "still working toward it" branch as a genuinely active
    // one, computing and rendering a real wins-needed estimate for an
    // attempt that's already over. Currently unreachable via the picker
    // (rnFetchActivePropAccounts() excludes breached/closed accounts) but
    // fixed at the source rather than left latent for whenever that filter
    // is ever loosened.
    $expectedWinsNeeded = in_array($status, ['passed', 'breached', 'closed'], true) ? null
        : (($avgWin !== null && $avgWin > 0 && $remaining > 0) ? (int)ceil($remaining / $avgWin) : ($remaining <= 0 ? 0 : null));

    // Trade-derived date range — a real fallback for display/scoring
    // purposes when no lifecycle event exists to anchor this phase's own
    // start; rnBuildJourney() overrides 'start_date' with a real logged
    // event date when one is available (more authoritative than "first
    // trade tagged this phase").
    $capturedAts = array_filter(array_column($tradeRows, 'captured_at'));
    $closedAts = array_filter(array_column($tradeRows, 'closed_at'));
    $startDate = !empty($capturedAts) ? min($capturedAts) : (string)($account['created_at'] ?? date('Y-m-d H:i:s'));
    $endDate = $isCurrent ? date('Y-m-d H:i:s') : (!empty($closedAts) ? max($closedAts) : (!empty($capturedAts) ? max($capturedAts) : $startDate));

    return [
        'label' => $label, 'status' => $status, 'is_current' => $isCurrent,
        'start_date' => $startDate, 'end_date' => $endDate,
        'rows' => $rows, 'avg_win' => $avgWin, 'best_trade' => $bestTrade,
        'total_profit' => $totalProfit, 'win_rate' => $winRate, 'streak' => $streak,
        'progress_pct' => $progressPct, 'remaining' => $remaining,
        'target_amount' => $profitNeeded, 'target_balance' => $targetBalance,
        'account_size' => $accountSize, 'max_drawdown_amount' => $maxDrawdownAmount,
        // Additive — the raw resolved percentages feeding target_amount/
        // max_drawdown_amount above, so callers can build a correctly
        // labeled Program Configuration summary (rnProgramConfigSummary())
        // without re-deriving a percentage back out of a dollar amount.
        'profit_target_pct' => $targets['profit_target_pct'], 'max_dd_pct' => $targets['max_dd_pct'],
        'profit_target_configured' => $targets['profit_target_configured'], 'max_dd_configured' => $targets['max_dd_configured'],
        'expected_wins_needed' => $expectedWinsNeeded,
        'challenge_metrics' => $challengeMetrics,
        'trade_count' => count($rows),
        // Maximum Loss Model (2026-08-17) — additive, so renderRnPhaseCard()/
        // renderRnFundedCard() can resolve this phase's own real
        // ddReferenceBalance (rnDdReferenceBalance()) for its Maximum
        // Drawdown/Breach Level chips without a second query — this
        // function already received $rules as a parameter, it just never
        // carried it out to the caller before.
        'rules' => $rules,
        // The real, whole-account running balance as of this segment's
        // OWN last row (or its own starting point, if it has no rows
        // yet) — rnBuildJourney() carries this into the NEXT segment's
        // own $startingBalance, so the balance walk is continuous across
        // the whole account, never resetting per phase.
        'ending_balance' => $runningBalance,
    ];
}

/**
 * Reconstructs one account's real phase-by-phase journey by partitioning
 * ALL of its real closed trades once (JourneySegmentation::
 * partitionAccountJourney(), src/JourneySegmentation.php), then
 * building one card per non-empty bucket in Phase 1 → Phase 2 → Phase 3
 * → Funded order. Every bucket except the last is unconditionally
 * 'passed' — its own presence, with real trades in it, is proof the
 * account moved past it, independent of whatever lifecycle_events
 * exist or don't. Only the LAST bucket's status depends on the
 * account's live lifecycle_state (active/funded_active/breached/closed/
 * passed-not-yet-promoted).
 */
function rnBuildJourney(string $accountId, array $account, string $userId, $db, array $rules): array {
    $rnValidationCond = validationScopeSql(ValidationService::getWorkflowState($userId, $db));
    $lifecycleState = $account['lifecycle_state'] ?? '';
    $currentLabel = match (true) {
        str_contains($lifecycleState, 'phase_1') => 'Phase 1',
        str_contains($lifecycleState, 'phase_2') => 'Phase 2',
        str_contains($lifecycleState, 'phase_3') => 'Phase 3',
        $lifecycleState === 'funded_active'       => 'Funded Account',
        default                                   => 'Current Phase',
    };

    $trades = $db->raw(
        "SELECT id, instrument, direction, session, entry_price, actual_entry_price,
                sl_price, actual_sl_price, tp_price, tp2_price, tp3_price, tp4_price, tp5_price,
                tp6_price, tp7_price, tp8_price, tp9_price, tp10_price, risk_reward_tp1,
                opened_at, closed_at, captured_at, trade_classification, classification_reason,
                prop_firm_funded, prop_firm_phase, prop_phase_at_entry,
                max_potential_loss, max_potential_profit, risk_per_position, notes,
                broker_open_time, broker_close_time, is_reconciled,
                (SELECT COUNT(*) FROM trade_screenshots ts WHERE ts.trade_id = trades.id) AS screenshot_count,
                CASE WHEN :atype = 'prop_firm' THEN COALESCE(challenge_pnl, broker_pnl, projected_pnl, 0) ELSE COALESCE(broker_pnl, projected_pnl, 0) END AS effective_pnl
         FROM trades
         WHERE account_id = :aid AND user_id = :uid AND status = 'closed' AND deleted_at IS NULL
           AND {$rnValidationCond}
         ORDER BY captured_at ASC",
        [':atype' => $account['account_type'], ':aid' => $accountId, ':uid' => $userId]
    );
    rnAttachPriorLossCounts($trades);

    if (empty($trades)) {
        $card = rnBuildPhaseCardFromRows($accountId, $account, $currentLabel, rnDeriveTerminalStatus($lifecycleState, $currentLabel), [], $rules);
        $card['is_funded'] = $currentLabel === 'Funded Account';
        $realStart = rnFindStartedOnDate($db, $accountId, $currentLabel);
        if ($realStart !== null) $card['start_date'] = $realStart;
        return [$card];
    }

    // Journey segmentation (Instant Funding's no-phases rule + funded-
    // state reconciliation) lives entirely in JourneySegmentation (src/
    // JourneySegmentation.php, real PHP-enforced class privacy — see
    // that file's own header), so Active Journeys and Trading Archive
    // (TradingArchiveEngine::gatherPropSegments(), which calls this
    // exact same public method) can never again independently drift on
    // how the same account's journey is bucketed. Do not re-inline this
    // logic here — if it ever needs to change, change it once in
    // JourneySegmentation.
    $buckets = JourneySegmentation::partitionAccountJourney($trades, $account['challenge_type'] ?? null, $currentLabel);

    $keys = array_keys($buckets);
    $n = count($keys);
    $out = [];
    // Reverted (2026-08-07) — cross-phase balance threading was a real
    // mistake, based on a wrong assumption. Verified live, directly: a
    // fresh Phase 1 -> Phase 2 promotion DOES correctly reset
    // challenge_accounts.current_balance to account_size (confirmed via
    // its own updated_at matching the promotion timestamp exactly) —
    // real prop-firm evaluations genuinely start each phase fresh,
    // ignoring the prior phase's profit. trading_accounts.current_balance
    // never resets at all, which is correct ONLY for a funded account's
    // real, continuous capital — not for an account still mid-evaluation.
    // Each phase/segment's own table correctly starts its own balance
    // walk fresh at $accountSize again, matching the real, approved,
    // pre-existing design (see rnBuildPhaseCardFromRows()'s own doc
    // comment for what's still genuinely fixed vs. what was reverted).
    foreach ($keys as $i => $label) {
        $isLast = $i === $n - 1;
        $status = $isLast ? rnDeriveTerminalStatus($lifecycleState, $label) : 'passed';
        $card = rnBuildPhaseCardFromRows($accountId, $account, $label, $status, $buckets[$label], $rules);
        $card['is_funded'] = $label === 'Funded Account';
        $realStart = rnFindStartedOnDate($db, $accountId, $label);
        if ($realStart !== null) $card['start_date'] = $realStart;
        if ($status === 'passed') {
            $lastTrade = end($buckets[$label]);
            $card['passed_on'] = rnFindPassedOnDate($db, $accountId, $label) ?? ($lastTrade['closed_at'] ?? $lastTrade['captured_at'] ?? null);
        }

        // Single source of truth for the account's CURRENT segment's own
        // total profit / balance (2026-08-08) — see
        // docs/architecture/SINGLE_SOURCE_OF_TRUTH_AUDIT.md §1d/1e and the
        // MIGRATION_PLAN's Wave 3e. PropFirmRuleEngine::compute() only
        // ever answers about the account's real, live CURRENT phase —
        // substituted here ONLY when its own phase_label matches this
        // exact segment (the same array_key_last()-style safety check
        // already established elsewhere in this codebase), so a
        // historical/already-passed segment (which PropFirmRuleEngine
        // structurally cannot compute) is never touched.
        //
        // This does NOT change rnBuildPhaseCardFromRows()'s own internals,
        // its per-row trade-table balance walk, or any of its other
        // returned fields (avg_win/best_trade/win_rate/streak/
        // target_amount/max_drawdown_amount) — deliberately scoped to
        // exactly the summary fields the audit found independently
        // re-derived downstream (renderRnFundedCard()'s and
        // renderRnCoachingSection()'s own "$phase['account_size'] +
        // $phase['total_profit']" — both read this same card, so fixing
        // it here closes all three at once without editing either
        // function). Mathematically PROVEN identical to this file's own
        // local walk before this change (same trades — identical status/
        // deleted_at/is_validation filters — same P&L cascade, same
        // account_size anchor) — this repoint changes the SOURCE of the
        // number, not its value; a real divergence would indicate a bug
        // and is logged rather than silently trusted either way.
        if ($isLast && ($account['account_type'] ?? null) === 'prop_firm') {
            $pfr = PropFirmRuleEngine::compute($accountId);
            if (!isset($pfr['error']) && $pfr['phase_label'] === $label) {
                $canonicalTotalProfit = (float)$pfr['phase_balance'] - (float)$pfr['starting_balance'];
                if (abs($canonicalTotalProfit - $card['total_profit']) > 0.01) {
                    error_log("[rnBuildJourney] PropFirmRuleEngine total_profit diverges from local calc for account {$accountId}: canonical={$canonicalTotalProfit} local={$card['total_profit']}");
                }
                $card['total_profit'] = $canonicalTotalProfit;
                $card['remaining'] = max(0.0, $card['target_amount'] - $canonicalTotalProfit);
                $card['progress_pct'] = $card['target_amount'] > 0 ? max(0.0, ($canonicalTotalProfit / $card['target_amount']) * 100) : 100.0;
                $card['ending_balance'] = (float)$pfr['phase_balance'];
                $card['expected_wins_needed'] = ($status === 'passed') ? null
                    : (($card['avg_win'] !== null && $card['avg_win'] > 0 && $card['remaining'] > 0) ? (int)ceil($card['remaining'] / $card['avg_win']) : ($card['remaining'] <= 0 ? 0 : null));
            }
        }

        $out[] = $card;
    }
    return $out;
}

/**
 * Renders the full Right Now page: header + account selector, one card
 * per real phase/segment of the selected account's journey, and the
 * purple "Right Now (Active Coaching)" section for whichever segment is
 * currently live.
 */
/**
 * Item 1 — Right Now is exclusively for accounts the trader is currently
 * managing. Excludes breached/closed accounts (those belong in Completed
 * Journeys) — the prior version only checked ta.is_active=1, which never
 * actually excluded a breached account (a breached account is still
 * "active" in the is_active column sense). Dormant-but-still-ACTIVE
 * accounts (zero trades yet) are correctly still included — nothing here
 * filters on trade activity, only on real lifecycle state.
 */
// Maximum Loss Model comment note doesn't apply here — this is the real
// gap the 2026-08-16 blanket page gate was worked around instead of fixed
// (2026-08-18 correction): neither account-picker query filtered by
// trading_accounts.workflow_context, the same real, persisted,
// first-class "which workflow does this account belong to" column
// Trading Center's own AccountPicker::apFetchAccounts() already respects
// (see CLAUDE.md's "Account-level workflow ownership" session) — an
// account is owned by exactly one workflow for its whole life, fixed at
// creation. Both functions now take the caller's already-resolved
// workflow state and filter on it directly, so a validating user's Active
// Journeys can only ever see accounts created under 'validation', and a
// main-workflow user can only ever see 'main' accounts — no derived
// heuristic, the same plain equality check apFetchAccounts() uses.
function rnFetchActivePropAccounts(string $userId, $db, string $workflowState): array {
    return $db->raw(
        "SELECT ta.id, ta.label, ta.account_type, ta.lifecycle_state, ta.starting_balance, ta.current_balance,
                ta.created_at, ta.prop_firm_name, ca.challenge_type, ca.account_size, ca.rules, ca.leverage,
                ta.platform, ta.broker_server, ta.broker_login, ta.broker_password_encrypted, ta.account_number, ta.tags, ta.account_notes,
                (SELECT MAX(COALESCE(closed_at, captured_at)) FROM trades WHERE account_id = ta.id AND deleted_at IS NULL) AS last_activity
         FROM trading_accounts ta
         JOIN challenge_accounts ca ON ca.account_id = ta.id
         WHERE ta.user_id = :uid AND ta.deleted_at IS NULL AND ta.is_active = 1 AND ta.workflow_context = :wfctx
           AND (ta.lifecycle_state IS NULL OR ta.lifecycle_state NOT IN ('breached', 'closed'))
         ORDER BY last_activity DESC",
        [':uid' => $userId, ':wfctx' => $workflowState]
    );
}

/** Same active-only rule as prop accounts (item 1), for the Live Accounts switch (item 2). Workflow-scoped the same way — see rnFetchActivePropAccounts()'s own comment. */
function rnFetchActiveLiveAccounts(string $userId, $db, string $workflowState): array {
    return $db->raw(
        "SELECT ta.id, ta.label, ta.account_type, ta.lifecycle_state, ta.starting_balance, ta.current_balance,
                ta.created_at, ta.currency, ta.net_deposits, ta.net_withdrawals, ta.initial_deposit, ta.peak_equity,
                ta.platform, ta.broker_server, ta.broker_login, ta.broker_password_encrypted, ta.account_number, ta.tags, ta.account_notes,
                (SELECT MAX(COALESCE(closed_at, captured_at)) FROM trades WHERE account_id = ta.id AND deleted_at IS NULL) AS last_activity
         FROM trading_accounts ta
         WHERE ta.user_id = :uid AND ta.deleted_at IS NULL AND ta.is_active = 1 AND ta.account_type = 'personal' AND ta.workflow_context = :wfctx
           AND (ta.lifecycle_state IS NULL OR ta.lifecycle_state NOT IN ('closed'))
         ORDER BY last_activity DESC",
        [':uid' => $userId, ':wfctx' => $workflowState]
    );
}

/** Right Now Journal URL builder — keeps zone=now + rn_type + (optional) rn_account + (optional) rn_challenge consistent everywhere they're linked from. */
function rnUrl(string $rnType, ?string $accountId = null, ?string $challenge = null): string {
    $params = ['zone' => 'now', 'rn_type' => $rnType];
    if ($accountId !== null) $params['rn_account'] = $accountId;
    $effectiveChallenge = $challenge ?? (($rnType === 'prop' && !empty($_GET['rn_challenge']) && $_GET['rn_challenge'] !== 'all') ? $_GET['rn_challenge'] : null);
    if ($rnType === 'prop' && $effectiveChallenge) $params['rn_challenge'] = $effectiveChallenge;
    return '/traderalign/journal?' . http_build_query($params);
}

/**
 * Item 2, Prop Firms / Live Accounts switch, plus the MT5 upload entry
 * point. Default remains Prop Firms.
 *
 * $currentAccountId threads whichever account the trader already has
 * selected (the raw ?rn_account= GET value, read directly rather than
 * waiting for renderRightNow()'s own resolved/defaulted $accountId,
 * which isn't computed until after this function is called: reading the
 * same GET param here is equivalent whenever the trader has actually
 * picked one, and simply omits the pre-fill on a first visit where
 * nothing's been chosen yet) into mt5-upload.php's own account selector,
 * so clicking through doesn't dead-end into a second, disconnected
 * "which account is this?" prompt. The whole point of putting this
 * entry point here, next to an already-account-scoped workspace, is that
 * it should carry that context forward, not discard it.
 */
function renderRnTypeSwitch(string $rnType, ?string $currentAccountId = null): void {
    $uploadUrl = '/traderalign/mt5-upload' . ($currentAccountId ? '?account_id=' . urlencode($currentAccountId) : '');
    ?>
    <div class="rn-toolbar-row">
        <div class="rn-type-switch">
            <a href="<?= htmlspecialchars(rnUrl('prop')) ?>" class="rn-type-tab <?= $rnType === 'prop' ? 'active' : '' ?>">Prop Firms</a>
            <a href="<?= htmlspecialchars(rnUrl('live')) ?>" class="rn-type-tab <?= $rnType === 'live' ? 'active' : '' ?>">Live Accounts</a>
        </div>
        <a href="<?= htmlspecialchars($uploadUrl) ?>" class="rn-mt5-link" title="Reconcile your journaled trades against a real MT5 broker statement">
            <span class="rn-mt5-link-icon">🔗</span> Upload MT5 Statement
        </a>
    </div>
    <?php
}

/**
 * Item 3 — account selector redesign. Never dumps every account into view
 * at once: the 5 most-recently-traded accounts (the list is already
 * ordered that way by the caller's query) show by default, the rest are
 * rendered too — so Search can reach them — but hidden behind "Show More",
 * the same reveal pattern renderRnTradeTable() already uses for trade
 * rows. Identical markup/behavior for both Prop Firms and Live Accounts;
 * only the $accounts list passed in differs.
 */
function renderRnAccountPicker(array $accounts, string $selectedId, string $rnType): void {
    // Trading Center redesign: delegates to the shared component
    // (src/AccountPicker.php) instead of its own copy of this markup —
    // rnUrl() is passed through as the base-URL/params binding so this
    // page's own ?zone=now&rn_type=...&rn_account=...&rn_challenge=...
    // scheme is unaffected. rnFetchActivePropAccounts()/
    // rnFetchActiveLiveAccounts() and every other rn* function are
    // untouched by this change.
    $effectiveChallenge = ($rnType === 'prop' && !empty($_GET['rn_challenge']) && $_GET['rn_challenge'] !== 'all') ? $_GET['rn_challenge'] : null;
    $baseParams = ['zone' => 'now', 'rn_type' => $rnType];
    if ($effectiveChallenge) $baseParams['rn_challenge'] = $effectiveChallenge;
    apRenderAccountPicker(
        $accounts,
        $selectedId,
        '/traderalign/journal',
        $baseParams,
        'rn_account',
        function (array $a) use ($rnType): string {
            return $rnType === 'prop' ? apChallengeTypeLabel($a['challenge_type'] ?? null) : 'Live Account';
        }
    );
}

function renderRightNow(string $userId, $db, PDO $pdo): void {
    $rnType = ($_GET['rn_type'] ?? 'prop') === 'live' ? 'live' : 'prop';
    renderRnTypeSwitch($rnType, isset($_GET['rn_account']) ? (string)$_GET['rn_account'] : null);

    // Same per-request-cached read every other workflow check in this file
    // already uses (ValidationService::getWorkflowState() micro-caches per
    // userId within one request) — the account list itself is now
    // workflow-scoped, not just the trades inside it.
    $rnWorkflowState = ValidationService::getWorkflowState($userId, $db);
    $accounts = $rnType === 'prop' ? rnFetchActivePropAccounts($userId, $db, $rnWorkflowState) : rnFetchActiveLiveAccounts($userId, $db, $rnWorkflowState);

    // Item 4 — Challenge Program filter (1 Step / 2 Step / 3 Step / Instant
    // Funding), Prop Firms only. Filters the account list before selection
    // so both the picker and the auto-selected default respect it.
    $challengeFilter = 'all';
    if ($rnType === 'prop') {
        $challengeFilter = $_GET['rn_challenge'] ?? 'all';
        if (!in_array($challengeFilter, ['all', 'one_step', 'two_step', 'three_step', 'instant_funding'], true)) $challengeFilter = 'all';
        renderRnChallengeFilter($rnType, $challengeFilter);
        if ($challengeFilter !== 'all') {
            $accounts = array_values(array_filter($accounts, fn($a) => ($a['challenge_type'] ?? '') === $challengeFilter));
        }
    }

    if (empty($accounts)) {
        if ($rnType === 'prop' && $challengeFilter !== 'all') {
            $msg = 'No active ' . rnChallengeTypeLabel($challengeFilter) . ' accounts right now. Try a different program filter above.';
        } else {
            $msg = $rnType === 'prop'
                ? 'No active prop firm accounts right now. Breached and closed accounts have moved to the Trading Archive. Add a prop firm account from the Dashboard to see your journey here.'
                : 'No active live accounts right now. Closed accounts have moved to the Trading Archive. Add a live account from the Dashboard to see it here.';
        }
        echo '<div class="empty-state"><h3>No active accounts</h3><p style="font-size:.85rem">' . htmlspecialchars($msg) . '</p></div>';
        return;
    }

    $selectedId = $_GET['rn_account'] ?? null;
    $selectedAccount = null;
    foreach ($accounts as $a) { if ($a['id'] === $selectedId) { $selectedAccount = $a; break; } }
    // $accounts is already ordered most-recently-traded first and already
    // pre-filtered to active-only (item 1) — the first entry is always a
    // sane default, no separate "prefer active" pass needed anymore.
    if (!$selectedAccount) $selectedAccount = $accounts[0];

    $accountId = $selectedAccount['id'];
    ?>
    <div class="rn-header">
        <div>
            <h1 class="rn-title">Active Journeys</h1>
            <div class="rn-subtitle">Manage the accounts you're still actively trading</div>
        </div>
        <div class="rn-header-right">
            <?php renderRnAccountPicker($accounts, $accountId, $rnType); ?>
            <button type="button" class="rn-btn-gold" onclick="rnToggleWorkspace('rn-account-summary')">⧉ View Account Summary</button>
        </div>
    </div>
    <?php // Item 6 — one shared filter banner; rnFilterTrades()/rnClearTradeFilter() (JS) show/hide it and fill in its text. ?>
    <div id="rn-filter-banner" class="rn-filter-banner" style="display:none">
        <span id="rn-filter-banner-text"></span>
        <button type="button" class="rn-btn-outline" onclick="rnClearTradeFilter()">✕ Clear Filter</button>
    </div>
    <?php
    // Trade Journal filter bar (journal redesign, 2026-07-31; trimmed same
    // day — the compact trade-list result panel this used to feed was
    // removed as duplicated functionality, see renderTradeJournalSection()'s
    // doc comment; refined again 2026-07-31 to actually filter — see
    // rnFilterTradeJournalRows()). $jtFilters sources the filter bar's
    // current values (Instrument/Session/Classification/Date) and is
    // applied, per phase/segment, to narrow which rows each of THIS
    // already-selected account's own trade tables display — not a
    // cross-account query, since there's no separate results panel here.
    //
    // rn_account MUST be threaded into $jtBaseParams (2026-07-31 fix, state-
    // sync bug): the filter form below submits via GET, which replaces the
    // ENTIRE query string with only the fields the form itself carries — it
    // does not merge with whatever was already in the URL. Without
    // rn_account as a hidden field here, submitting a filter (or clicking
    // Clear) silently dropped the selected account from the URL, and
    // $selectedId above would fall back to $accounts[0] (the most-recently-
    // traded account) on the next request — the Account Selector's choice
    // being silently overridden by the filter engine. The Account Selector
    // ($accountId, already resolved above) is the single source of truth for
    // which account is active; every filter must operate against it and
    // must never cause it to change.
    $jtFilters = buildJournalTradesFilters($_GET);
    $jtBaseParams = ['rn_type' => $rnType, 'rn_account' => $accountId];
    if ($rnType === 'prop' && $challengeFilter !== 'all') $jtBaseParams['rn_challenge'] = $challengeFilter;
    renderTradeJournalSection($jtFilters, $userId, $db, $jtBaseParams);

    $rules = [];
    $allTradeRows = [];
    if ($rnType === 'prop') {
        $rules = !empty($selectedAccount['rules']) ? (json_decode($selectedAccount['rules'], true) ?: []) : [];
        $phases = rnBuildJourney($accountId, $selectedAccount, $userId, $db, $rules);
        $currentPhase = null;
        foreach ($phases as $p) { if ($p['is_current']) { $currentPhase = $p; break; } }

        // Trade Journal filters (Search/Instrument/Session/Classification/
        // Date range, above) narrow which rows each phase's own trade table
        // DISPLAYS — everything else (progress/streak/risk-rec/coaching
        // evidence) still reads the phase's real, unfiltered 'rows'.
        foreach ($phases as $phase) {
            $phase['display_rows'] = rnFilterTradeJournalRows($phase['rows'], $jtFilters, $selectedAccount['label'] ?? '');
            if (!empty($phase['is_funded'])) {
                renderRnFundedCard($userId, $db, $selectedAccount, $phase, $rules);
            } else {
                renderRnPhaseCard($selectedAccount, $phase);
            }
            foreach ($phase['rows'] as $r) { $allTradeRows[] = $r['trade']; }
        }
        // Item 7 — real account-preservation state drives Recommended Risk,
        // not just whatever the declared strategy says. See
        // rnComputeRiskRecommendation()'s own doc comment for the full
        // reasoning; this builds its real inputs from the SAME
        // challenge_metrics/phase data already computed above, nothing
        // re-queried.
        $cm = $currentPhase['challenge_metrics'] ?? null;
        $overallUsedRatio = ($cm && (float)($cm['overall_drawdown']['limit_percentage'] ?? 0) > 0)
            ? (float)$cm['overall_drawdown']['current_percentage'] / (float)$cm['overall_drawdown']['limit_percentage'] : 0.0;
        $dailyUsedRatio = ($cm && (float)($cm['daily_drawdown']['limit_percentage'] ?? 0) > 0)
            ? (float)$cm['daily_drawdown']['current_percentage'] / (float)$cm['daily_drawdown']['limit_percentage'] : 0.0;
        // §1.4 fix (2026-08-10) — $cm is only null when getChallengeMetrics()
        // threw (its own try/catch a few hundred lines up) or the account
        // simply has no current phase yet — either way $overallUsedRatio/
        // $dailyUsedRatio silently defaulting to 0.0 above used to be
        // indistinguishable from a real, confirmed 0%-drawdown-used reading,
        // which forced the confident "healthy range" tier/reason text with
        // no real data behind it. Threading the real availability flag
        // through lets rnComputeRiskRecommendation() say so honestly instead
        // of silently asserting a health reading it never actually computed.
        $riskRec = rnComputeRiskRecommendation($accountId, $userId, $db, [
            'dd_used_ratio' => max($overallUsedRatio, $dailyUsedRatio),
            'dd_data_available' => $cm !== null,
            'near_failure' => !empty($cm['distance_to_breach']['is_near_breach']),
            'streak' => $currentPhase['streak'] ?? 0,
            'recent_trades' => $currentPhase !== null ? rnRecentTradesForRisk($currentPhase['rows']) : [],
        ]);
    } else {
        $segment = rnBuildLiveSegment($accountId, $selectedAccount, $userId, $db);
        // Trade Journal filters narrow the trade table only — see the
        // matching comment on the prop branch above.
        $segment['display_rows'] = rnFilterTradeJournalRows($segment['rows'], $jtFilters, $selectedAccount['label'] ?? '');
        renderRnLiveCard($userId, $db, $selectedAccount, $segment);
        $am = $segment['account_metrics'];
        $equity = $am['equity'] ?? ($segment['starting_balance'] + $segment['total_profit']);
        $peakEquity = ($am['peak_equity'] ?? null) !== null ? (float)$am['peak_equity'] : max($equity, $segment['starting_balance']);
        // Item 7 (live accounts have no declared drawdown limit) — a 20%
        // drawdown-from-peak informal ceiling stands in for "the account's
        // drawdown budget," the same way it already does for flagging a
        // live account on Reports elsewhere in this app. This is a judgment
        // call (no firm-imposed number exists to reference for a personal
        // account), documented as such rather than silently invented.
        $ddFromPeakPct = $peakEquity > 0 ? max(0.0, ($peakEquity - $equity) / $peakEquity) : 0.0;
        $riskRec = rnComputeRiskRecommendation($accountId, $userId, $db, [
            'dd_used_ratio' => $ddFromPeakPct / 0.20,
            'near_failure' => false,
            'streak' => $segment['streak'] ?? 0,
            'recent_trades' => rnRecentTradesForRisk($segment['rows']),
        ]);
        $allTradeRows = array_column($segment['rows'], 'trade');
    }

    // Collapsible "Right Now — Active Coaching" section (journal redesign,
    // 2026-07-31). Kevin's brief: "collapsed by default... shows only the
    // heading" — a plain <details> gives native collapse/expand with no
    // new JS, and the inner content (the coaching stats/recommendation
    // card, already headed "Right Now (Active Coaching)" internally, plus
    // Psychology Meets Strategy) is byte-for-byte the same as before,
    // just no longer always-visible. Both render calls moved here
    // (unchanged) from inside the prop/live branches above, since
    // $currentPhase/$segment stay in scope across the if/else (PHP has no
    // block-level scoping) and this is the one place both branches need
    // to converge before the shared Psychology fetch.
    ?>
    <details class="rn-collapsible">
        <summary class="rn-collapsible-summary">Active Coaching</summary>
        <div class="rn-collapsible-body">
    <?php
    if ($rnType === 'prop') {
        renderRnCoachingSection($userId, $db, $selectedAccount, $currentPhase, $riskRec);
    } else {
        renderRnCoachingSectionLive($userId, $db, $selectedAccount, $segment, $riskRec);
    }
    // Items 2-10 — "Psychology Meets Strategy". A dedicated fetch (never
    // reusing $allTradeRows) because this is the one place in Right Now
    // that reads mood_before/energy_before/confidence_before and checklist
    // ticked_pct — kept isolated so those reads stay confined to exactly
    // this section, per the architectural boundary in item 8.
    $psychTrades = rnFetchAccountPsychologyTrades($accountId, $userId, $db, $selectedAccount['account_type']);
    renderRnPsychologyStrategy($psychTrades);
    ?>
        </div>
    </details>
    <?php

    renderRnCoachingWorkspace($userId, $db, $selectedAccount, $rnType, $allTradeRows, $riskRec);
    renderRnAccountSummaryPanel($selectedAccount, $rnType, $userId, $db, $pdo, $rnWorkflowState);
    renderRnGoalModal();
    renderRnTransactionModal();
    renderRnScreenshotModal();
}

/**
 * ONE reusable trade table — used identically by every stage (Phase 1,
 * Phase 2, Phase 3, Instant Funding, Funded), per Kevin's explicit "one
 * identical table everywhere, only the dataset changes" rule. Never call
 * a different rendering path for a different stage; if a stage's table
 * needs a new column, add it here once so every stage gets it.
 *
 * Columns: Date, Session, Bias, Entry, SL, TP (with a "+N" badge when
 * additional TP levels are configured — hover-to-expand is intentionally
 * NOT implemented yet, per the brief), Pips/Points, P&L (USD), Balance,
 * R:R, Duration, State, View Trade (2026-08-06: renamed from
 * "Screenshot" and disabled to a real "Coming Soon" state — see
 * docs/architecture/TRADERALIGN_MT5_SYNC_ENGINE_AND_TRADE_REVIEW_
 * ARCHITECTURE.md, which names this exact feature "View Trade" and
 * explicitly describes it as replacing the old "Upload Screenshot"
 * entry point once built; it previously linked into the old single-
 * trade 4-Stage view instead, which was never the real destination),
 * Notes (no free-text trade-note field exists anywhere in this
 * schema yet — shown as a clearly-marked placeholder, not fabricated),
 * and See Full Details.
 *
 * Shows the full table when there ARE trades — never a "No closed
 * trades..." message unless $rows is genuinely empty. Initially reveals
 * only the most recent 7 rows (rows are already newest-first is NOT
 * assumed — callers pass rows in the stage's own real chronological
 * order, oldest first, matching the approved prototype's own row order);
 * "Show More"/"Show Less" reveal/hide the rest, identical behavior on
 * every table via one shared JS function (rnToggleTableRows()).
 */
/**
 * Trade Journal filters — narrows which rows renderRnTradeTable() actually
 * displays for the currently selected account/segment. Deliberately kept
 * separate from $phase['rows']/$segment['rows'] themselves (still passed
 * around unfiltered everywhere else — phase stats, streaks, the risk
 * recommendation, the coaching workspace's evidence) so narrowing what's
 * DISPLAYED in the trade table never changes any other computed number on
 * the page. Called once per phase/segment right before it's rendered; the
 * result is stashed on 'display_rows' and read by renderRnTradeTable()'s
 * three call sites in place of 'rows'.
 *
 * Search (q) matches instrument, the account's own label, or the trade's
 * notes — OR across the three, not AND. Instrument/session/classification
 * are exact (case-insensitive) matches. Date filtering is inclusive; a
 * single date (either From or To set alone) narrows to that one calendar
 * day rather than an open-ended "on or after"/"up to."
 */
function rnFilterTradeJournalRows(array $rows, array $filters, string $accountLabel): array {
    $q = trim((string)($filters['q'] ?? ''));
    $instrument = trim((string)($filters['instrument'] ?? ''));
    $session = trim((string)($filters['session'] ?? ''));
    $classification = trim((string)($filters['classification'] ?? ''));
    $from = trim((string)($filters['from'] ?? ''));
    $to = trim((string)($filters['to'] ?? ''));

    if ($q === '' && $instrument === '' && $session === '' && $classification === '' && $from === '' && $to === '') {
        return $rows;
    }

    // A lone date (From only, or To only) narrows to that single day —
    // Kevin's own spec: "Do not require both dates." Both set = an
    // inclusive range.
    $singleDate = null;
    if ($from !== '' && $to === '') $singleDate = $from;
    elseif ($to !== '' && $from === '') $singleDate = $to;

    return array_values(array_filter($rows, function ($r) use ($q, $instrument, $session, $classification, $from, $to, $singleDate, $accountLabel) {
        $t = $r['trade'];

        if ($instrument !== '' && strcasecmp((string)($t['instrument'] ?? ''), $instrument) !== 0) return false;
        if ($session !== '' && strcasecmp((string)($t['session'] ?? ''), $session) !== 0) return false;
        if ($classification !== '' && strcasecmp((string)($t['trade_classification'] ?? ''), $classification) !== 0) return false;

        if ($singleDate !== null || ($from !== '' && $to !== '')) {
            $dateSrc = $t['closed_at'] ?? $t['captured_at'] ?? null;
            if ($dateSrc === null) return false;
            $tradeDate = date('Y-m-d', strtotime($dateSrc));
            if ($singleDate !== null) {
                if ($tradeDate !== $singleDate) return false;
            } else {
                if ($tradeDate < $from || $tradeDate > $to) return false;
            }
        }

        if ($q !== '') {
            $hit = false;
            foreach ([(string)($t['instrument'] ?? ''), $accountLabel, (string)($t['notes'] ?? '')] as $h) {
                if ($h !== '' && stripos($h, $q) !== false) { $hit = true; break; }
            }
            if (!$hit) return false;
        }

        return true;
    }));
}

/**
 * Item 6 (2026-08-06) — a real correctness bug, reported directly: a
 * single trade that immediately breached an account (e.g. -$400 on a
 * $5,000/8%-max-drawdown account — the ENTIRE drawdown budget gone in one
 * trade) was rendering "Controlled Loss" in the State column. Root
 * cause: that column's only source was `trades.trade_classification` — a
 * real, stored value, but one `sp_classify_trade` computes from the
 * trader's own DECLARED risk% and a 10-trade loss-streak lookback, never
 * from how much of the account's own real drawdown budget a trade
 * actually consumed. A trader who declares 10% risk and instantly blows
 * the account still reads "within plan" under that scheme — backwards
 * for a column whose whole job is warning about account preservation.
 *
 * This does NOT touch `trade_classification` or sp_classify_trade
 * themselves — that stored value keeps its own established meaning and
 * is still exactly what Reports/Trader Profile/Trading Archive's
 * findings-and-patterns engine read elsewhere. It only replaces what the
 * STATE COLUMN itself displays for a genuine LOSS, with a real, computed
 * drawdown-budget-consumption ratio instead: how much of the account's
 * own configured max-drawdown dollar limit had been used, cumulatively,
 * by the time this trade closed (walked from the account's own real
 * running peak equity, the same technique TradingArchiveEngine's own
 * segmentMetrics()/buildAccountFailureJourney() already use elsewhere in
 * this app). Wins and breakevens are untouched — the reported bug was
 * specifically about losses reading as safe when they weren't. Returns
 * null (never guesses) when the trade isn't a real loss, or when the
 * caller didn't supply real account-size/drawdown-limit context —
 * renderRnTradeTable() falls back to the stored classification in both
 * cases, so a call site with no real limit to check against behaves
 * exactly as it always has.
 *
 * Six tiers, referenced against $ddUsedRatio (0 = no real drawdown yet,
 * 1.0 = this trade consumed the entire configured limit — i.e. the real
 * breach trade).
 *
 * Item 2 (2026-08-06, refinement pass) — re-anchored to share its zone
 * boundaries with rnComputeRiskRecommendation() (Active Coaching's own
 * Recommended Risk tile, above), rather than a second, independently
 * chosen set of breakpoints for the same underlying idea. Compared both
 * systems before changing anything: Active Coaching is the stronger
 * implementation — beyond the account's own drawdown-budget consumption,
 * it also downgrades further for a real losing streak (≤-3) and for a
 * recent tilt/revenge/oversized-risk pattern in the last 10 closed
 * trades, neither of which this per-row State label currently has the
 * per-trade context to evaluate cheaply across every trades-table call
 * site — so that richer logic was left exactly as it is, not rebuilt
 * here. What WAS a real inconsistency: this function's own 12.5/50/70/85
 * breakpoints didn't line up with Active Coaching's 25/50/75 healthy/
 * moderate/serious/critical zones, so the same account state could read
 * as "healthy" in one card and already-escalated in the other. Fixed by
 * moving this function's tier boundaries onto that exact same 25/50/75
 * split — the two systems now always agree on which zone a given
 * drawdown-budget level falls into; this function still reports six
 * labels (finer-grained than Coaching's four) by splitting the healthy
 * zone into "Disciplined"/"Controlled" and the critical zone into
 * "Revenge Risk"/"Critical Risk" (the real breach trade itself).
 */
function rnAccountPreservationState(float $pnl, ?float $ddUsedRatio): ?array {
    if ($pnl >= -0.01 || $ddUsedRatio === null) return null;
    return match (true) {
        $ddUsedRatio <= 0.125 => ['Disciplined Loss', 'rn-state-neutral'],   // Active Coaching: healthy zone (< 25%)
        $ddUsedRatio <= 0.25  => ['Controlled Loss', 'rn-state-neutral'],    // Active Coaching: healthy zone (< 25%)
        $ddUsedRatio <= 0.5   => ['Tilt Warning', 'rn-state-warn'],          // Active Coaching: moderate zone (25-50%)
        $ddUsedRatio <= 0.75  => ['High Risk Behaviour', 'rn-state-bad'],    // Active Coaching: serious zone (50-75%)
        $ddUsedRatio < 1.0    => ['Revenge Risk', 'rn-state-bad'],           // Active Coaching: critical zone (≥ 75%)
        default               => ['Critical Risk', 'rn-state-bad'],         // the real breach trade itself
    };
}

/** Real numbers behind rnAccountPreservationState()'s verdict — the account's own actual drawdown-budget consumption, never the trader's declared risk%. */
function rnAccountPreservationTooltip(float $pnl, float $ddDollarAfter, float $maxDrawdownDollar, float $ddUsedRatio): string {
    $moneyPnl = '-$' . number_format(abs($pnl), 2);
    return "Closed {$moneyPnl}. After this trade, the account had used " . number_format(min(999, $ddUsedRatio * 100), 1)
        . '% of its $' . number_format($maxDrawdownDollar, 2) . ' maximum drawdown budget ($' . number_format($ddDollarAfter, 2)
        . " drawn down from its own real running peak equity), based on real account preservation, not declared risk.";
}

/**
 * $breachTradeId (optional, additive — every existing call site that
 * omits it renders byte-identically to before) marks exactly one row as
 * the account's real final/breaching trade — a red left-bar + tint via
 * .rn-row-breach (see journal.php's own <style> block) plus a "BREACH
 * TRADE" badge, never a redesign of the table itself.
 *
 * $accountSize/$maxDrawdownDollar (optional, additive) feed
 * rnAccountPreservationState() above (item 6) — every call site that
 * supplies them gets the real, drawdown-budget-based State column for
 * its loss rows; a call site that doesn't (none currently) keeps the old
 * stored-classification behavior exactly as before.
 *
 * $isBreachView (optional, additive, default false) is true only for
 * renderBreachTradesView() below — a finished account's trade history
 * told in the past tense: Duration's empty state reads in the past
 * tense (item 5), Notes becomes read-only (item 8, the account is
 * already finished — nothing left to journal), and the "See Full
 * Details" column (a link into the live 4-Stage trade-review workflow)
 * is dropped entirely (item 9) — that workflow was already available to
 * the trader before the breach; reopening it here adds nothing Lessons
 * From Failure's own coaching cards don't already cover.
 */
function renderRnTradeTable(array $rows, string $tableId, bool $filtered = false, ?string $breachTradeId = null, ?float $accountSize = null, ?float $maxDrawdownDollar = null, bool $isBreachView = false, ?float $ddReferenceBalance = null, bool $hideDetailsColumn = false): void {
    if (empty($rows)) {
        echo $filtered
            ? '<div class="rn-empty-note">No trades match the selected filters.</div>'
            : '<div class="rn-empty-note">No closed trades in this phase yet.</div>';
        return;
    }
    $tpExtraCols = ['tp2_price', 'tp3_price', 'tp4_price', 'tp5_price', 'tp6_price', 'tp7_price', 'tp8_price', 'tp9_price', 'tp10_price'];
    $initialVisible = 7;
    $totalRows = count($rows);

    // Real drawdown-budget consumption per row, only when real account-
    // size/drawdown-limit context was supplied. Deliberately measured
    // against the account's own STATIC starting floor (accountSize −
    // balance) for a FIXED account, never a trailing peak — a real live
    // account confirmed why a trailing-peak model would be wrong THERE:
    // an account that ran its balance well above its starting size
    // before pulling back still has real, unspent drawdown budget
    // relative to its own (never-moving, Fixed) floor, even though it's
    // fallen a long way from its own peak — a peak-relative ratio
    // flagged a real trade "Critical Risk" at 162%+ on an account that
    // was still thousands of dollars above its actual floor, for a
    // breach that (confirmed against the real account) was caused by an
    // unrelated consistency-rule violation, not drawdown at all.
    //
    // Maximum Loss Model (2026-08-17) — that finding was about a Fixed
    // account specifically, where the real floor genuinely never moves,
    // so a peak-relative measure was the wrong yardstick. For a real
    // TRAILING account the floor DOES move with the peak, so the
    // opposite substitution applies: this ratio must be measured against
    // $ddReferenceBalance (the account's real peak for Trailing,
    // identical to $accountSize for Fixed — see rnDdReferenceBalance())
    // rather than the always-static $accountSize, or a Trailing account
    // would understate how much of its REAL, currently-relevant budget
    // has actually been used. Falls back to $accountSize when the caller
    // hasn't supplied one, byte-identical to the pre-existing behavior
    // this whole comment documents.
    $ddByIdx = [];
    $ddRefForTable = $ddReferenceBalance ?? $accountSize;
    if ($ddRefForTable !== null && $maxDrawdownDollar !== null && $maxDrawdownDollar > 0) {
        foreach ($rows as $idx => $r) {
            $ddDollar = max(0.0, $ddRefForTable - $r['balance']);
            $ddByIdx[$idx] = ['dollar' => $ddDollar, 'ratio' => $ddDollar / $maxDrawdownDollar];
        }
    }
    ?>
    <div class="rn-table-scroll">
    <table class="rn-table" id="<?= htmlspecialchars($tableId) ?>">
        <thead>
            <tr>
                <th>Date ⇅</th><th>Instrument</th><th>Session</th><th>Bias</th><th>Entry</th><th>SL</th><th>TP</th>
                <th>Pips / Points</th><th>P&amp;L (USD)</th><th>Balance</th><th>R:R</th>
                <th>Duration</th><th>State</th><th>View Trade</th><th>Screenshot</th><th>Notes</th>
                <?php if (!$isBreachView && !$hideDetailsColumn): ?><th>See Full Details</th><?php endif; ?>
            </tr>
        </thead>
        <tbody>
        <?php foreach ($rows as $idx => $r): $t = $r['trade'];
            $entry = $t['actual_entry_price'] ?? $t['entry_price'];
            $sl = $t['actual_sl_price'] ?? $t['sl_price'];
            $tp = $t['tp_price'];
            $direction = $t['direction'] ?? 'long';

            // Item 2 (2026-08-06) — pips must reflect this trade's REAL
            // exit, signed so a loss always reads negative and a win
            // always reads positive, direction-aware. The old formula
            // was tp_price − entry unconditionally — the PLANNED
            // distance to target, not what actually happened, so a real
            // loss that hit stop loss displayed a positive number
            // (TP always sits on the profitable side of entry) and a
            // winning short displayed a negative one (TP sits below
            // entry for a short).
            $exitForPips = (($t['actual_exit_price'] ?? null) !== null && $t['actual_exit_price'] !== '') ? $t['actual_exit_price'] : (($r['pnl'] >= 0) ? $tp : $sl);
            if ($exitForPips !== null && $exitForPips !== '' && $entry !== null && $entry !== '') {
                $rawDiff = (float)$exitForPips - (float)$entry;
                if ($direction === 'short') $rawDiff = -$rawDiff;
                $pips = rnPipsOrPoints($t['instrument'], $rawDiff);
            } else {
                $pips = null;
            }

            $extraTpLevels = [];
            foreach ($tpExtraCols as $ci => $k) {
                if (($t[$k] ?? null) !== null && $t[$k] !== '') $extraTpLevels[] = ['n' => $ci + 2, 'price' => (float)$t[$k]];
            }
            $extraTpCount = count($extraTpLevels);
            $isBreachRow = $breachTradeId !== null && ($t['id'] ?? null) === $breachTradeId;

            // Item 6 — real losses use the account-preservation
            // classification computed above; wins/breakevens, and any
            // loss where no account context was supplied, keep the
            // existing stored trade_classification behavior. The one
            // exception: the row that actually ended the account always
            // reads Critical Risk, full stop — a real, confirmed edge
            // case found while verifying this fix live: an account can
            // breach for a reason that has nothing to do with drawdown
            // consumption at all (e.g. a real CONSISTENCY_RULE breach,
            // confirmed on a real account this session, where the
            // breaching trade's own balance never even dropped below
            // the account's starting balance) — the drawdown-ratio tier
            // would otherwise soften or mislabel the one row that
            // matters most, which is the same class of bug this item
            // exists to fix in the first place.
            if ($isBreachRow) {
                $stateLabel = 'Critical Risk'; $stateClass = 'rn-state-bad';
                $stateTooltip = 'This was the trade that ended the account.';
            } else {
                $ddInfo = $ddByIdx[$idx] ?? null;
                $preservationState = rnAccountPreservationState($r['pnl'], $ddInfo['ratio'] ?? null);
                if ($preservationState !== null) {
                    [$stateLabel, $stateClass] = $preservationState;
                    $stateTooltip = rnAccountPreservationTooltip($r['pnl'], $ddInfo['dollar'], $maxDrawdownDollar, $ddInfo['ratio']);
                } else {
                    [$stateLabel, $stateClass] = rnStateInfo($t['trade_classification'] ?? null);
                    $stateTooltip = rnClassificationTooltip($t['trade_classification'] ?? null, $r['pnl'], $t);
                }
            }

            // Item 13: Duration must come from MT5 reconciliation
            // (broker_open_time/broker_close_time, gated on is_reconciled),
            // never the app-level opened_at/closed_at timestamps — those
            // measure how long the Drafts workflow took, not how long the
            // position was actually held at the broker.
            $isReconciled = !empty($t['is_reconciled']);
            $durationMin = ($isReconciled && !empty($t['broker_open_time']) && !empty($t['broker_close_time']))
                ? (strtotime($t['broker_close_time']) - strtotime($t['broker_open_time'])) / 60
                : null;
            // 2026-08-19 fix: "Available after MT5 reconciliation" was a
            // static placeholder that stayed wrong forever for any trade
            // that never gets a statement uploaded against it — per
            // explicit instruction, an unreconciled trade now shows a
            // real, actionable link instead of a dead-end status string.
            $uploadHref = !empty($t['account_id']) ? '/traderalign/mt5-upload?account_id=' . urlencode($t['account_id']) : '/traderalign/mt5-upload';
            $durationDisplay = $isReconciled
                ? htmlspecialchars(rnFormatDuration($durationMin))
                : '<a href="' . htmlspecialchars($uploadHref) . '" class="rn-pending-note" title="Duration is sourced from MT5 broker reconciliation, not app timestamps. Upload this account\'s MT5 statement to populate it.">Upload broker data</a>';

            $rr = ($t['risk_reward_tp1'] !== null && $t['risk_reward_tp1'] !== '') ? '1 : ' . number_format((float)$t['risk_reward_tp1'], 2) : '-';
            $detailUrl = '/traderalign/journal?' . http_build_query(['view' => 'trades', 'trade_id' => $t['id']]);
            // A breach trade is the whole point of this specific render —
            // it must never be hidden behind "Show More" just because it
            // happens to sit past the initial-7 cutoff.
            $isExtra = $idx >= $initialVisible && !$isBreachRow;
            $tradeIdAttr = htmlspecialchars($t['id']);
            $noteVal = (string)($t['notes'] ?? '');
            $rowClasses = trim(($isExtra ? 'rn-row-extra' : '') . ($isBreachRow ? ' rn-row-breach' : ''));
        ?>
            <tr data-classification="<?= htmlspecialchars($t['trade_classification'] ?? '') ?>"<?= $rowClasses !== '' ? ' class="' . $rowClasses . '"' : '' ?><?= $isExtra ? ' style="display:none"' : '' ?>>
                <td><?= !empty($t['closed_at']) ? date('d M Y', strtotime($t['closed_at'])) : '-' ?></td>
                <td><?= !empty($t['instrument']) ? htmlspecialchars(strtoupper($t['instrument'])) : '-' ?></td>
                <td><?= htmlspecialchars($t['session'] ?? '-') ?></td>
                <td class="<?= ($t['direction'] ?? '') === 'long' ? 'positive' : 'negative' ?>"><?= ($t['direction'] ?? '') === 'long' ? 'Long' : 'Short' ?></td>
                <td><?= ($entry !== null && $entry !== '') ? number_format((float)$entry, 4) : '-' ?></td>
                <td><?= ($sl !== null && $sl !== '') ? number_format((float)$sl, 4) : '-' ?></td>
                <td>
                    <span class="rn-tp-cell">
                    <?= ($tp !== null && $tp !== '') ? number_format((float)$tp, 4) : '-' ?>
                    <?php if ($extraTpCount > 0): ?>
                        <span class="rn-tp-badge" data-expanded="0" title="<?= $extraTpCount ?> additional take-profit level<?= $extraTpCount === 1 ? '' : 's' ?> configured, click to expand" onclick="rnToggleTpExpand(this)">+<?= $extraTpCount ?></span>
                        <span class="rn-tp-expand" style="display:none">
                            <?php foreach ($extraTpLevels as $lvl): ?>
                            <span class="rn-tp-extra">TP<?= $lvl['n'] ?> <?= number_format($lvl['price'], 4) ?></span>
                            <?php endforeach; ?>
                        </span>
                    <?php endif; ?>
                    </span>
                </td>
                <td class="<?= $pips === null ? '' : ($pips >= 0 ? 'positive' : 'negative') ?>"><?= $pips === null ? '-' : (($pips >= 0 ? '+' : '') . $pips) ?></td>
                <td class="<?= pnlClass($r['pnl']) ?>"><?= pnlFormat($r['pnl']) ?></td>
                <td>$<?= number_format($r['balance'], 2) ?></td>
                <td><?= $rr ?></td>
                <td><?= $durationDisplay ?></td>
                <td><span class="rn-state <?= $stateClass ?>" title="<?= htmlspecialchars($stateTooltip) ?>"><?= htmlspecialchars($stateLabel) ?></span><?php if ($isBreachRow): ?> <span class="rn-badge rn-badge-red" title="The trade that ended this account.">BREACH TRADE</span><?php endif; ?></td>
                <td><span class="rn-view-btn rn-view-btn-disabled" title="View Trade: an interactive chart replay of this trade's real setup and execution is coming soon. This feature is currently under development.">🕒 Coming Soon</span></td>
                <?php $screenshotCount = (int)($t['screenshot_count'] ?? 0); ?>
                <td><span class="rn-screenshot-badge<?= $screenshotCount === 0 ? ' rn-screenshot-badge-empty' : '' ?>" data-trade-id="<?= $tradeIdAttr ?>" title="Upload or view screenshots for this trade" onclick="rnOpenScreenshotModal('<?= $tradeIdAttr ?>')"><?= $screenshotCount > 0 ? '📷 ' . $screenshotCount : '+ Add' ?></span></td>
                <td class="rn-notes-cell" data-trade-id="<?= $tradeIdAttr ?>">
                    <?php if ($isBreachView): ?>
                        <?php if ($noteVal !== ''): ?>
                        <span class="rn-notes-text-inner" title="<?= htmlspecialchars($noteVal) ?>"><?= rnTruncatedNoteHtml($noteVal, 90) ?></span>
                        <?php else: ?>
                        <span class="ta-empty-note">No notes were added.</span>
                        <?php endif; ?>
                    <?php else: ?>
                    <span class="rn-notes-display" style="<?= $noteVal === '' ? '' : 'display:none' ?>">
                        <button type="button" class="rn-note-add-btn" onclick="rnEditNote(this)" title="Add a note to this trade">+ Add Note</button>
                    </span>
                    <span class="rn-notes-display" style="<?= $noteVal === '' ? 'display:none' : '' ?>">
                        <span class="rn-notes-text-inner" title="<?= htmlspecialchars($noteVal) ?>"><?= $noteVal !== '' ? rnTruncatedNoteHtml($noteVal, 90) : '' ?></span>
                        <button type="button" class="rn-note-edit-btn" onclick="rnEditNote(this)" title="Edit note">✎</button>
                    </span>
                    <span class="rn-notes-edit" style="display:none">
                        <textarea class="rn-notes-input" maxlength="<?= RN_NOTE_MAX_LENGTH ?>" data-original="<?= htmlspecialchars($noteVal) ?>"><?= htmlspecialchars($noteVal) ?></textarea>
                        <div class="rn-notes-hint">Max <?= number_format(RN_NOTE_MAX_LENGTH) ?> characters (~300 words)</div>
                        <button type="button" class="rn-note-save-btn" onclick="rnSaveNote(this)">Save</button>
                        <button type="button" class="rn-note-cancel-btn" onclick="rnCancelNote(this)">Cancel</button>
                    </span>
                    <?php endif; ?>
                </td>
                <?php if (!$isBreachView && !$hideDetailsColumn): ?>
                <td><a href="<?= htmlspecialchars($detailUrl) ?>" class="rn-details-link">View Details ›</a></td>
                <?php endif; ?>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>
    </div>
    <?php if ($totalRows > $initialVisible): ?>
    <div class="rn-table-more-wrap">
        <button type="button" class="rn-show-more-btn" data-table="<?= htmlspecialchars($tableId) ?>" data-more-label="Show More (<?= $totalRows - $initialVisible ?> more)" onclick="rnToggleTableRows(this)">Show More (<?= $totalRows - $initialVisible ?> more)</button>
    </div>
    <?php endif; ?>
    <?php
}

/**
 * The ONE destination "View in Trades Table" from Lessons From Failure
 * lands on — a breached (or closed-at-a-loss) account's REAL, full trade
 * history, reusing the exact same renderRnTradeTable() component Active
 * Journeys' own phase/live cards use, per explicit instruction: the
 * table itself is never redesigned for this. What changes is only the
 * story around it — a past-tense header banner naming the real breach
 * reason/date, and the account's real final trade highlighted red via
 * renderRnTradeTable()'s own $breachTradeId param.
 *
 * Deliberately NOT reachable from Active Journeys' own account picker —
 * rnFetchActivePropAccounts()/rnFetchActiveLiveAccounts() still exclude
 * breached/closed accounts exactly as before; this view fetches the one
 * requested account directly via
 * TradingArchiveEngine::gatherAccountSegmentsById(), completely
 * bypassing those active-only fetchers, and is only ever linked to from
 * Lessons From Failure's own evidence table. No coaching, no goal
 * section, no account picker of its own, no present-tense recommendation
 * anywhere on this page — everything here is either past-tense narration
 * of a finished account or the reused, unmodified trade table.
 */
function renderBreachTradesView(string $userId, $db, string $accountId): void {
    $gathered = TradingArchiveEngine::gatherAccountSegmentsById($userId, $db, $accountId);
    $segments = $gathered['segments'];
    $type = $gathered['type'];

    if (empty($segments)) {
        ?>
        <div class="empty-state">
            <h3>Account not found</h3>
            <p style="font-size:.85rem">This account doesn't exist, doesn't belong to you, or has no closed trades yet. <a href="<?= htmlspecialchars(archiveUrl(['archive_lens' => 'failure'])) ?>">Back to Lessons From Failure</a></p>
        </div>
        <?php
        return;
    }

    $terminal = end($segments);
    $accountLabel = $segments[0]['account_label'];
    $rows = TradingArchiveEngine::flattenRows($segments);
    $n = count($rows);
    $breachTradeId = $n > 0 ? ($rows[$n - 1]['trade']['id'] ?? null) : null;

    $isBreached = $type === 'prop' ? $terminal['status'] === 'breached' : $terminal['status'] === 'closed_loss';
    $reasonLabel = TradingArchiveEngine::bucketOutcome($terminal['current_outcome'] ?? null);
    $endDate = $terminal['end_date'] ?? null;

    // Item 1 (2026-08-06) — the title itself now names exactly where the
    // breach happened (the phase-card title row below renders
    // $terminal['phase_label'] directly, in red), so a trader arriving
    // here doesn't have to go back and re-check Lessons From Failure to
    // know. $terminal['phase_label'] is already the real, correctly-
    // relabeled bucket (Phase 1/2/3, "Funded Account", or "Instant
    // Funding" — see buildAccountFailureJourney()'s own Instant Funding
    // relabeling comment for why that specific string is never the raw
    // "Current Phase" fallback).

    // Item 6 — same real drawdown-budget context rnAccountPreservationState()
    // needs, computed the same way Active Journeys' own phase/live cards
    // already pass it in (see those call sites' own comments). Prop:
    // the account's own real configured max-drawdown %, when this
    // account's rules actually declared one — never a fabricated
    // default (stays null, so the State column falls back to the stored
    // classification, exactly like every other "not specified" case in
    // this app). Live: the same informal 20%-of-starting-balance ceiling
    // this file already uses elsewhere for personal accounts.
    $stateAccountSize = (float)$terminal['account_size'];
    $isInstantFunding = $type === 'prop' && ($terminal['challenge_type'] ?? null) === 'instant_funding';
    $maxDdPctForState = $type === 'prop' ? ($terminal['max_dd_pct_configured'] ?? null) : 20.0;
    // Maximum Loss Model (2026-08-17) — same real ddReferenceBalance used
    // below for rnProgramConfigSummary(); computed once here so this
    // State-column drawdown-budget figure (fed into renderRnTradeTable()
    // a few lines down) and the Program Configuration card's own Maximum
    // Drawdown/Breach Level chips can never disagree with each other.
    $stateDdReference = rnDdReferenceBalance($terminal['rules'] ?? [], $stateAccountSize, $terminal['rows'] ?? []);
    $stateMaxDrawdownDollar = ($maxDdPctForState !== null && $maxDdPctForState > 0) ? $stateDdReference * $maxDdPctForState / 100 : null;

    // 2026-08-06 — the same shared, correctly-separated Program
    // Configuration summary every other stats line on this page now
    // uses (rnProgramConfigSummary()) — Maximum Drawdown ($400 on a
    // $5,000/8% account) and Breach Level / Minimum Equity ($4,600) are
    // two different values and must never share one label again.
    // Instant Funding has no real profit-target key (see
    // buildAccountFailureJourney()'s own guard against fabricating one)
    // and live accounts have no formal profit-target concept at all —
    // both correctly render "Not specified" rather than a guess. Live's
    // Maximum Drawdown/Breach Level use the same informal 20% ceiling
    // this file already uses elsewhere for personal accounts, not a
    // real declared rule — that's what "informal" in this comment means,
    // not a claim this account actually has one.
    // §1.2 fix (2026-08-10) — this used to call rnPhaseTargets() and take
    // its always-populated (8/5/5-defaulted) 'profit_target_pct' with a
    // no-op '?? null' (rnPhaseTargets() never itself returns null there),
    // which silently contradicted this very comment block's own stated
    // intent ("a finished account's own record should never imply a
    // rule it didn't actually have"). Now reads the real 'configured'
    // flag rnPhaseTargets() exposes and only passes a value through when
    // it was genuinely declared — a finished/breached account with no
    // real target on record correctly shows "Not specified" again.
    $profitTargetPctForConfig = null;
    if ($type === 'prop' && !$isInstantFunding && function_exists('rnPhaseTargets')) {
        $rules = $terminal['rules'] ?? [];
        $resolvedTargets = rnPhaseTargets($rules, $terminal['phase_label']);
        $profitTargetPctForConfig = !empty($resolvedTargets['profit_target_configured']) ? $resolvedTargets['profit_target_pct'] : null;
    }
    // Maximum Loss Model (2026-08-17) — $stateDdReference (computed above,
    // Trailing's real Breach Level anchored to this terminal segment's
    // own real peak balance) reused here too, so this card's chips and
    // the trade table's State column above can never disagree.
    $cfg = rnProgramConfigSummary($stateAccountSize, $profitTargetPctForConfig, $maxDdPctForState, true, true, $stateDdReference);

    // Item 4 (2026-08-06) — the breach banner addresses the trader by their
    // real name, the same user_profiles.display_name every other
    // personalized surface in this app reads (Trader Profile's "Dear
    // {name}," letter, onboarding's own fallback) — falls back to
    // 'Trader' when a legacy account never set one, matching that exact
    // established precedent rather than inventing a new fallback string.
    $traderDisplayName = 'Trader';
    try {
        $nameRow = $db->raw("SELECT display_name FROM user_profiles WHERE user_id = :uid", [':uid' => $userId]);
        if (!empty($nameRow) && !empty($nameRow[0]['display_name'])) $traderDisplayName = $nameRow[0]['display_name'];
    } catch (\Throwable $e) { /* stays 'Trader' */ }
    ?>
    <div class="rn-phase-card <?= $isBreached ? 'rn-card-breached' : '' ?>">
        <div class="rn-phase-head">
            <div class="rn-phase-head-left">
                <div class="rn-phase-title-row">
                    <h2 class="rn-phase-title">
                        <?php if ($isBreached && $type === 'prop'): ?>
                        <span style="color:var(--rn-red)"><?= htmlspecialchars($terminal['phase_label']) ?></span> - <?= htmlspecialchars($accountLabel) ?>
                        <?php elseif ($isBreached): ?>
                        <span style="color:var(--rn-red)">Closed</span> - <?= htmlspecialchars($accountLabel) ?>
                        <?php else: ?>
                        <?= htmlspecialchars($accountLabel) ?>
                        <?php endif; ?>
                    </h2>
                    <?php if ($isBreached): ?><span class="rn-badge rn-badge-red"><?= $type === 'live' ? 'CLOSED AT A LOSS' : 'BREACHED' ?></span><?php endif; ?>
                </div>
                <div class="rn-phase-stats-line">
                    <?php
                    $finalBalanceValue = '$' . number_format($n > 0 ? end($rows)['balance'] : $stateAccountSize, 2);
                    if ($type === 'prop') {
                        $btvExtra = [
                            ['label' => 'Final Balance', 'value' => $finalBalanceValue, 'sub' => null, 'tone' => $isBreached ? 'negative' : null],
                        ];
                        if ($isBreached) {
                            $btvExtra[] = ['label' => 'Breached On', 'value' => $endDate ? date('d M Y', strtotime($endDate)) : '-', 'sub' => null, 'tone' => null];
                        }
                        echo rnProgramConfigCards($cfg, $btvExtra);
                    } else {
                    ?>
                    <?php // Live accounts have no formally declared drawdown rule at
                    // all (only the informal 20%-of-starting-balance reference this
                    // file uses elsewhere to feed the State column's classification)
                    // — showing a "Maximum Drawdown"/"Breach Level" figure here would
                    // itself invite exactly the kind of confusion this fix exists to
                    // remove, so this stays a plain, honest chip summary instead. ?>
                    <div class="rn-config-grid">
                        <div class="rn-config-chip"><div class="rn-config-chip-label">Start Balance</div><div class="rn-config-chip-value">$<?= number_format($stateAccountSize, 2) ?></div></div>
                        <div class="rn-config-chip<?= $isBreached ? ' rn-config-chip-negative' : '' ?>"><div class="rn-config-chip-label">Final Balance</div><div class="rn-config-chip-value"><?= $finalBalanceValue ?></div></div>
                        <div class="rn-config-chip"><div class="rn-config-chip-label">Trades Taken</div><div class="rn-config-chip-value"><?= $n ?></div></div>
                        <?php if ($isBreached): ?>
                        <div class="rn-config-chip"><div class="rn-config-chip-label">Closed On</div><div class="rn-config-chip-value"><?= $endDate ? date('d M Y', strtotime($endDate)) : '-' ?></div></div>
                        <?php endif; ?>
                    </div>
                    <?php } ?>
                </div>
            </div>
            <div class="rn-phase-head-right">
                <a href="<?= htmlspecialchars(archiveUrl(['archive_type' => $type, 'archive_lens' => 'failure', 'archive_account' => $accountId])) ?>" class="rn-btn-outline">← Back to Lessons From Failure</a>
            </div>
        </div>
        <?php if ($isBreached):
            // Item 4 (2026-08-06) — rewritten to address the trader by name
            // and lead with the real reason first, never describing its own
            // grammatical tense to the reader (a real, reported mistake from
            // the prior pass — "told in the past tense" was meant as an
            // internal build note, not copy for the page).
            $breachVerb = $type === 'live' ? 'closed at a loss' : 'breached';
            $causedVerb = $type === 'live' ? 'closed the account' : 'caused the breach';
        ?>
        <div class="rn-breached-banner" style="margin:0 1.6rem 1.35rem;border-radius:var(--radius-lg)">
            <span class="rn-trophy">⛔</span>
            <div>
                <div class="rn-breached-title">Unfortunately <?= htmlspecialchars($traderDisplayName) ?>, this account <?= $breachVerb ?> because of <?= htmlspecialchars($reasonLabel) ?>.</div>
                <div class="rn-breached-detail">Below is the account's complete trade history, from its first trade to its last. The row highlighted in red is the trade that <?= $causedVerb ?>. Everything above it is the real context that led there.</div>
            </div>
        </div>
        <?php endif; ?>
    </div>
    <div class="rn-trade-journal">
        <h3 class="ta-card-h3">Final Trades Before <?= $type === 'live' ? 'Closure' : 'Breach' ?> <span class="ta-card-h3-note">(Full Trade History)</span></h3>
        <?php renderRnTradeTable($rows, 'rn-breach-full-table', false, $breachTradeId, $stateAccountSize, $stateMaxDrawdownDollar, true, $stateDdReference); ?>
    </div>
    <?php
}

/** One Phase N card — passed (green), active (blue), or breached (red). */
function renderRnPhaseCard(array $account, array $phase): void {
    $meta = [
        'passed'   => ['PASSED', 'rn-badge-green', 'rn-card-passed'],
        'active'   => ['ACTIVE', 'rn-badge-blue', 'rn-card-active'],
        'breached' => ['BREACHED', 'rn-badge-red', 'rn-card-breached'],
        'closed'   => ['CLOSED', 'rn-badge-neutral', 'rn-card-closed'],
    ][$phase['status']] ?? ['-', 'rn-badge-neutral', ''];
    [$badgeText, $badgeClass, $cardClass] = $meta;
    $pct = min(100, max(0, $phase['progress_pct']));
    // 2026-08-06 — real, reported bug fixed here: this line previously
    // computed the real $4,600 breach FLOOR correctly but labeled it
    // "Max Drawdown," which on that same $5,000/8% account is actually
    // $400. rnProgramConfigSummary() is the one shared place both this
    // fact and its correct label now come from — see that function's
    // own doc comment.
    // Maximum Loss Model (2026-08-17) — this phase's own real
    // ddReferenceBalance (account_size for Fixed, this phase's own real
    // peak balance for Trailing — rnDdReferenceBalance(), reading
    // $phase['rules']/$phase['rows'], both additive on rnBuildPhaseCardFromRows()'s
    // return shape).
    $phaseDdReference = rnDdReferenceBalance($phase['rules'] ?? [], $phase['account_size'], $phase['rows'] ?? []);
    $cfg = rnProgramConfigSummary($phase['account_size'], $phase['profit_target_pct'], $phase['max_dd_pct'], $phase['profit_target_configured'] ?? true, $phase['max_dd_configured'] ?? true, $phaseDdReference);
    ?>
    <div class="rn-phase-card <?= $cardClass ?>">
        <div class="rn-phase-head">
            <div class="rn-phase-head-left">
                <div class="rn-phase-title-row">
                    <h2 class="rn-phase-title"><?= htmlspecialchars($phase['label']) ?> - <?= htmlspecialchars($account['label']) ?></h2>
                    <span class="rn-badge <?= $badgeClass ?>"><?= $badgeText ?></span>
                </div>
                <div class="rn-phase-stats-line">
                    <?php
                    $phaseExtra = [];
                    if ($phase['status'] === 'passed' && $phase['passed_on']) {
                        $phaseExtra[] = ['label' => 'Passed On', 'value' => date('d M Y', strtotime($phase['passed_on'])), 'sub' => null, 'tone' => 'positive'];
                    }
                    echo rnProgramConfigCards($cfg, $phaseExtra);
                    ?>
                </div>
            </div>
            <div class="rn-phase-head-right">
                <div class="rn-metric-ring">
                    <!-- Wave 6 (Single Source of Truth migration, §Wave 6): text
                         label now reads the same capped $pct the ring's own
                         --pct fill already uses, instead of the raw uncapped
                         $phase['progress_pct'] — a trader who overshot their
                         profit target previously saw a visually-full 100% ring
                         next to a "137.5%" text label, an internally
                         inconsistent display. Capped at 100%, matching every
                         other progress display in the app (e.g. Trading
                         Archive's Pass Rate/Goal Achievement donuts). -->
                    <div class="rn-ring" style="--pct:<?= $pct ?>"><span><?= round($pct, 1) ?>%</span></div>
                    <div class="rn-metric-label">Progress to Pass</div>
                </div>
                <div class="rn-metric"><div class="rn-metric-value">$<?= number_format($phase['remaining'], 2) ?></div><div class="rn-metric-label">Remaining To Target</div></div>
                <div class="rn-metric">
                    <div class="rn-metric-value <?= $phase['avg_win'] !== null ? 'positive' : 'rn-metric-waiting' ?>"><?= $phase['avg_win'] !== null ? '+$' . number_format($phase['avg_win'], 2) : 'No winning trades recorded yet' ?></div>
                    <div class="rn-metric-label" title="Average profit across this phase's own winning trades so far.">Average Winning Trade</div>
                </div>
                <div class="rn-metric">
                    <?php
                    // Item 1 — a passed phase and "Expected Wins Needed" are
                    // mutually exclusive facts about the SAME metric; showing
                    // both at once ("Target already reached" next to a wins-
                    // needed label) reads as contradictory. Exactly one state
                    // renders now: a passed phase replaces the whole tile
                    // (label included) with a single success state, never
                    // both a success message AND a "needed" label together.
                    ?>
                    <?php if ($phase['status'] === 'passed'): ?>
                    <div class="rn-metric-value positive">✓ Target Reached</div>
                    <div class="rn-metric-label">Progress to Pass</div>
                    <?php elseif (in_array($phase['status'], ['breached', 'closed'], true)): ?>
                    <!-- §1.5 fix (2026-08-10) — a breached/closed attempt is
                         over; showing "N more wins needed" (or the "at least
                         one win" waiting state, which also implies the
                         attempt is still in progress) would misrepresent an
                         ended attempt as one still being pursued. -->
                    <div class="rn-metric-value rn-metric-waiting">Attempt Ended</div>
                    <div class="rn-metric-label">Estimated Wins Remaining</div>
                    <?php elseif ($phase['expected_wins_needed'] !== null): ?>
                    <div class="rn-metric-value"><?= $phase['expected_wins_needed'] ?></div>
                    <div class="rn-metric-label" title="Estimated additional winning trades needed to close the remaining distance to target, at this phase's own current average win.">Estimated Wins Remaining</div>
                    <?php else: ?>
                    <div class="rn-metric-value rn-metric-waiting">Estimate needs at least one win</div>
                    <div class="rn-metric-label">Estimated Wins Remaining</div>
                    <?php endif; ?>
                </div>
            </div>
        </div>
        <?php // Maximum Loss Model (2026-08-17) — $cfg['max_drawdown_dollar']
        // (already model-aware, computed a few lines above) replaces the
        // raw $phase['max_drawdown_amount'] here so this table's per-row
        // State/drawdown context can never disagree with this same card's
        // own Maximum Drawdown chip. ?>
        <?php renderRnTradeTable($phase['display_rows'] ?? $phase['rows'], 'rn-table-' . preg_replace('/[^a-z0-9]+/', '-', strtolower($phase['label'])), isset($phase['display_rows']), null, $phase['account_size'], $cfg['max_drawdown_dollar'], false, $phaseDdReference); ?>
        <?php if ($phase['status'] === 'passed'): $msg = rnPhasePassedMessage($phase['label']); ?>
        <div class="rn-passed-banner">
            <span class="rn-trophy">🏆</span>
            <div>
                <div class="rn-passed-title"><?= htmlspecialchars($msg['title']) ?> on <?= $phase['passed_on'] ? date('d M Y', strtotime($phase['passed_on'])) : '-' ?></div>
                <div class="rn-passed-detail"><?= htmlspecialchars($msg['body']) ?></div>
            </div>
        </div>
        <?php elseif ($phase['status'] === 'breached'): $msg = rnPhaseBreachedMessage($phase['label']); ?>
        <div class="rn-breached-banner">
            <span class="rn-trophy">⛔</span>
            <div>
                <div class="rn-breached-title"><?= htmlspecialchars($msg['title']) ?></div>
                <div class="rn-breached-detail"><?= htmlspecialchars($msg['body']) ?></div>
            </div>
        </div>
        <?php endif; ?>
    </div>
    <?php
}

/** Tallies real trade_classification values across a set of phase/segment rows — feeds Current Goal coaching (item 1) without a second query, reusing the same rows already fetched for the trade table. */
function rnClassificationCounts(array $rows): array {
    $counts = ['disciplined_win' => 0, 'controlled_loss' => 0, 'breakeven' => 0, 'elevated_win' => 0, 'loss_high_risk' => 0, 'tilt_warning' => 0, 'revenge_trade' => 0];
    $total = 0;
    foreach ($rows as $r) {
        $c = $r['trade']['trade_classification'] ?? null;
        if ($c === null || !isset($counts[$c])) continue;
        $counts[$c]++;
        $total++;
    }
    return ['counts' => $counts, 'total' => $total];
}

/** Item 6 — full noun phrase for a classification, used as the pluralizable label inside a filter chip (distinct from rnStateInfo()'s compact table-badge text). */
function rnClassificationNoun(?string $classification): string {
    return match ($classification) {
        'disciplined_win' => 'Disciplined Win', 'controlled_loss' => 'Controlled Loss', 'breakeven' => 'Breakeven Trade',
        'elevated_win' => 'Elevated Win', 'loss_high_risk' => 'Oversized Loss', 'tilt_warning' => 'Tilt Warning Trade',
        'revenge_trade' => 'Revenge Trade', default => 'Trade',
    };
}

/**
 * Item 6 — every place coaching text cites a specific trade classification
 * count (e.g. "3 Revenge Trades", "1 Tilt Warning trade") gets a real
 * clickable filter, not just a number to trust blindly. Renders nothing
 * when count is 0 (nothing to click through to). $tableId scopes the
 * filter to one specific rendered <table> (a phase/segment's own table,
 * right below it); omit it to filter every rn-table on the page at once
 * (used by the Full Coaching Analysis workspace, whose counts span the
 * WHOLE account across every phase/segment table, not just one).
 */
function rnFilterChip(?string $classification, int $count, string $label, ?string $tableId = null): string {
    if ($count <= 0 || $classification === null) return '';
    $countLabel = $count . ' ' . $label . ($count === 1 ? '' : 's');
    $tableAttr = $tableId !== null ? htmlspecialchars($tableId, ENT_QUOTES) : '';
    return ' <button type="button" class="rn-filter-chip" onclick="rnFilterTrades(\'' . htmlspecialchars($classification, ENT_QUOTES) . '\', \'' . htmlspecialchars($countLabel, ENT_QUOTES) . '\', \'' . $tableAttr . '\')">View ' . htmlspecialchars($countLabel) . ' →</button>';
}

/**
 * Item 1 — "Current Goal" as a real, persisted feature: empty state, a
 * modal-driven Set Goal action, live-recomputed progress, and coaching
 * built from this account's own real numbers. Shared between the Funded
 * prop card and the Live Account card (item 8) — only the caller's
 * $currentBalance/$avgWin/$coachCtx differ; the section itself, the
 * modal, and the persistence are identical for both.
 */
function renderRnGoalSection(string $userId, $db, array $account, float $currentBalance, ?float $avgWin, array $coachCtx): void {
    $accountId = $account['id'];
    $goal = rnFetchActiveGoal($accountId, $userId, $db);
    $balanceAttr = htmlspecialchars(number_format($currentBalance, 2, '.', ''));
    // Item 11 — the header action button is context-aware: no goal yet ->
    // nothing here (the empty state below has its own "+ Set Goal"); a goal
    // in progress -> "Edit Goal"; a goal already reached -> "Create Next
    // Goal", so the label always matches what clicking it actually does.
    $progress = $goal ? rnGoalProgress($goal, $currentBalance, $avgWin) : null;
    $headerBtnLabel = ($progress && $progress['achieved']) ? 'Create Next Goal' : 'Edit Goal';
    ?>
    <div class="rn-goal-section">
        <div class="rn-goal-head">
            <div class="rn-goal-title">🎯 Growth Target</div>
            <?php if ($goal): ?>
            <button type="button" class="rn-btn-outline" onclick="rnOpenGoalModal('<?= htmlspecialchars($accountId) ?>', '<?= $balanceAttr ?>')"><?= htmlspecialchars($headerBtnLabel) ?></button>
            <?php endif; ?>
        </div>
        <?php if (!$goal): ?>
        <div class="rn-goal-empty">
            <div>
                <div class="rn-goal-empty-title">No personal goal has been set yet.</div>
                <div class="rn-goal-empty-sub">Set a target balance to help track your long-term account growth.</div>
            </div>
            <button type="button" class="rn-btn-gold" onclick="rnOpenGoalModal('<?= htmlspecialchars($accountId) ?>', '<?= $balanceAttr ?>')">+ Set Goal</button>
        </div>
        <?php else:
            $coaching = rnGoalCoachingText($progress, $coachCtx);
            $pct = min(100, max(0, $progress['pct']));
        ?>
        <div class="rn-goal-body">
            <div class="rn-goal-progress-row">
                <div>
                    <div class="rn-goal-reach-label">Target Balance</div>
                    <div class="rn-goal-reach">$<?= number_format($progress['target'], 2) ?></div>
                    <?php if (!empty($goal['reason'])): ?><div class="rn-goal-reason">Your reason: "<?= htmlspecialchars($goal['reason']) ?>"</div><?php endif; ?>
                </div>
                <div class="rn-goal-pct <?= $progress['achieved'] ? 'positive' : '' ?>"><?= round($pct, 1) ?>% Complete</div>
            </div>
            <div class="rn-goal-bar"><div class="rn-goal-bar-fill" style="width:<?= $pct ?>%"></div></div>
            <?php // Item 12 — the dollar amount remaining sits right next to the bar, not buried a scroll away in the stats grid below. ?>
            <div class="rn-goal-remaining-note">
                <?php if ($progress['achieved']): ?>
                <span class="positive">🎉 Goal reached</span>
                <?php else: ?>
                $<?= number_format($progress['remaining'], 2) ?> Remaining To Goal
                <?php endif; ?>
            </div>
            <div class="rn-goal-stats">
                <div class="rn-goal-stat"><div class="rn-goal-stat-value">$<?= number_format($currentBalance, 2) ?></div><div class="rn-goal-stat-label" title="This account's current real balance right now.">Current Balance</div></div>
                <?php // Item 1 — once the goal is achieved, "remaining" and "wins
                      // needed" are both zero by definition; showing "$0.00" and
                      // "0" next to labels that imply something is still owed
                      // reads as contradictory. Both tiles collapse into one
                      // shared success state instead, so only one state is ever
                      // visible for an achieved goal, same as the phase card
                      // above. ?>
                <?php if ($progress['achieved']): ?>
                <div class="rn-goal-stat"><div class="rn-goal-stat-value positive">✓ Reached</div><div class="rn-goal-stat-label">Remaining To Goal</div></div>
                <div class="rn-goal-stat"><div class="rn-goal-stat-value positive">✓ Goal Achieved</div><div class="rn-goal-stat-label">Estimated Wins Remaining</div></div>
                <?php else: ?>
                <div class="rn-goal-stat"><div class="rn-goal-stat-value">$<?= number_format($progress['remaining'], 2) ?></div><div class="rn-goal-stat-label" title="Dollar distance between the current balance and the target balance above.">Remaining To Goal</div></div>
                <div class="rn-goal-stat"><div class="rn-goal-stat-value"><?= $progress['expected_wins_needed'] !== null ? $progress['expected_wins_needed'] : '<span class="rn-metric-waiting">Needs at least one win to estimate</span>' ?></div><div class="rn-goal-stat-label" title="Estimated additional winning trades needed to close the remaining distance, at this account's own current average winning trade.">Estimated Wins Remaining</div></div>
                <?php endif; ?>
                <div class="rn-goal-stat"><div class="rn-goal-stat-value"><?= $avgWin !== null ? '+$' . number_format($avgWin, 2) : '<span class="rn-metric-waiting">No winning trades recorded yet</span>' ?></div><div class="rn-goal-stat-label" title="Average profit across this account's own winning trades so far, the figure the wins-remaining estimate above is based on.">Average Winning Trade</div></div>
            </div>
            <?php if ($progress['achieved']): ?>
            <div class="rn-goal-achieved-banner">🎉 Goal reached! Use "Create Next Goal" above to keep building.</div>
            <?php endif; ?>
            <?php
            // Item 6 — every coaching row that cites a specific trade
            // classification count gets a real "View N Trades →" filter
            // chip, scoped to this exact card's own table (rnFilterChip()
            // renders nothing when the count is 0 — a row that didn't cite
            // a specific count gets no chip).
            $flagTableId = $coachCtx['table_id'] ?? null;
            ?>
            <div class="rn-goal-coaching">
                <div class="rn-goal-coach-row"><span class="rn-goal-coach-label rn-goal-coach-bad">🔥 Highest Priority</span><p><?= htmlspecialchars($coaching['highest_priority']) ?><?= rnFilterChip($coaching['flag_classification'], $coaching['flag_count'], rnClassificationNoun($coaching['flag_classification']), $flagTableId) ?></p></div>
                <div class="rn-goal-coach-row"><span class="rn-goal-coach-label rn-goal-coach-good">✅ Keep Doing</span><p><?= htmlspecialchars($coaching['keep_doing']) ?><?= rnFilterChip($coaching['keep_classification'], $coaching['keep_count'], rnClassificationNoun($coaching['keep_classification']), $flagTableId) ?></p></div>
                <div class="rn-goal-coach-row"><span class="rn-goal-coach-label rn-goal-coach-warn">⚠ Focus On Improving</span><p><?= htmlspecialchars($coaching['focus_improve']) ?><?= rnFilterChip($coaching['flag_classification'], $coaching['flag_count'], rnClassificationNoun($coaching['flag_classification']), $flagTableId) ?></p></div>
                <div class="rn-goal-coach-row"><span class="rn-goal-coach-label rn-goal-coach-good">📈 Doing Well</span><p><?= htmlspecialchars($coaching['doing_well']) ?></p></div>
                <div class="rn-goal-coach-row"><span class="rn-goal-coach-label">🛡 Consistency Impact</span><p><?= htmlspecialchars($coaching['consistency_impact']) ?></p></div>
            </div>
        </div>
        <?php endif; ?>
    </div>
    <?php
}

/** The gold-accented Funded Account card, including its bottom summary strip. */
function renderRnFundedCard(string $userId, $db, array $account, array $phase, array $rules): void {
    // Item 6 — computed once and reused for both this card's own trade
    // table AND the Goal section's filter chips below it, so a chip can
    // never point at a table id that doesn't match the table actually
    // rendered.
    $tableId = 'rn-table-' . preg_replace('/[^a-z0-9]+/', '-', strtolower($phase['label']));
    $statusLabel = match ($phase['status']) { 'breached' => 'BREACHED', 'closed' => 'CLOSED', default => 'ACTIVE' };
    $statusBadgeClass = match ($phase['status']) { 'breached' => 'rn-badge-red', 'closed' => 'rn-badge-neutral', default => 'rn-badge-gold' };
    // Reverted (2026-08-07) — this card's own "Current Balance" is
    // deliberately scoped to THIS segment (account_size + this segment's
    // own trades' P&L), matching how a funded account genuinely starts
    // fresh at its own account_size the moment it's funded (same real
    // per-promotion reset every phase gets — verified live). A same-day
    // attempt to switch this to accountCurrentBalance() (a
    // src/RenderHelpers.php function that read the whole-account, never-
    // resetting trading_accounts.current_balance directly — since deleted
    // in the 2026-08-08 Single Source of Truth migration, Wave 5f, for
    // exactly this reason) was reverted after live testing showed it
    // broke real trade-sizing math for accounts still mid-evaluation. See
    // rnBuildPhaseCardFromRows()'s own doc comment for the full history.
    $currentBalance = $phase['account_size'] + $phase['total_profit'];
    $returnPct = $phase['account_size'] > 0 ? ($phase['total_profit'] / $phase['account_size']) * 100 : 0.0;

    $activeGoal = rnFetchActiveGoal($account['id'], $userId, $db);
    // 2026-08-06 — a funded account's own real drawdown rule wasn't
    // shown anywhere on this card before; now uses the same shared,
    // correctly-labeled Maximum Drawdown / Breach Level (Minimum Equity)
    // pair every other program-configuration display on this page uses
    // (rnProgramConfigSummary()) — never the raw floor mislabeled as the
    // loss amount, or vice versa.
    // Maximum Loss Model (2026-08-17) — same phase-scoped ddReferenceBalance
    // resolution as renderRnPhaseCard() above (a Funded segment is just
    // another phase card from the same rnBuildPhaseCardFromRows() shape).
    $fundedDdReference = rnDdReferenceBalance($phase['rules'] ?? [], $phase['account_size'], $phase['rows'] ?? []);
    $cfg = rnProgramConfigSummary($phase['account_size'], $phase['profit_target_pct'] ?? null, $phase['max_dd_pct'] ?? null, $phase['profit_target_configured'] ?? true, $phase['max_dd_configured'] ?? true, $fundedDdReference);

    // 2026-08-19 fix: this card's own $coachCtx passed to
    // renderRnGoalSection() below has always read a bare $consistency
    // variable (see rnGoalCoachingText()'s own "Consistency Impact"
    // paragraph) that was never actually computed anywhere in this
    // function, a real pre-existing "Undefined variable" warning. Same
    // real Consistency Score already shown on the Active Coaching card
    // (renderRnCoachingSection()), computed the same way, not a second
    // formula.
    $consistency = null;
    try {
        $cs = PropFirmRuleEngine::consistencyStatus($account['id']);
        $consistency = !empty($cs['score_available']) ? $cs['current_score_pct'] : null;
    } catch (\Throwable $e) { $consistency = null; }
    ?>
    <div class="rn-phase-card rn-card-funded">
        <div class="rn-phase-head">
            <div class="rn-phase-head-left">
                <div class="rn-phase-title-row">
                    <h2 class="rn-phase-title">Funded Account - <?= htmlspecialchars($account['label']) ?></h2>
                    <span class="rn-badge <?= $statusBadgeClass ?>"><?= $statusLabel ?></span>
                </div>
                <div class="rn-phase-stats-line">
                    <?php
                    $fundedExtra = [
                        ['label' => 'Current Balance', 'value' => '$' . number_format($currentBalance, 2), 'sub' => null, 'tone' => $currentBalance >= $phase['account_size'] ? 'positive' : 'negative'],
                    ];
                    if (!empty($phase['start_date'])) {
                        $fundedExtra[] = ['label' => 'Funded On', 'value' => date('d M Y', strtotime($phase['start_date'])), 'sub' => null, 'tone' => null];
                    }
                    echo rnProgramConfigCards($cfg, $fundedExtra);
                    ?>
                </div>
            </div>
            <div class="rn-phase-head-right">
                <div class="rn-metric"><div class="rn-metric-value">$<?= number_format($currentBalance, 2) ?></div><div class="rn-metric-label">Current Balance</div></div>
                <div class="rn-metric"><div class="rn-metric-value <?= pnlClass($phase['total_profit']) ?>"><?= pnlFormat($phase['total_profit']) ?></div><div class="rn-metric-label">Total Profit</div></div>
                <div class="rn-metric rn-goal-metric">
                    <?php if ($activeGoal): $gp = rnGoalProgress($activeGoal, $currentBalance, $phase['avg_win']); ?>
                    <div class="rn-metric-value"><?= $gp['achieved'] ? '✓ Reached' : '🎯 Reach' ?> $<?= number_format($gp['target'], 0) ?><br><span class="rn-goal-metric-pct"><?= round($gp['pct']) ?>% Complete</span></div>
                    <?php else: ?>
                    <div class="rn-metric-value rn-metric-waiting">No target balance set yet, set one below</div>
                    <?php endif; ?>
                    <div class="rn-metric-label" title="Your own personal target balance for this account, set below, separate from the prop firm's own profit target above.">Growth Target</div>
                </div>
            </div>
        </div>
        <?php // Maximum Loss Model (2026-08-17) — $cfg['max_drawdown_dollar']
        // (model-aware, computed above) replaces the raw
        // $phase['max_drawdown_amount'], same reasoning as renderRnPhaseCard(). ?>
        <?php renderRnTradeTable($phase['display_rows'] ?? $phase['rows'], $tableId, isset($phase['display_rows']), null, $phase['account_size'], $cfg['max_drawdown_dollar'], false, $fundedDdReference); ?>
        <div class="rn-funded-strip">
            <div><span class="rn-funded-strip-label">Funded Status:</span> <span class="rn-badge <?= $statusBadgeClass ?>"><?= $statusLabel ?></span></div>
            <div><span class="rn-funded-strip-label">Total Profit:</span> <span class="<?= pnlClass($phase['total_profit']) ?>"><?= pnlFormat($phase['total_profit']) ?> (<?= ($returnPct >= 0 ? '+' : '') . number_format($returnPct, 2) ?>%)</span></div>
            <div><span class="rn-funded-strip-label">Win Rate:</span> <?= number_format($phase['win_rate'], 2) ?>%</div>
            <div><span class="rn-funded-strip-label">Trades Taken:</span> <?= $phase['trade_count'] ?></div>
            <div><span class="rn-funded-strip-label">Best Trade:</span> <span class="<?= $phase['best_trade'] !== null ? 'positive' : 'rn-metric-waiting' ?>"><?= $phase['best_trade'] !== null ? pnlFormat($phase['best_trade']) : 'No winning trades recorded yet' ?></span></div>
            <div><span class="rn-funded-strip-label"><?= $phase['streak'] > 0 ? 'Current Winning Streak:' : ($phase['streak'] < 0 ? 'Current Losing Streak:' : 'Current Streak:') ?></span> <span class="<?= $phase['streak'] >= 0 ? 'positive' : 'negative' ?>"><?= $phase['streak'] === 0 ? 'None yet' : abs($phase['streak']) . ' ' . ($phase['streak'] > 0 ? 'Wins' : 'Losses') ?></span></div>
        </div>
        <?php if ($phase['status'] === 'breached'): $msg = rnFundedBreachedMessage(); ?>
        <div class="rn-breached-banner">
            <span class="rn-trophy">⛔</span>
            <div>
                <div class="rn-breached-title"><?= htmlspecialchars($msg['title']) ?></div>
                <div class="rn-breached-detail"><?= htmlspecialchars($msg['body']) ?></div>
            </div>
        </div>
        <?php endif; ?>
    </div>
    <?php
    $cls = rnClassificationCounts($phase['rows']);
    renderRnGoalSection($userId, $db, $account, $currentBalance, $phase['avg_win'], [
        'win_rate' => $phase['win_rate'], 'streak' => $phase['streak'],
        'disciplined_count' => $cls['counts']['disciplined_win'], 'tilt_count' => $cls['counts']['tilt_warning'],
        'revenge_count' => $cls['counts']['revenge_trade'], 'high_risk_loss_count' => $cls['counts']['loss_high_risk'],
        'consistency' => $consistency, 'classified_total' => $cls['total'], 'table_id' => $tableId,
    ]);
}

/**
 * Risk Panel (2026-08-17) — replaces the old merged "Coaching
 * Recommendation" / "Recommended Risk" / "Until You Recover To" tiles
 * inside Active Coaching with 3 deliberately independent cards (see
 * src/RiskPanelEngine.php's own header comment for the full reasoning).
 * This function only renders — every number it shows comes straight from
 * the $card arrays RiskPanelEngine already computed; nothing here does
 * its own math.
 */
function renderRnRiskPanel(array $overallCard, array $streakCard, array $dailyCard, string $accountId): void {
    $cards = [$overallCard, $streakCard, $dailyCard];
    ?>
    <div class="rp-panel">
        <?php foreach ($cards as $card): ?>
        <div class="rp-card rp-color-<?= $card['status_color'] ?>">
            <div class="rp-card-head">
                <span class="rp-card-title"><?= htmlspecialchars($card['title']) ?></span>
                <span class="rp-pill rp-pill-<?= $card['status_color'] ?>"><?= htmlspecialchars($card['status_label']) ?></span>
            </div>
            <div class="rp-big"><?= htmlspecialchars($card['big_value']) ?><?php if (!empty($card['big_unit'])): ?> <span class="rp-big-unit"><?= htmlspecialchars($card['big_unit']) ?></span><?php endif; ?></div>
            <div class="rp-sub"><?= htmlspecialchars($card['sub_line']) ?></div>
            <div class="rp-next"><?= htmlspecialchars($card['next_action']) ?></div>
            <?php if (!empty($card['probation_note'])): ?>
            <div class="rp-probation"><?= htmlspecialchars($card['probation_note']) ?></div>
            <?php endif; ?>
            <?php if (!empty($card['secondary'])): ?>
            <div class="rp-secondary">
                <?php foreach ($card['secondary'] as $s): ?>
                <div class="rp-secondary-item"><span class="rp-secondary-label"><?= htmlspecialchars($s['label']) ?></span><span class="rp-secondary-value"><?= htmlspecialchars($s['value']) ?></span></div>
                <?php endforeach; ?>
            </div>
            <?php endif; ?>
            <?php if (!empty($card['manual_review_owed'])): ?>
            <button type="button" class="rp-review-btn" onclick="rpClearStreakReview('<?= htmlspecialchars($accountId) ?>', this)">I've Completed My Review</button>
            <?php endif; ?>
        </div>
        <?php endforeach; ?>
    </div>
    <?php
}

/**
 * "Right Now (Active Coaching)" — purple section, always scoped to
 * whichever segment is the account's real CURRENT one. Renders nothing
 * when the account has no current live segment (fully breached/closed
 * journeys have nothing to coach on right now).
 */
function renderRnCoachingSection(string $userId, $db, array $account, ?array $currentPhase, array $riskRec): void {
    if ($currentPhase === null) return;
    $accountId = $account['id'];
    $cm = $currentPhase['challenge_metrics'];
    // Reverted (2026-08-07) — see rnBuildPhaseCardFromRows()'s own doc
    // comment for the full history. This card's own "Current Balance" is
    // deliberately scoped to the account's real CURRENT segment
    // (account_size + that segment's own trades' P&L) — the correct
    // model, since each phase/funded promotion genuinely resets the
    // account fresh (verified live).
    $currentBalance = $currentPhase['account_size'] + $currentPhase['total_profit'];
    // Maximum Loss Model (2026-08-17) — real, live bug: $currentPhase
    // ['max_drawdown_amount'] (rnBuildPhaseCardFromRows()'s own local
    // calc, account_size-anchored only) was rendering right next to
    // $ddAmount below, which is already correctly sourced from
    // $cm = getChallengeMetrics() (PropFirmRuleEngine, real model-aware).
    // For a Trailing account those two figures could visibly disagree on
    // the same card. $cm is always populated here — this function only
    // ever runs for the account's real CURRENT phase (rnBuildPhaseCardFromRows()
    // only fetches challenge_metrics when $isCurrent) — so prefer its
    // real limit_amount; the old local value is now only a fallback for
    // the (structurally unreachable here) case $cm is missing.
    $maxDdLimit = $cm['overall_drawdown']['limit_amount'] ?? $currentPhase['max_drawdown_amount'];
    $ddAmount = (float)($cm['overall_drawdown']['current_amount'] ?? 0);
    $ddPct = (float)($cm['overall_drawdown']['current_percentage'] ?? 0);
    $balanceDeltaPct = $currentPhase['account_size'] > 0 ? ($currentPhase['total_profit'] / $currentPhase['account_size']) * 100 : 0.0;

    // Consistency Score (corrected 2026-08-18) — the real prop-firm
    // consistency metric (biggest winning day / total profit this
    // phase), always computed via PropFirmRuleEngine::consistencyStatus()
    // whenever there's real positive profit to measure, regardless of
    // whether the firm has actually configured an enforceable rule.
    // Deliberately replaces the old EdgeHealthEngine "Execution
    // Consistency: Last 30 Days" tile, which measured a completely
    // different thing (risk-sizing coefficient of variation) and could
    // independently show "Excellent" while this same phase was in an
    // active drawdown or losing streak — a real, reported contradiction.
    $cs = null;
    try { $cs = PropFirmRuleEngine::consistencyStatus($accountId); } catch (\Throwable $e) { $cs = null; }

    // Risk Panel (2026-08-17) — 3 independent cards replacing the old
    // merged Coaching Recommendation / Recommended Risk / Until You
    // Recover To tiles below. Every number comes from RiskPanelEngine,
    // which itself only ever reads $cm (PropFirmRuleEngine's own single
    // source of truth for drawdown/balance) — nothing here re-derives
    // any dollar figure. $currentPhase['rows'] is the SAME workflow-
    // scoped, chronological trade set the stat row above is already
    // built from — no new query for the streak card either.
    $overallCard = RiskPanelEngine::overallLossRiskCard($currentBalance, $cm['overall_drawdown'] ?? [], true);
    $streakCard = RiskPanelEngine::lossStreakCard($currentPhase['rows'], RiskPanelEngine::latestReview($accountId, $db));
    $dailyCard = RiskPanelEngine::dailyLossGuardCard($cm['daily_drawdown'] ?? null);
    ?>
    <div class="rn-coaching">
        <div class="rn-coaching-head">
            <div><span class="rn-coaching-title">Right Now</span> <span class="rn-coaching-sub">(Active Coaching)</span></div>
            <span class="rn-live-badge"><span class="rn-live-dot"></span>LIVE</span>
        </div>
        <div class="rn-coaching-stats">
            <div class="rn-coaching-stat">
                <div class="rn-coaching-stat-label">Current Balance</div>
                <div class="rn-coaching-stat-value">$<?= number_format($currentBalance, 2) ?></div>
                <div class="rn-coaching-stat-delta <?= $balanceDeltaPct >= 0 ? 'positive' : 'negative' ?>"><?= ($balanceDeltaPct >= 0 ? '+' : '') . number_format($balanceDeltaPct, 2) ?>%</div>
            </div>
            <div class="rn-coaching-stat">
                <div class="rn-coaching-stat-label">Max Drawdown Limit</div>
                <div class="rn-coaching-stat-value">$<?= number_format($maxDdLimit, 2) ?></div>
            </div>
            <div class="rn-coaching-stat">
                <div class="rn-coaching-stat-label">Drawdown</div>
                <div class="rn-coaching-stat-value negative">$<?= number_format($ddAmount, 2) ?></div>
                <div class="rn-coaching-stat-delta negative">-<?= number_format($ddPct, 2) ?>%</div>
            </div>
            <?php renderRnConsistencyStat($cs); ?>
        </div>
        <?php renderRnRiskPanel($overallCard, $streakCard, $dailyCard, $accountId); ?>
        <div class="rn-coaching-footer" style="justify-content:flex-end">
            <button type="button" class="rn-btn-outline" onclick="rnToggleWorkspace('rn-coaching-workspace')">View Full Coaching Analysis ›</button>
        </div>
    </div>
    <?php
}

/**
 * Consistency Score stat tile (2026-08-18) — biggest winning day as a
 * percent of total profit this phase, via
 * PropFirmRuleEngine::consistencyStatus(). Score and rule status are
 * shown independently: the score is a real analytics metric whenever
 * there's positive profit to compute it from; the rule (a firm-configured
 * maximum) is optional enforcement on top of it, never the other way
 * around. Never a bare percentage with no context, and never a
 * fabricated 0%/pass when total profit isn't positive.
 */
function renderRnConsistencyStat(?array $cs): void {
    $tooltip = 'Biggest single day\'s realized profit as a percent of this phase\'s total profit so far. Lower means gains are spread more evenly across days, which is what most prop firms\' consistency rules require. Always shown when there is positive profit to measure, whether or not this account has a configured consistency requirement.';
    ?>
    <div class="rn-coaching-stat">
        <div class="rn-coaching-stat-label" title="<?= htmlspecialchars($tooltip) ?>">Consistency Score</div>
        <?php if ($cs !== null && !empty($cs['score_available'])): ?>
        <div class="rn-coaching-stat-value"><?= number_format($cs['current_score_pct'], 1) ?>%</div>
        <?php if (!empty($cs['rule_configured'])): ?>
        <div class="rn-score-tag-inline <?= $cs['is_compliant'] ? 'rn-score-good' : 'rn-score-warn' ?>"><?= $cs['is_compliant'] ? 'Within Limit' : 'Above Limit' ?> (max <?= number_format((float)$cs['max_allowed_pct'], 0) ?>%)</div>
        <?php else: ?>
        <div class="rn-coaching-stat-delta" style="color:var(--foreground-dim);font-weight:600">No consistency rule configured on this account</div>
        <?php endif; ?>
        <?php else: ?>
        <div class="rn-coaching-stat-value rn-metric-waiting" style="font-size:.8rem">Not available yet. Requires positive total profit.</div>
        <?php endif; ?>
    </div>
    <?php
}

/**
 * Item 2 (Live Accounts) — a live/personal account has no phase/challenge
 * concept, so unlike rnBuildJourney() there is exactly one segment: the
 * account's whole real trade history. Metrics are the item-2 list (Current
 * Equity, Net Profit, Monthly Return, Current Drawdown, Risk Exposure, Win
 * Rate, Expectancy, Consistency Score) sourced from the SAME engine every
 * other page on this app already uses for account metrics
 * (AnalyticsEngine::getAccountMetrics()) — never a parallel formula.
 */
function rnBuildLiveSegment(string $accountId, array $account, string $userId, $db): array {
    $rnValidationCond = validationScopeSql(ValidationService::getWorkflowState($userId, $db));
    $startingBalance = (float)($account['starting_balance'] ?? 0);
    $trades = $db->raw(
        "SELECT id, instrument, direction, session, entry_price, actual_entry_price,
                sl_price, actual_sl_price, tp_price, tp2_price, tp3_price, tp4_price, tp5_price,
                tp6_price, tp7_price, tp8_price, tp9_price, tp10_price, risk_reward_tp1,
                opened_at, closed_at, captured_at, trade_classification, classification_reason,
                max_potential_loss, max_potential_profit, risk_per_position, notes,
                broker_open_time, broker_close_time, is_reconciled,
                (SELECT COUNT(*) FROM trade_screenshots ts WHERE ts.trade_id = trades.id) AS screenshot_count,
                COALESCE(broker_pnl, projected_pnl, 0) AS effective_pnl
         FROM trades
         WHERE account_id = :aid AND user_id = :uid AND status = 'closed' AND deleted_at IS NULL
           AND {$rnValidationCond}
         ORDER BY captured_at ASC",
        [':aid' => $accountId, ':uid' => $userId]
    );
    rnAttachPriorLossCounts($trades);

    $runningBalance = $startingBalance;
    $rows = [];
    foreach ($trades as $t) {
        $pnl = $t['effective_pnl'] !== null ? (float)$t['effective_pnl'] : 0.0;
        $runningBalance += $pnl;
        $rows[] = ['trade' => $t, 'pnl' => $pnl, 'balance' => $runningBalance];
    }
    $winners = array_values(array_filter($rows, fn($r) => $r['pnl'] > 0));
    $winnerPnls = array_column($winners, 'pnl');
    $avgWin = count($winnerPnls) > 0 ? array_sum($winnerPnls) / count($winnerPnls) : null;
    $bestTrade = count($winnerPnls) > 0 ? max($winnerPnls) : null;
    $winRate = count($rows) > 0 ? round(count($winners) / count($rows) * 100, 2) : 0.0;

    $streak = 0;
    for ($i = count($rows) - 1; $i >= 0; $i--) {
        $p = $rows[$i]['pnl'];
        $sign = $p > 0 ? 1 : ($p < 0 ? -1 : 0);
        if ($i === count($rows) - 1) { $streak = $sign; continue; }
        if ($sign !== 0 && $sign === ($streak <=> 0)) $streak += $sign; else break;
    }

    $accountMetrics = null;
    try { $accountMetrics = AnalyticsEngine::getAccountMetrics($accountId); } catch (\Throwable $e) { $accountMetrics = null; }

    $monthMetrics = null;
    try {
        $monthMetrics = AnalyticsEngine::getAccountMetrics($accountId, ['dateFrom' => date('Y-m-01 00:00:00'), 'dateTo' => date('Y-m-d H:i:s')]);
    } catch (\Throwable $e) { $monthMetrics = null; }

    // Risk Exposure — a genuine, real "right now" figure: the sum of
    // risk_pct across every currently OPEN position on this account. Zero
    // open positions is a real zero, not a placeholder.
    $openRiskRows = $db->raw(
        "SELECT risk_pct FROM trades WHERE account_id = :aid AND user_id = :uid AND status = 'open' AND deleted_at IS NULL AND {$rnValidationCond}",
        [':aid' => $accountId, ':uid' => $userId]
    );
    $riskExposure = 0.0;
    foreach ($openRiskRows as $orow) { $riskExposure += (float)($orow['risk_pct'] ?? 0); }

    $capturedAts = array_filter(array_column($trades, 'captured_at'));
    $startDate = !empty($capturedAts) ? min($capturedAts) : (string)($account['created_at'] ?? date('Y-m-d H:i:s'));

    return [
        'rows' => $rows, 'avg_win' => $avgWin, 'best_trade' => $bestTrade,
        'total_profit' => array_sum(array_column($rows, 'pnl')), 'win_rate' => $winRate, 'streak' => $streak,
        'starting_balance' => $startingBalance, 'trade_count' => count($rows),
        'start_date' => $startDate, 'end_date' => date('Y-m-d H:i:s'),
        'account_metrics' => $accountMetrics, 'month_metrics' => $monthMetrics,
        'risk_exposure_pct' => $riskExposure, 'open_position_count' => count($openRiskRows),
    ];
}

/** Live Account card — same layout/cards/table as a prop-firm phase card (item 2: "the page should feel identical, only the metrics change"). */
function renderRnLiveCard(string $userId, $db, array $account, array $segment): void {
    // Item 6 — computed once, reused for both this card's own trade table
    // and the Goal section's filter chips below it.
    $tableId = 'rn-table-live-' . preg_replace('/[^a-z0-9]+/', '-', strtolower($account['id']));
    $lifecycleState = $account['lifecycle_state'] ?? 'active';
    $statusLabel = $lifecycleState === 'closed' ? 'CLOSED' : 'ACTIVE';
    $statusBadgeClass = $lifecycleState === 'closed' ? 'rn-badge-neutral' : 'rn-badge-blue';

    $am = $segment['account_metrics'];
    $hasTrades = ($am['total_trades'] ?? $segment['trade_count']) > 0;
    $equity = $am['equity'] ?? ($segment['starting_balance'] + $segment['total_profit']);
    $netPnl = $am['net_pnl'] ?? $segment['total_profit'];
    $ddPct = $hasTrades ? (float)($am['current_drawdown_pct'] ?? 0) : null;
    $winRateEngine = $hasTrades ? (float)($am['win_rate'] ?? 0) : null;
    $expectancy = $hasTrades ? (float)($am['expectancy'] ?? 0) : null;
    $totalTrades = $am['total_trades'] ?? $segment['trade_count'];

    $mm = $segment['month_metrics'];
    $monthlyHasTrades = ($mm['total_trades'] ?? 0) > 0;
    $monthlyReturnPct = $monthlyHasTrades ? (float)($mm['return_pct'] ?? 0) : null;

    $activeGoal = rnFetchActiveGoal($account['id'], $userId, $db);

    // Item 8 — deposits/withdrawals via the app's own existing, real
    // /api/accounts/transaction endpoint (updates trading_accounts'
    // net_deposits/net_withdrawals/current_balance/peak_equity and logs a
    // lifecycle_event) — never a second, competing ledger. Trading Profit
    // is kept isolated from cash movement, same architectural split
    // Reports' own Capital Flow card already establishes.
    $netDeposits = (float)($account['net_deposits'] ?? 0);
    $netWithdrawals = (float)($account['net_withdrawals'] ?? 0);
    $netEquity = $segment['starting_balance'] + $netDeposits - $netWithdrawals + $segment['total_profit'];

    // 2026-08-19 fix: same real "Undefined variable $consistency" bug as
    // renderRnFundedCard() (see that function's own comment), just for
    // the Live Accounts card. A live account has no PropFirmRuleEngine
    // phase concept to call into, so this reuses the exact same formula
    // (biggest single day's realized profit as a percent of total profit)
    // computed directly from this segment's own already-fetched rows,
    // never a second parallel engine.
    $consistency = null;
    if ($segment['total_profit'] > 0) {
        $byDay = [];
        foreach ($segment['rows'] as $r) {
            $day = substr((string)($r['trade']['captured_at'] ?? ''), 0, 10);
            if ($day === '') continue;
            $byDay[$day] = ($byDay[$day] ?? 0.0) + $r['pnl'];
        }
        $biggestDay = !empty($byDay) ? max($byDay) : 0.0;
        if ($biggestDay > 0) $consistency = round(($biggestDay / $segment['total_profit']) * 100, 2);
    }
    ?>
    <div class="rn-phase-card rn-card-live">
        <div class="rn-phase-head">
            <div class="rn-phase-head-left">
                <div class="rn-phase-title-row">
                    <h2 class="rn-phase-title">Live Account - <?= htmlspecialchars($account['label']) ?></h2>
                    <span class="rn-badge <?= $statusBadgeClass ?>"><?= $statusLabel ?></span>
                </div>
                <div class="rn-phase-stats-line">
                    Starting Balance: $<?= number_format($segment['starting_balance'], 2) ?>
                    &bull; Current Equity: $<?= number_format($equity, 2) ?>
                    &bull; Trades Taken: <?= $totalTrades ?>
                </div>
            </div>
            <div class="rn-phase-head-right">
                <div class="rn-metric"><div class="rn-metric-value">$<?= number_format($equity, 2) ?></div><div class="rn-metric-label">Current Equity</div></div>
                <div class="rn-metric"><div class="rn-metric-value <?= pnlClass($netPnl) ?>"><?= pnlFormat($netPnl) ?></div><div class="rn-metric-label">Net Profit</div></div>
                <div class="rn-metric">
                    <div class="rn-metric-value <?= $winRateEngine === null ? 'rn-metric-waiting' : '' ?>"><?= $winRateEngine !== null ? number_format($winRateEngine * 100, 1) . '%' : 'No closed trades yet to calculate a win rate' ?></div>
                    <div class="rn-metric-label">Win Rate</div>
                </div>
                <div class="rn-metric rn-goal-metric">
                    <?php if ($activeGoal): $gp = rnGoalProgress($activeGoal, $equity, $segment['avg_win']); ?>
                    <div class="rn-metric-value"><?= $gp['achieved'] ? '✓ Reached' : '🎯 Reach' ?> $<?= number_format($gp['target'], 0) ?><br><span class="rn-goal-metric-pct"><?= round($gp['pct']) ?>% Complete</span></div>
                    <?php else: ?>
                    <div class="rn-metric-value rn-metric-waiting">No target balance set yet, set one below</div>
                    <?php endif; ?>
                    <div class="rn-metric-label" title="Your own personal target balance for this account, set below.">Growth Target</div>
                </div>
            </div>
        </div>
        <?php
        // Live accounts have no declared drawdown limit — reuses the
        // same informal 20%-of-starting-balance ceiling this file
        // already established elsewhere (rnComputeRiskRecommendation()'s
        // own "20% drawdown-from-peak informal ceiling stands in for the
        // account's drawdown budget" reasoning) rather than inventing a
        // second convention for the same real gap.
        renderRnTradeTable($segment['display_rows'] ?? $segment['rows'], $tableId, isset($segment['display_rows']), null, $segment['starting_balance'], $segment['starting_balance'] * 0.20);
        ?>
        <div class="rn-funded-strip">
            <div><span class="rn-funded-strip-label">Monthly Return:</span>
                <?php if ($monthlyReturnPct !== null): ?>
                <span class="<?= pnlClass($monthlyReturnPct) ?>"><?= ($monthlyReturnPct >= 0 ? '+' : '') . number_format($monthlyReturnPct * 100, 2) ?>%</span>
                <?php else: ?>
                <span class="rn-metric-waiting">No trades closed this month yet</span>
                <?php endif; ?>
            </div>
            <div><span class="rn-funded-strip-label">Current Drawdown:</span>
                <?= $ddPct !== null ? number_format(abs($ddPct) * 100, 2) . '%' : '<span class="rn-metric-waiting">No trade history yet to measure drawdown</span>' ?>
            </div>
            <div><span class="rn-funded-strip-label">Risk Exposure:</span>
                <?= $segment['open_position_count'] > 0
                    ? number_format($segment['risk_exposure_pct'], 2) . '% across ' . $segment['open_position_count'] . ' open position' . ($segment['open_position_count'] === 1 ? '' : 's')
                    : '0.00% - no open positions right now' ?>
            </div>
            <div><span class="rn-funded-strip-label">Expectancy:</span>
                <?php if ($expectancy !== null): ?>
                <span class="<?= pnlClass($expectancy) ?>"><?= ($expectancy >= 0 ? '+$' : '-$') . number_format(abs($expectancy), 2) ?> per trade</span>
                <?php else: ?>
                <span class="rn-metric-waiting">No closed trades yet to calculate expectancy</span>
                <?php endif; ?>
            </div>
            <div><span class="rn-funded-strip-label">Best Trade:</span> <span class="<?= $segment['best_trade'] !== null ? 'positive' : 'rn-metric-waiting' ?>"><?= $segment['best_trade'] !== null ? pnlFormat($segment['best_trade']) : 'No winning trades recorded yet' ?></span></div>
            <div><span class="rn-funded-strip-label"><?= $segment['streak'] > 0 ? 'Current Winning Streak:' : ($segment['streak'] < 0 ? 'Current Losing Streak:' : 'Current Streak:') ?></span> <span class="<?= $segment['streak'] >= 0 ? 'positive' : 'negative' ?>"><?= $segment['streak'] === 0 ? 'None yet' : abs($segment['streak']) . ' ' . ($segment['streak'] > 0 ? 'Wins' : 'Losses') ?></span></div>
        </div>
    </div>

    <div class="rn-capital-panel">
        <div class="rn-goal-head">
            <div class="rn-goal-title">💵 Capital Movements</div>
            <?php if ($lifecycleState !== 'closed'): ?>
            <div class="rn-capital-actions">
                <button type="button" class="rn-btn-outline" onclick="rnOpenTxModal('<?= htmlspecialchars($account['id']) ?>', 'deposit')">+ Deposit</button>
                <button type="button" class="rn-btn-outline" onclick="rnOpenTxModal('<?= htmlspecialchars($account['id']) ?>', 'withdrawal')">- Withdraw</button>
            </div>
            <?php endif; ?>
        </div>
        <div class="rn-capital-grid">
            <div class="rn-capital-stat"><div class="rn-capital-stat-value <?= pnlClass($segment['total_profit']) ?>"><?= pnlFormat($segment['total_profit']) ?></div><div class="rn-capital-stat-label">Trading Profit</div></div>
            <div class="rn-capital-stat"><div class="rn-capital-stat-value positive">+$<?= number_format($netDeposits, 2) ?></div><div class="rn-capital-stat-label">Deposits</div></div>
            <div class="rn-capital-stat"><div class="rn-capital-stat-value negative">-$<?= number_format($netWithdrawals, 2) ?></div><div class="rn-capital-stat-label">Withdrawals</div></div>
            <div class="rn-capital-stat"><div class="rn-capital-stat-value">$<?= number_format($netEquity, 2) ?></div><div class="rn-capital-stat-label">Net Equity</div></div>
        </div>
        <div class="rn-capital-note">Trading Profit reflects closed-trade outcomes only. Deposits and withdrawals are cash movements, kept separate so growth from trading is never conflated with capital you added or took out.</div>
    </div>
    <?php
    $cls = rnClassificationCounts($segment['rows']);
    renderRnGoalSection($userId, $db, $account, $equity, $segment['avg_win'], [
        'win_rate' => $winRateEngine !== null ? $winRateEngine * 100 : null, 'streak' => $segment['streak'],
        'disciplined_count' => $cls['counts']['disciplined_win'], 'tilt_count' => $cls['counts']['tilt_warning'],
        'revenge_count' => $cls['counts']['revenge_trade'], 'high_risk_loss_count' => $cls['counts']['loss_high_risk'],
        'consistency' => $consistency, 'classified_total' => $cls['total'], 'table_id' => $tableId,
    ]);
}

/** Live-account version of "Right Now (Active Coaching)" — same layout as the prop version, drawdown measured from real historical peak equity rather than a challenge's declared max-drawdown limit (which doesn't exist for a personal account). */
function renderRnCoachingSectionLive(string $userId, $db, array $account, array $segment, array $riskRec): void {
    $rnValidationCond = validationScopeSql(ValidationService::getWorkflowState($userId, $db));
    $accountId = $account['id'];
    $am = $segment['account_metrics'];
    $totalTrades = $am['total_trades'] ?? $segment['trade_count'];
    $equity = $am['equity'] ?? ($segment['starting_balance'] + $segment['total_profit']);
    $peakEquity = ($am['peak_equity'] ?? null) !== null ? (float)$am['peak_equity'] : max($equity, $segment['starting_balance']);
    $ddAmount = max(0.0, $peakEquity - $equity);
    $balanceDeltaPct = $segment['starting_balance'] > 0 ? ($segment['total_profit'] / $segment['starting_balance']) * 100 : 0.0;

    // §1.1a/§1.1b fix (2026-08-10) — both this card and the "Drawdown From
    // Peak" tile below used to gate on $inDrawdown (the RISK-SIZING tier,
    // which only flips at ~5%+ of peak for live accounts per
    // rnComputeRiskRecommendation()'s dd_used_ratio/0.20 scaling), not on
    // the real $ddAmount fact. A real drawdown under that ~5% floor
    // rendered the literal "$0.00 / At peak" here despite $ddAmount being a
    // real, already-computed, nonzero value — and the recovery card below
    // rendered unconditionally the same way §1.1a's prop-side sibling did.
    // Both now key off the real fact ($ddAmount > 0), not the risk tier.
    $isInDrawdown = $ddAmount > 0.005;

    // Risk Panel (2026-08-17) — same 3 independent cards as the prop-firm
    // coaching section above. Live accounts have no firm-declared
    // drawdown limit or daily-loss rule, so Card 1 uses the same informal
    // 20%-from-peak ceiling already established elsewhere in this file
    // (RiskPanelEngine::liveOverallDrawdown()) and Card 3 honestly shows
    // "NOT SET" rather than fabricating a number.
    $overallCard = RiskPanelEngine::overallLossRiskCard($equity, RiskPanelEngine::liveOverallDrawdown($equity, $peakEquity), false);
    $streakCard = RiskPanelEngine::lossStreakCard($segment['rows'], RiskPanelEngine::latestReview($accountId, $db));
    $dailyCard = RiskPanelEngine::dailyLossGuardCard(null);

    // Item 8 — Live Accounts coach against margin/capital preservation, NOT
    // breach rules (a personal account has none). Sourced from the most
    // recent trade's own real margin snapshot (trades.margin_dollars /
    // free_margin_dollars / margin_used_pct — captured by the calculator at
    // save time, when filled in). Genuinely absent on legacy trades that
    // never had margin details entered — shown as an honest gap, never a
    // fabricated number.
    $marginRow = null;
    try {
        $mRows = $db->raw(
            "SELECT margin_dollars, free_margin_dollars, margin_used_pct, leverage, captured_at
             FROM trades WHERE account_id = :aid AND user_id = :uid AND deleted_at IS NULL
               AND {$rnValidationCond} AND margin_used_pct IS NOT NULL
             ORDER BY captured_at DESC LIMIT 1",
            [':aid' => $accountId, ':uid' => $userId]
        );
        $marginRow = $mRows[0] ?? null;
    } catch (\Throwable $e) { $marginRow = null; }
    $marginUsedPct = $marginRow !== null ? (float)$marginRow['margin_used_pct'] : null;
    $marginCallProximity = match (true) {
        $marginUsedPct === null => null,
        $marginUsedPct >= 80 => ['Critical: near margin call', 'negative'],
        $marginUsedPct >= 50 => ['Elevated: worth reducing exposure', 'rn-metric-waiting'],
        default => ['Healthy', 'positive'],
    };
    ?>
    <div class="rn-coaching">
        <div class="rn-coaching-head">
            <div><span class="rn-coaching-title">Right Now</span> <span class="rn-coaching-sub">(Active Coaching)</span></div>
            <span class="rn-live-badge"><span class="rn-live-dot"></span>LIVE</span>
        </div>
        <div class="rn-coaching-stats rn-coaching-stats-live">
            <div class="rn-coaching-stat">
                <div class="rn-coaching-stat-label">Current Equity</div>
                <div class="rn-coaching-stat-value">$<?= number_format($equity, 2) ?></div>
                <div class="rn-coaching-stat-delta <?= $balanceDeltaPct >= 0 ? 'positive' : 'negative' ?>"><?= ($balanceDeltaPct >= 0 ? '+' : '') . number_format($balanceDeltaPct, 2) ?>%</div>
            </div>
            <div class="rn-coaching-stat">
                <div class="rn-coaching-stat-label">Peak Equity</div>
                <div class="rn-coaching-stat-value">$<?= number_format($peakEquity, 2) ?></div>
            </div>
            <div class="rn-coaching-stat">
                <div class="rn-coaching-stat-label">Drawdown From Peak</div>
                <?php if ($isInDrawdown): ?>
                <div class="rn-coaching-stat-value negative">$<?= number_format($ddAmount, 2) ?></div>
                <div class="rn-coaching-stat-delta negative">-<?= number_format($peakEquity > 0 ? ($ddAmount / $peakEquity) * 100 : 0, 2) ?>%</div>
                <?php else: ?>
                <div class="rn-coaching-stat-value positive">$0.00</div>
                <div class="rn-coaching-stat-delta">At peak</div>
                <?php endif; ?>
            </div>
            <div class="rn-coaching-stat">
                <div class="rn-coaching-stat-label">Margin Call Proximity</div>
                <?php if ($marginCallProximity !== null): ?>
                <div class="rn-coaching-stat-value <?= $marginCallProximity[1] ?>" style="font-size:.85rem"><?= htmlspecialchars($marginCallProximity[0]) ?></div>
                <div class="rn-coaching-stat-delta">Margin used <?= number_format($marginUsedPct, 1) ?>%<?= $marginRow['free_margin_dollars'] !== null ? ' &middot; Free margin $' . number_format((float)$marginRow['free_margin_dollars'], 2) : '' ?> (last trade)</div>
                <?php else: ?>
                <div class="rn-coaching-stat-value rn-metric-waiting" style="font-size:.78rem">No margin data captured yet</div>
                <div class="rn-coaching-stat-delta" style="color:var(--foreground-dim);font-weight:600">Fill in margin details when saving a trade to unlock this</div>
                <?php endif; ?>
            </div>
        </div>
        <?php renderRnRiskPanel($overallCard, $streakCard, $dailyCard, $accountId); ?>
        <div class="rn-coaching-footer" style="justify-content:flex-end">
            <button type="button" class="rn-btn-outline" onclick="rnToggleWorkspace('rn-coaching-workspace')">View Full Coaching Analysis ›</button>
        </div>
    </div>
    <?php
}

/**
 * Item 5 — "View Full Coaching Analysis" expands an in-page workspace
 * instead of redirecting to Reports. Reports explains historical
 * performance; this answers "what should I do on my next trade?" — built
 * from EdgeHealthEngine::getScore() (account-scoped via its new optional
 * $accountId param — real strengths/weaknesses/trends/confidence, never a
 * second scoring formula), this account's own most-recently-attached
 * strategy's real checklist_items (a genuine, declared, rule-by-rule
 * checklist — not invented), and this account's own real
 * trade_classification history (already fetched by the caller, no new
 * query) for emotional-pattern / behaviours-to-avoid content.
 */
function renderRnCoachingWorkspace(string $userId, $db, array $account, string $rnType, array $allTrades, array $riskRec): void {
    $accountId = $account['id'];
    $edge = null;
    try { $edge = EdgeHealthEngine::getScore($userId, null, $accountId); } catch (\Throwable $e) { $edge = null; }

    $checklist = [];
    $strategyName = null;
    try {
        // 2026-08-12 workflow-isolation audit — same fix as
        // rnComputeRiskRecommendation() above: scope to the CURRENT
        // workflow's own trades, not whichever trade (validation or main)
        // happened to be captured most recently on this shared account.
        $rnCoachStratCond = validationScopeSql(ValidationService::getWorkflowState($userId, $db), 't');
        $svRow = $db->raw(
            "SELECT sv.id, s.name FROM trades t
             JOIN strategy_versions sv ON sv.id = t.strategy_version_id
             JOIN strategies s ON s.id = sv.strategy_id
             WHERE t.account_id = :aid AND t.user_id = :uid AND t.strategy_version_id IS NOT NULL AND {$rnCoachStratCond}
             ORDER BY t.captured_at DESC LIMIT 1",
            [':aid' => $accountId, ':uid' => $userId]
        );
        if (!empty($svRow)) {
            $strategyName = trim($svRow[0]['name']); // a real, pre-existing trailing-space data quirk on some strategy names — trimmed at display, same convention already established elsewhere in this app
            $checklist = $db->raw(
                "SELECT category, polished_text, raw_text, is_required FROM checklist_items WHERE strategy_version_id = :svid ORDER BY category, position",
                [':svid' => $svRow[0]['id']]
            );
        }
    } catch (\Throwable $e) { $checklist = []; }

    $classCounts = [];
    $recentCount = 0;
    foreach ($allTrades as $t) {
        $c = $t['trade_classification'] ?? null;
        if ($c === null) continue;
        $classCounts[$c] = ($classCounts[$c] ?? 0) + 1;
        $recentCount++;
    }
    $tiltCount = $classCounts['tilt_warning'] ?? 0;
    $revengeCount = $classCounts['revenge_trade'] ?? 0;
    $disciplinedCount = $classCounts['disciplined_win'] ?? 0;
    $highRiskLossCount = $classCounts['loss_high_risk'] ?? 0;
    $elevatedWinCount = $classCounts['elevated_win'] ?? 0;
    ?>
    <div id="rn-coaching-workspace-backdrop" class="rn-modal-backdrop" style="display:none" onclick="if(event.target===this) rnToggleWorkspace('rn-coaching-workspace')">
    <div id="rn-coaching-workspace" class="rn-workspace rn-modal-panel" style="display:none">
        <div class="rn-workspace-head">
            <h3>Full Coaching Analysis - <?= htmlspecialchars($account['label']) ?></h3>
            <button type="button" class="rn-workspace-close" onclick="rnToggleWorkspace('rn-coaching-workspace')">✕ Close</button>
        </div>
        <?php if ($edge === null || empty($edge['has_history'])): ?>
        <div class="rn-empty-note">No closed trades on this account yet. This coaching analysis will populate once trades close.</div>
        <?php else: ?>
        <div class="rn-workspace-confidence"><?= htmlspecialchars($edge['narrative']['confidence_line'] ?? '') ?></div>
        <div class="rn-workspace-grid">
            <?php foreach ($edge['factors'] as $key => $f): ?>
            <div class="rn-workspace-card">
                <div class="rn-workspace-card-head">
                    <span title="<?= htmlspecialchars(rnEdgeFactorTooltip($key)) ?>"><?= htmlspecialchars($f['label']) ?> <span style="opacity:.55;font-weight:400">ⓘ</span></span>
                    <span class="rn-workspace-score" title="<?= htmlspecialchars(rnEdgeFactorTooltip($key)) ?>"><?= round($f['score']) ?>/100</span>
                </div>
                <div class="rn-workspace-trend rn-trend-<?= htmlspecialchars($f['trend']) ?>">
                    <?= match ($f['trend']) { 'improving' => '▲ Improving', 'declining' => '▼ Deteriorating', 'stable' => '▬ Stable', default => 'Not enough history yet' } ?>
                </div>
                <p><?= htmlspecialchars($edge['narrative']['factor_lines'][$key] ?? '') ?></p>
            </div>
            <?php endforeach; ?>
        </div>

        <div class="rn-workspace-row">
            <div class="rn-workspace-card">
                <div class="rn-workspace-card-head"><span>Emotional Pattern</span></div>
                <?php if ($recentCount === 0): ?>
                <p>No classified trades on this account yet.</p>
                <?php else: ?>
                <p>Of the last <?= $recentCount ?> classified trade<?= $recentCount === 1 ? '' : 's' ?> on this account: <b><?= $disciplinedCount ?></b> Disciplined Win<?= $disciplinedCount === 1 ? '' : 's' ?><?= rnFilterChip('disciplined_win', $disciplinedCount, 'Disciplined Win') ?>, <b><?= $tiltCount ?></b> Tilt Warning<?= $tiltCount === 1 ? '' : 's' ?><?= rnFilterChip('tilt_warning', $tiltCount, 'Tilt Warning Trade') ?>, <b><?= $revengeCount ?></b> Revenge Trade<?= $revengeCount === 1 ? '' : 's' ?><?= rnFilterChip('revenge_trade', $revengeCount, 'Revenge Trade') ?>.
                <?= ($tiltCount + $revengeCount) > 0
                    ? ' This account has real evidence of trading through losing stretches rather than pausing, the single highest-leverage behavior to change.'
                    : ' No tilt or revenge-trading patterns detected on this account so far, real discipline after a loss.' ?></p>
                <?php endif; ?>
            </div>
            <div class="rn-workspace-card">
                <div class="rn-workspace-card-head"><span>Behaviours to Avoid</span></div>
                <ul class="rn-workspace-list">
                    <?php if ($highRiskLossCount > 0): ?><li>Sizing beyond the plan on losing trades (<?= $highRiskLossCount ?> occurrence<?= $highRiskLossCount === 1 ? '' : 's' ?>)<?= rnFilterChip('loss_high_risk', $highRiskLossCount, 'Oversized Loss') ?>, keep size fixed at entry, never adjust mid-trade.</li><?php endif; ?>
                    <?php if ($tiltCount > 0): ?><li>Continuing to trade immediately after a loss without a deliberate pause (<?= $tiltCount ?> flagged trade<?= $tiltCount === 1 ? '' : 's' ?>)<?= rnFilterChip('tilt_warning', $tiltCount, 'Tilt Warning Trade') ?>.</li><?php endif; ?>
                    <?php if ($revengeCount > 0): ?><li>Increasing size specifically to "win back" a loss (<?= $revengeCount ?> flagged trade<?= $revengeCount === 1 ? '' : 's' ?>)<?= rnFilterChip('revenge_trade', $revengeCount, 'Revenge Trade') ?>, this is the single pattern most worth eliminating.</li><?php endif; ?>
                    <?php if ($elevatedWinCount > 0): ?><li>Letting winners run well past the planned target (<?= $elevatedWinCount ?> occurrence<?= $elevatedWinCount === 1 ? '' : 's' ?>)<?= rnFilterChip('elevated_win', $elevatedWinCount, 'Elevated Win') ?>, a good outcome, but worth confirming it's process, not luck.</li><?php endif; ?>
                    <?php if (($tiltCount + $revengeCount + $highRiskLossCount + $elevatedWinCount) === 0): ?><li>No flagged behavioural patterns on this account yet, keep journaling to build a fuller picture.</li><?php endif; ?>
                </ul>
            </div>
        </div>

        <div class="rn-workspace-card">
            <div class="rn-workspace-card-head"><span>Next Trade Checklist<?= $strategyName ? ': ' . htmlspecialchars($strategyName) : '' ?></span></div>
            <?php if (empty($checklist) && $riskRec['declared'] === null): ?>
            <p>No strategy attached to this account's trades yet. Attach one in Strategy Builder to get real, rule-by-rule coaching here.</p>
            <?php else: ?>
            <div class="rn-coach-block">
                <div class="rn-coach-block-title">Setup</div>
                <p><?= htmlspecialchars(rnCoachingSetupText($checklist)) ?></p>
            </div>
            <div class="rn-coach-block">
                <div class="rn-coach-block-title">Entry</div>
                <p><?= htmlspecialchars(rnCoachingEntryText($checklist)) ?></p>
            </div>
            <div class="rn-coach-block">
                <div class="rn-coach-block-title">Execution</div>
                <p><?= htmlspecialchars(rnCoachingExecutionText($checklist)) ?></p>
            </div>
            <div class="rn-coach-block rn-coach-block-last">
                <div class="rn-coach-block-title">Risk</div>
                <p><?= htmlspecialchars(rnCoachingRiskText($checklist, $riskRec)) ?></p>
            </div>
            <?php endif; ?>
        </div>
        <?php endif; ?>
    </div>
    </div>
    <?php
}

/**
 * Item 6 — "View Account Summary" describes the account itself (facts),
 * never its performance (that's Reports' and this page's own coaching
 * section's job).
 *
 * 2026-08-18 — this modal's OWN account-details rendering (a separately-
 * built Identity/Strategy/Trading Rules/Timeline/Configuration grouping)
 * was deleted, per Kevin's own explicit "delete it" instruction, and
 * replaced with the exact same rendering Trading Center's Account Home
 * page already shows — src/AccountSummaryView.php's
 * renderAccountSummarySections(), the same function that page now calls
 * too. Reported directly: the two pages showed genuinely different
 * information for the same account (this modal never showed Risk
 * Boundary/Total Trades/phase-aware Current Balance/Account Status at
 * all; Trading Center never showed which strategy the account was
 * running), with no shared source. This modal now only supplies its own
 * chrome (backdrop, header, Close button) around that one shared
 * rendering — every account fact, and the "Edit Account Details" flow
 * (renderAccountAdditionalDetails(), same file), is now genuinely
 * identical on both pages, not just visually similar.
 */
function renderRnAccountSummaryPanel(array $account, string $rnType, string $userId, $db, PDO $pdo, string $workflowState = 'main'): void {
    ?>
    <div id="rn-account-summary-backdrop" class="rn-modal-backdrop" style="display:none" onclick="if(event.target===this) rnToggleWorkspace('rn-account-summary')">
    <div id="rn-account-summary" class="rn-workspace rn-modal-panel" style="display:none">
        <div class="rn-workspace-head">
            <h3>Account Summary - <?= htmlspecialchars($account['label']) ?></h3>
            <button type="button" class="rn-workspace-close" onclick="rnToggleWorkspace('rn-account-summary')">✕ Close</button>
        </div>
        <?php renderAccountSummarySections($account, $rnType, $db, $pdo, $userId, $workflowState); ?>
    </div>
    </div>
    <?php
}

/** Item 1 — Set/Replace Goal modal, shared by every "Set Goal"/"Set New Goal" trigger on the page (Funded card and Live card alike). One modal, one form, POSTs to /api/journal/save-goal, then reloads to reflect the new/replaced goal. */
function renderRnGoalModal(): void {
    ?>
    <div id="rn-goal-modal-backdrop" class="rn-modal-backdrop" style="display:none" onclick="if(event.target===this) rnCloseGoalModal()">
        <div class="rn-workspace rn-modal-panel rn-modal-panel-sm">
            <div class="rn-workspace-head">
                <h3>Set Goal</h3>
                <button type="button" class="rn-workspace-close" onclick="rnCloseGoalModal()">✕ Close</button>
            </div>
            <div id="rn-goal-modal-error" class="rn-modal-error" style="display:none"></div>
            <div class="rn-modal-form">
                <label class="rn-modal-label">Current Balance</label>
                <div id="rn-goal-modal-current" class="rn-modal-static">$0.00</div>
                <label class="rn-modal-label" for="rn-goal-target">Target Balance</label>
                <input type="number" id="rn-goal-target" class="rn-modal-input" min="0.01" step="0.01" placeholder="e.g. 8000">
                <label class="rn-modal-label" for="rn-goal-reason">Optional reason</label>
                <textarea id="rn-goal-reason" class="rn-modal-textarea" maxlength="500" placeholder="Grow account before first withdrawal."></textarea>
                <div class="rn-modal-actions">
                    <button type="button" class="rn-note-cancel-btn" onclick="rnCloseGoalModal()">Cancel</button>
                    <button type="button" class="rn-note-save-btn" id="rn-goal-save-btn" onclick="rnSaveGoal()">Save Goal</button>
                </div>
            </div>
        </div>
    </div>
    <?php
}

/** Item 8 — Deposit/Withdraw modal for Live Accounts, posts to the app's own existing /api/accounts/transaction endpoint. */
function renderRnTransactionModal(): void {
    ?>
    <div id="rn-tx-modal-backdrop" class="rn-modal-backdrop" style="display:none" onclick="if(event.target===this) rnCloseTxModal()">
        <div class="rn-workspace rn-modal-panel rn-modal-panel-sm">
            <div class="rn-workspace-head">
                <h3 id="rn-tx-modal-title">Record Deposit</h3>
                <button type="button" class="rn-workspace-close" onclick="rnCloseTxModal()">✕ Close</button>
            </div>
            <div id="rn-tx-modal-error" class="rn-modal-error" style="display:none"></div>
            <div class="rn-modal-form">
                <label class="rn-modal-label" for="rn-tx-amount">Amount</label>
                <input type="number" id="rn-tx-amount" class="rn-modal-input" min="0.01" step="0.01" placeholder="e.g. 500">
                <div class="rn-modal-actions">
                    <button type="button" class="rn-note-cancel-btn" onclick="rnCloseTxModal()">Cancel</button>
                    <button type="button" class="rn-note-save-btn" id="rn-tx-save-btn" onclick="rnSaveTx()">Save</button>
                </div>
            </div>
        </div>
    </div>
    <?php
}

/**
 * Trade Screenshot upload/view/delete modal, restored 2026-08-18 as its
 * own new feature (trade_screenshots table) — a genuinely separate
 * concept from "View Trade" (the disabled future chart-replay column,
 * left completely untouched). One shared instance per page, opened via
 * rnOpenScreenshotModal(tradeId) from a screenshot badge cell inside
 * renderRnTradeTable(); assets/js/trade-screenshots.js drives it.
 */
function renderRnScreenshotModal(): void {
    ?>
    <div id="rn-screenshot-modal-backdrop" class="rn-screenshot-modal-backdrop" style="display:none" onclick="if(event.target===this) rnCloseScreenshotModal()">
        <div class="rn-screenshot-modal-panel">
            <div class="rn-screenshot-modal-head">
                <h3>Trade Screenshots</h3>
                <button type="button" class="rn-screenshot-modal-close" onclick="rnCloseScreenshotModal()">✕ Close</button>
            </div>
            <div id="rn-screenshot-modal-error" class="rn-screenshot-modal-error" style="display:none"></div>
            <div id="rn-screenshot-modal-body"></div>
            <label class="rn-screenshot-modal-label" for="rn-screenshot-modal-file">Upload a screenshot (from TradingView or elsewhere)</label>
            <input type="file" id="rn-screenshot-modal-file" class="rn-screenshot-modal-file" accept="image/jpeg,image/png,image/gif,image/webp">
            <div class="rn-screenshot-modal-actions">
                <button type="button" class="rn-screenshot-cancel-btn" onclick="rnCloseScreenshotModal()">Close</button>
                <button type="button" class="rn-screenshot-upload-btn" id="rn-screenshot-upload-btn" onclick="rnUploadScreenshot()">Upload</button>
            </div>
        </div>
    </div>
    <?php
}

// ════════════════════════════════════════════════════════════════════
// TRADE JOURNAL — the canonical, all-accounts-capable trade explorer,
// now embedded inside Active Journeys (see renderTradeJournalSection()
// below) instead of living on a separate "Journal Trades" page.
// ?trade_id=X drills into a single trade regardless of workspace.
// ════════════════════════════════════════════════════════════════════
// The filtered LIST fetch (account/type-defaulted) happens inside
// renderRightNow() itself, where the selected account and rn_type are
// already known — see renderTradeJournalSection()'s call site.
$jtTradeId = $_GET['trade_id'] ?? null;
$jtTrade = null;

if ($jtTradeId) {
    $jtTrade = $db->selectById('trades', $jtTradeId);
    // Workflow-aware, not an unconditional validation-trade block: a
    // validating user must be able to open their OWN validation trades
    // here (spec §8), while a main-workflow user must still never be
    // able to open a validation trade by guessing/pasting its id.
    $jtTradeIsValidation = !empty($jtTrade['is_validation']);
    $jtTradeInScope = $journalWorkflowState === 'validation' ? $jtTradeIsValidation : !$jtTradeIsValidation;
    if (!$jtTrade || $jtTrade['user_id'] !== $userId || !$jtTradeInScope) {
        $jtTrade = null;
    }
}

// ?breach_account=X — the one and only entry point into a breached/
// closed-at-a-loss account's real trade history from inside Active
// Journeys. Reached exclusively via Lessons From Failure's "View in
// Trades Table" link (see renderBreachTradesView()'s own doc comment
// below) — never listed in, or reachable through, Active Journeys' own
// account picker, which still only ever shows active accounts.
$breachAccountId = $_GET['breach_account'] ?? null;

/**
 * Trade Journal filter bar (journal redesign, 2026-07-31; trimmed same
 * day — removed the compact "Showing X of Y trades" result list this
 * used to render below the filters, which duplicated the real phase/
 * Funded/Live trade tables Active Journeys already renders further down
 * the page, and removed the account selector, which duplicated the
 * global account picker in the page header next to "View Account
 * Summary." Refined again 2026-07-31 (filter-functionality pass): "Any
 * Status" was removed outright (redundant with the State column already
 * visible in every trade row) and every remaining control — Instrument,
 * Session, Classification, From/To Date — now actually narrows the trade
 * table(s) below via rnFilterTradeJournalRows(), applied per phase/segment
 * at render.php's call site in renderRightNow(). Selects and date inputs
 * auto-submit on change. $filters still sources its current values from
 * src/JournalTradesQuery.php's buildJournalTradesFilters() (kept as the
 * one shared GET-param reader — its 'q' free-text field is still read
 * there for api/journal/trades-more.php's sake, just no longer exposed as
 * a control on this page — see the removed-search-box note below).
 *
 * Search box removed 2026-07-31 (state-sync fix pass) — redundant with the
 * Account Selector's own search, and having two separate search inputs on
 * one page was a source of confusion, not just clutter.
 *
 * $baseParams MUST include rn_account (the currently selected account, per
 * the Account Selector — the single source of truth for which account is
 * active) alongside rn_type/rn_challenge. This form submits via GET, which
 * replaces the whole query string with only the fields the form carries —
 * any state not threaded through $baseParams as a hidden field is silently
 * dropped the moment Filter or Clear is used, which previously caused the
 * selected account to revert to the default/most-recent one. See the call
 * site in renderRightNow() for the fix.
 */
function renderTradeJournalSection(array $filters, string $userId, $db, array $baseParams = []): void {
    $rnValidationCond = validationScopeSql(ValidationService::getWorkflowState($userId, $db));
    $instruments = $db->raw("SELECT DISTINCT instrument FROM trades WHERE user_id = :uid AND {$rnValidationCond} AND instrument IS NOT NULL AND instrument != '' ORDER BY instrument", [':uid' => $userId]);
    $sessions = ['Sydney', 'Tokyo', 'London', 'New York'];
    $classifications = $db->raw("SELECT DISTINCT trade_classification FROM trades WHERE user_id = :uid AND {$rnValidationCond} AND trade_classification IS NOT NULL ORDER BY trade_classification", [':uid' => $userId]);

    $cleanFilters = array_filter($filters, fn($v) => $v !== null && $v !== '');
    ?>
    <div class="rn-trade-journal">
    <div class="section-label"><span>Trade Journal</span></div>
    <form method="get" class="filter-bar" id="rn-tj-filter-form">
        <input type="hidden" name="workspace" value="active">
        <?php foreach ($baseParams as $bpk => $bpv): if ($bpv === null || $bpv === '') continue; ?>
        <input type="hidden" name="<?= htmlspecialchars($bpk) ?>" value="<?= htmlspecialchars($bpv) ?>">
        <?php endforeach; ?>
        <select name="instrument" title="Filter by instrument" aria-label="Filter by instrument"><option value="">All Instruments</option>
            <?php foreach ($instruments as $i): ?><option value="<?= htmlspecialchars($i['instrument']) ?>" <?= $filters['instrument'] === $i['instrument'] ? 'selected' : '' ?>><?= htmlspecialchars($i['instrument']) ?></option><?php endforeach; ?>
        </select>
        <select name="session" title="Filter by trading session" aria-label="Filter by trading session"><option value="">All Sessions</option>
            <?php foreach ($sessions as $s): ?><option value="<?= $s ?>" <?= $filters['session'] === $s ? 'selected' : '' ?>><?= $s ?></option><?php endforeach; ?>
        </select>
        <select name="classification" title="Filter by trade classification" aria-label="Filter by trade classification"><option value="">All Classifications</option>
            <?php foreach ($classifications as $c): $cv = $c['trade_classification']; ?><option value="<?= htmlspecialchars($cv) ?>" <?= $filters['classification'] === $cv ? 'selected' : '' ?>><?= htmlspecialchars(ucwords(str_replace('_', ' ', $cv))) ?></option><?php endforeach; ?>
        </select>
        <span class="filter-date-label" title="Filter trades from this date onward (mm/dd/yyyy). Leave &quot;To&quot; empty to show only this single day.">From</span>
        <input type="date" name="from" value="<?= htmlspecialchars($filters['from'] ?? '') ?>" placeholder="From (mm/dd/yyyy)" title="From date (mm/dd/yyyy). Leave &quot;To&quot; empty to show only trades on this single day" aria-label="From date (mm/dd/yyyy)">
        <span class="filter-date-label" title="Filter trades up to and including this date (mm/dd/yyyy).">To</span>
        <input type="date" name="to" value="<?= htmlspecialchars($filters['to'] ?? '') ?>" placeholder="To (mm/dd/yyyy)" title="To date (mm/dd/yyyy), inclusive of this day" aria-label="To date (mm/dd/yyyy)">
        <button type="submit" class="btn btn-primary btn-sm">Filter</button>
        <?php if (array_filter($cleanFilters, fn($v, $k) => !in_array($k, ['account_type'], true), ARRAY_FILTER_USE_BOTH)): ?><a href="<?= activeJourneysUrl($baseParams) ?>" class="btn btn-sm" style="align-self:center">Clear</a><?php endif; ?>
    </form>
    <script>
    (function () {
        var form = document.getElementById('rn-tj-filter-form');
        if (!form) return;
        // Selects and dates narrow the table as soon as a value is picked —
        // no extra click needed (the Filter button still works too, e.g.
        // with JS disabled).
        Array.prototype.forEach.call(form.querySelectorAll('select, input[type="date"]'), function (el) {
            el.addEventListener('change', function () { form.submit(); });
        });
    })();
    </script>
    </div>
    <?php
}

// ════════════════════════════════════════════════════════════════════
// SINGLE-TRADE 4-STAGE LIFECYCLE — Active Journeys → Trade → View Details.
//
// Reports already compares GROUPS of trades through the four stages
// (Declared Edge / Calculator Intent / Manual Outcome / Broker
// Reconciliation), one metric per row, aligned across four stage
// columns — see reports.php's "four-stage-summary" section and its
// .fs-stage-row/.fs-stage-card CSS. This is the exact same philosophy
// applied to ONE trade instead of a reporting period: every row below
// compares one metric across all four stages, never mixing two
// different things in one row. It reuses AnalyticsEngine's real,
// already-verified per-trade computation (getFourStageRaw(), a thin
// public wrapper around the same computeFourStageRaw() Reports' period
// aggregate already calls) rather than re-deriving any of Stage 1-4's
// math — this page only re-shapes those already-real numbers into a
// row-per-metric table and writes plain-English commentary about what
// changed between stages.
//
// Visual language is reused, not reinvented: this page has its own
// established blue/purple/gold/green stage-accent palette
// (--rn-blue/--rn-purple/--rn-gold/--rn-green, defined above) that maps
// 1:1 onto Reports' own stage colors (var(--primary)/#A78BFA/
// var(--warning)/var(--positive)) — reused here under their existing
// Right Now names rather than importing Reports' token names into a
// page that already has matching ones of its own.
//
// Strategy/Strategy Version are shown once in the page header (mirrors
// Reports' own "Report for {strategy} (vN)" header) rather than as a
// table row, since which strategy is attached doesn't evolve stage to
// stage the way Risk %/RR/Entry/etc. do. Notes are rendered as a plain
// callout below the table for the same reason — free text captured at
// any point isn't a stage-comparable metric.
// ════════════════════════════════════════════════════════════════════

/**
 * One table cell: a formatted value, or null (renders "Not available yet"),
 * plus an optional explanatory tooltip. $isHtml marks a value that is
 * already-safe, pre-built markup (an expandable "+N more" list, a
 * tone-colored badge) that must NOT be re-escaped by the render loop —
 * every other cell is plain text and still gets htmlspecialchars()'d there.
 */
function tl4Cell($value, ?string $note = null, bool $isHtml = false): array {
    return ['v' => $value, 'note' => $note, 'html' => $isHtml];
}

/** Renders one tl4Cell() as a standalone <td> string. Same fallback/tooltip convention as tl4RenderBlockTable(). Used by the MT5 Trade Record and Planned vs. Broker-Executed tables. */
function tl4TdFromCell(array $cell, string $extraClass = ''): string {
    $display = $cell['v'] !== null ? $cell['v'] : '-';
    $isHtml = !empty($cell['html']);
    $classAttr = trim(($cell['v'] === null ? 'tl4-na' : '') . ' ' . $extraClass);
    return '<td' . ($classAttr !== '' ? ' class="' . htmlspecialchars($classAttr) . '"' : '') . ($cell['note'] ? ' title="' . htmlspecialchars($cell['note']) . '"' : '') . '>'
        . ($isHtml ? $display : htmlspecialchars((string)$display)) . '</td>';
}

/** A short label with a colored status dot — used for derived, tone-carrying insights (Emotional State) rather than a plain value. */
function tl4ToneCell(?string $label, string $tone, ?string $note = null): array {
    if ($label === null) return tl4Cell(null, $note);
    $html = '<span class="tl4-tone-dot tl4-tone-' . htmlspecialchars($tone) . '"></span>' . htmlspecialchars($label);
    return tl4Cell($html, $note, true);
}

/**
 * Renders a possibly-long list as "A, B, C +N more" with a click-to-expand
 * toggle for the rest — used for Stage 1's Instrument(s)/Session(s)/
 * Timeframe(s) rows, where a strategy may declare anywhere from 2 to 100+
 * values (CLAUDE.md's own worked example: "EURUSD • GBPUSD • XAUUSD • +17
 * more"). Never lets the table overflow or break layout regardless of list
 * size. Toggled by tl4ToggleExpand() (inline <script> near the end of this
 * file).
 */
function tl4ExpandableList(array $items, int $visible = 3): array {
    $items = array_values(array_filter($items, fn($v) => $v !== null && $v !== ''));
    if (empty($items)) return tl4Cell(null);
    $shown = array_slice($items, 0, $visible);
    $rest = array_slice($items, $visible);
    $html = htmlspecialchars(implode(', ', $shown));
    if (!empty($rest)) {
        $html .= ' <button type="button" class="tl4-expand-btn" onclick="tl4ToggleExpand(this)">+' . count($rest) . ' more</button>'
            . '<span class="tl4-expand-rest" style="display:none">, ' . htmlspecialchars(implode(', ', $rest)) . '</span>';
    }
    return tl4Cell($html, null, true);
}

/**
 * Overall pre-trade psychology score — averages ONLY the trader's real
 * Calculator mood_before/energy_before/confidence_before (1-5 each), per
 * explicit instruction: never derived from risk%/SL/TP/RR or any other
 * calculator value, since those are execution intention (superseded by
 * Draft Resolution/MT5), while mood/energy/confidence are the trader's
 * actual pre-trade mindset and nothing later ever re-measures it.
 */
function tl4PsychologyScore($mood, $energy, $confidence): ?array {
    $vals = array_values(array_filter([$mood, $energy, $confidence], fn($v) => $v !== null && $v !== ''));
    if (empty($vals)) return null;
    $avg = array_sum($vals) / count($vals);
    $label = match (true) {
        $avg >= 4.5 => 'Calm, Focused, Confident',
        $avg >= 3.5 => 'Steady, Composed',
        $avg >= 2.5 => 'Neutral, Mixed',
        $avg >= 1.5 => 'Uneasy, Distracted',
        default => 'Anxious, Unfocused',
    };
    return ['score' => round($avg, 1), 'label' => $label, 'n' => count($vals)];
}

/**
 * Whether this trade has genuinely closed — the authoritative source is
 * trades.status, NOT stage3['win_loss']. Bug found during verification:
 * computeFourStageRaw()'s effective_pnl cascade falls back to
 * projected_pnl (a purely hypothetical "if it hits target" figure) when
 * challenge_pnl/broker_pnl are both null — which produces a non-null
 * win_loss for a trade that is genuinely still `open`, with no
 * actual_exit_price at all. Every "has this trade closed yet" check in
 * this file's derive-* / coaching / block-building functions must gate on
 * this, not on win_loss alone, or a still-open trade renders a fabricated
 * "win" outcome.
 */
function tl4IsClosed(array $trade): bool {
    return ($trade['status'] ?? null) === 'closed';
}

function tl4ConfidenceLabel(int $n): string {
    return match ($n) {
        1 => 'Very Low', 2 => 'Low', 3 => 'Moderate', 4 => 'High', 5 => 'Very High', default => '',
    };
}

/**
 * Manual Exit Reason (Stage 3/4) — an interpretation of the CONFIRMED Draft
 * Resolution outcome, never a new typed-in reason. Built purely from
 * already-confirmed values (tp_level_hit, win_loss, r_multiple vs. the
 * planned Risk:Reward), rendered as a natural-language sentence (2026-08-04
 * wording pass — never a database-style label like "Stop Loss Hit") that
 * makes an early manual close legible ("secured +2.0R vs. a planned +3.0R
 * target") instead of a bare "Manual Close".
 *
 * Unused as of 2026-08-13. Its only render site, the Manual Exit Reason
 * row, was removed. Left in place, not deleted.
 */
function tl4DeriveExitReason(array $raw, array $trade): array {
    $s2 = $raw['stage2']; $s3 = $raw['stage3']; $s4 = $raw['stage4'];
    $winLoss = tl4IsClosed($trade) ? ($s3['win_loss'] ?? null) : null;
    if ($winLoss === null) {
        $note = 'This trade hasn\'t closed yet.';
        return ['stage3' => tl4Cell(null, $note), 'stage4' => tl4Cell(null, $note)];
    }
    $plannedRr = $s2['risk_reward'];
    $realizedR = $s3['r_multiple'];

    // Real, declared "Managed Exit" data (2026-08-03) takes priority over
    // price-tolerance inference below — a genuine trader declaration at
    // Draft Resolution, not a guess derived from matching prices against
    // configured TP/SL levels. Natural-language sentence, not a robotic
    // label, per explicit instruction — includes the actual realized R
    // (from the real confirmed price/P&L) alongside the declared one, so
    // any difference from spread/slippage/discretionary management is
    // visible, not hidden.
    // Item 9 (2026-08-04): natural-language sentences, never a database-
    // style label ("Stop Loss Hit") — every branch below is still built
    // purely from already-confirmed values (exit_classification,
    // tp_level_hit, win_loss, r_multiple vs. the planned Risk:Reward),
    // never a fabricated reason.
    if ($s3['exit_classification'] === 'breakeven') {
        $label = 'The trade closed at breakeven: the confirmed stop loss matched the confirmed entry price.';
        $note = 'Declared at Draft Resolution: the confirmed Stop Loss matched the confirmed Entry Price, a deterministic rule.';
    } elseif ($s3['exit_classification'] === 'managed_exit' && $s3['declared_r_multiple'] !== null) {
        $declaredR = $s3['declared_r_multiple'];
        $label = 'Managed Exit at +' . number_format($declaredR, 2) . 'R'
            . ($plannedRr !== null ? ' before the planned +' . number_format($plannedRr, 2) . 'R target.' : '.')
            . ($realizedR !== null ? ' Actual realized outcome: ' . ($realizedR >= 0 ? '+' : '') . number_format($realizedR, 2) . 'R.' : '');
        $note = 'Declared at Draft Resolution as a real trade-management decision, not inferred from price matching. The actual submitted exit price/P&L remain the source of truth for the realized outcome.';
    } elseif ($s3['tp_level_hit'] !== null) {
        $label = 'The trade reached its planned Take Profit ' . $s3['tp_level_hit'] . ' target.';
        $note = 'Derived from the confirmed exit price, target hit, and realized R at Draft Resolution, not a separately typed-in reason.';
    } elseif ($winLoss === 'loss' && $s3['actual_exit_price'] !== null && $s3['actual_sl_price'] !== null && abs($s3['actual_sl_price']) > 0.0000001
        && abs($s3['actual_exit_price'] - $s3['actual_sl_price']) <= abs($s3['actual_sl_price']) * AnalyticsEngine::CALC_PRICE_TOLERANCE_RATIO) {
        $label = 'The trade reached its planned stop loss.';
        $note = 'Derived from the confirmed exit price, target hit, and realized R at Draft Resolution, not a separately typed-in reason.';
    } elseif ($winLoss === 'breakeven') {
        $label = 'The trade was manually closed at breakeven.';
        $note = 'Derived from the confirmed exit price, target hit, and realized R at Draft Resolution, not a separately typed-in reason.';
    } elseif ($winLoss === 'win' && $realizedR !== null && $plannedRr !== null && $plannedRr > 0 && $realizedR < $plannedRr * 0.75) {
        $label = 'The trade was manually closed before reaching its planned target, securing +' . number_format($realizedR, 2) . 'R against a planned +' . number_format($plannedRr, 2) . 'R.';
        $note = 'Derived from the confirmed exit price, target hit, and realized R at Draft Resolution, not a separately typed-in reason.';
    } elseif ($winLoss === 'win') {
        $label = 'The trade was manually closed near its planned target.';
        $note = 'Derived from the confirmed exit price, target hit, and realized R at Draft Resolution, not a separately typed-in reason.';
    } elseif ($winLoss === 'loss') {
        $label = $realizedR !== null
            ? ('The trade was manually closed early, cutting the loss at ' . number_format($realizedR, 2) . 'R before reaching its planned stop loss.')
            : 'The trade was manually closed as a loss, before reaching its planned stop loss.';
        $note = 'Derived from the confirmed exit price, target hit, and realized R at Draft Resolution, not a separately typed-in reason.';
    } else {
        $label = 'The trade was manually closed.';
        $note = 'Derived from the confirmed exit price, target hit, and realized R at Draft Resolution, not a separately typed-in reason.';
    }
    $stage3Cell = tl4Cell($label, $note);
    if (!empty($s4['reconciled'])) {
        $stage4Cell = tl4Cell($label, 'Confirmed by broker reconciliation, same exit mechanics, not independently re-derived.');
    } else {
        $stage4Cell = tl4Cell(null, 'Awaiting MT5 broker reconciliation. This will confirm the exit mechanics once a matching MT5 statement is uploaded.');
    }
    return ['stage3' => $stage3Cell, 'stage4' => $stage4Cell];
}

/**
 * Post-Trade Emotional State — Stage 3/4 (renamed from "Emotional State
 * (After Trade)", 2026-08-04, per explicit instruction that this is a
 * distinct, POST-trade concept and must never inherit Stage 2's pre-trade
 * psychology wording). A real, deterministic POST-TRADE PSYCHOLOGY SCORE
 * (2026-08-03 correctness pass), not a static classification-to-label
 * lookup. Computed from two already-confirmed Draft Resolution signals —
 * never self-reported, never guessed:
 *   1. A base score per the confirmed trade_classification (sp_classify_
 *      trade's own real output — a deterministic mapping, not invented).
 *   2. An adjustment when a win closed well short of its planned R:R
 *      target (an early exit — real, computed from r_multiple vs. the
 *      planned Risk:Reward).
 * The score maps deterministically to a natural-language SENTENCE (2026-08-04
 * — replaces bare tone labels like "Composed, Settled") via
 * tl4EmotionalSummary() below, keyed only on (classification, earlyExit,
 * win_loss) — the same trade always produces the same sentence, since
 * nothing here is randomized. The numeric score and its inputs are still
 * shown in the hover tooltip for transparency. Returns "Insufficient
 * psychology data." (exact wording requested) when the trade hasn't
 * closed/been classified yet — never fabricated.
 */
function tl4EmotionalSummary(string $classification, bool $earlyExit, string $winLoss): string {
    return match ($classification) {
        'disciplined_win' => $earlyExit
            ? 'Maintained composure after following the trading plan, closing the position before the full planned target rather than letting it run.'
            : 'Maintained composure after following the trading plan closely, through to the planned outcome.',
        'controlled_loss' => 'Accepted the planned loss without emotional escalation.',
        'breakeven' => 'Closed the trade calmly at breakeven, without any emotional escalation.',
        'elevated_win' => $earlyExit
            ? 'Showed some impatience, risk ran above the declared plan and the position was closed early, before the full target.'
            : 'Traded with more risk than declared, but stayed composed through to the result.',
        'loss_high_risk' => 'Took on risk beyond the declared plan, and the resulting loss likely carried some frustration.',
        'tilt_warning' => 'Shows signs of tilt: this trade\'s risk pattern is consistent with emotional decision-making taking over.',
        'revenge_trade' => 'Consistent with a revenge trade: an emotionally reactive attempt to immediately recover a prior loss.',
        default => 'The emotional pattern for this trade could not be determined from the available data.',
    };
}
function tl4DeriveEmotionalState(array $raw, array $trade): array {
    $s2 = $raw['stage2']; $s3 = $raw['stage3']; $s4 = $raw['stage4'];
    $classification = tl4IsClosed($trade) ? ($trade['trade_classification'] ?? null) : null;
    $winLoss = tl4IsClosed($trade) ? ($s3['win_loss'] ?? null) : null;
    if ($winLoss === null || $classification === null) {
        $waitNote = 'Insufficient psychology data.';
        return ['stage3' => tl4Cell(null, $waitNote), 'stage4' => tl4Cell(null, $waitNote), 'score' => null, 'score_label' => null];
    }
    $base = match ($classification) {
        'disciplined_win' => 90, 'controlled_loss' => 70, 'breakeven' => 60,
        'elevated_win' => 75, 'loss_high_risk' => 40, 'tilt_warning' => 25,
        'revenge_trade' => 10, default => 50,
    };
    $plannedRr = $s2['risk_reward']; $realizedR = $s3['r_multiple'];
    $earlyExit = ($winLoss === 'win' && $realizedR !== null && $plannedRr !== null && $plannedRr > 0 && $realizedR < $plannedRr * 0.75);
    $score = max(0, min(100, $base - ($earlyExit ? 15 : 0)));
    $tone = match (true) {
        $score >= 80 => 'positive', $score >= 60 => ($earlyExit ? 'caution' : 'neutral'),
        $score >= 40 => 'caution', default => 'negative',
    };
    // Short qualitative label, same two-word-descriptor style as Stage 2's
    // own tl4PsychologyScore() labels — a compact companion for the
    // Psychology row's Stage 3/4 cells (see below), distinct from the full
    // sentence tl4EmotionalSummary() produces for Post-Trade Emotional
    // State. Both are derived from this SAME score, computed once here.
    $scoreLabel = match (true) {
        $score >= 80 => 'Calm, Disciplined',
        $score >= 60 => $earlyExit ? 'Relieved, Impatient' : 'Composed, Settled',
        $score >= 40 => 'Uneasy, Second-Guessing',
        $score >= 20 => 'Frustrated, Overextended',
        default => 'Reactive, Emotionally Driven',
    };
    $summary = tl4EmotionalSummary($classification, $earlyExit, $winLoss);
    $note = 'Post-trade psychology score: ' . $score . '/100, computed from the confirmed classification (' . rnClassificationNoun($classification) . ')'
        . ($earlyExit ? ', adjusted down for an early exit relative to the planned Risk:Reward target' : '') . '. Never self-reported, deterministic, so this trade always produces the same summary.';
    $stage3Cell = tl4ToneCell($summary, $tone, $note);
    if (!empty($s4['reconciled'])) {
        $stage4Cell = tl4ToneCell($summary, $tone, 'Same confirmed score, no separate broker-side emotional signal exists. MT5 statements carry no psychology data, so the confirmed post-trade read is the only available answer once reconciled.');
    } else {
        $stage4Cell = tl4Cell(null, 'Awaiting MT5 reconciliation.');
    }
    return ['stage3' => $stage3Cell, 'stage4' => $stage4Cell, 'score' => $score, 'score_label' => $scoreLabel, 'reconciled' => !empty($s4['reconciled'])];
}

/**
 * Confidence Calibration (Stage 3/4, renamed from "Confidence Outcome" per
 * explicit instruction). Compares Stage 2's real, already-captured pre-trade
 * confidence_before rating against the CONFIRMED Draft Resolution outcome —
 * checklist completion, declared-risk discipline, win/loss. Never a new
 * rating, never fabricated — every branch is a real comparison of two
 * already-known values.
 *
 * As of 2026-08-13, the returned 'stage3'/'stage4' cells have no render
 * site (that row was removed). 'sentence'/'actionable' are still used by
 * tl4CoachingSummary().
 */
function tl4DeriveConfidenceCalibration(array $raw, array $trade, ?array $checklist): array {
    $s1 = $raw['stage1']; $s3 = $raw['stage3']; $s4 = $raw['stage4'];
    $confidence = isset($trade['confidence_before']) && $trade['confidence_before'] !== null ? (int)$trade['confidence_before'] : null;
    $winLoss = tl4IsClosed($trade) ? ($s3['win_loss'] ?? null) : null;
    // 2026-08-04: a missing confidence_before (legacy trades, before the
    // calculator's Pre-Trade State card became mandatory) is no longer a
    // dead end. There's genuinely nothing to CALIBRATE (calibration needs
    // both a stated belief and an outcome), but real checklist/risk-
    // discipline data usually still exists — compute a real sentence from
    // whatever IS available rather than a bare "not captured" message.
    if ($confidence === null) {
        if ($winLoss === null) {
            $note = 'No pre-trade confidence rating was captured, and this trade hasn\'t closed yet.';
            return ['stage3' => tl4Cell(null, $note), 'stage4' => tl4Cell(null, $note), 'sentence' => null, 'actionable' => false];
        }
        $ticked = $checklist['ticked_pct'] ?? null;
        $riskOverage = $s1['available'] && $s1['risk_overage'] !== null;
        $bits = [];
        if ($ticked !== null) $bits[] = 'checklist completion was ' . number_format((float)$ticked, 0) . '%';
        if ($riskOverage) $bits[] = 'risk exceeded the declared plan';
        elseif ($s1['available']) $bits[] = 'risk stayed within the declared plan';
        $sentence = 'No pre-trade confidence rating was captured for this trade, so it can\'t be calibrated against the outcome, but '
            . (!empty($bits) ? implode(' and ', $bits) . ', closing a ' . $winLoss . '.' : ('the trade closed a ' . $winLoss . ' with no other execution-discipline data recorded.'));
        $stage3Cell = tl4Cell($sentence, 'No confidence rating exists to calibrate against. This instead compares checklist completion and declared-risk discipline against the confirmed outcome, using whatever real data was recorded.');
        if (!empty($s4['reconciled'])) {
            $stage4Cell = tl4Cell($sentence, 'Same confirmed comparison, broker data doesn\'t change this either.');
        } else {
            $stage4Cell = tl4Cell(null, 'Awaiting MT5 reconciliation.');
        }
        return ['stage3' => $stage3Cell, 'stage4' => $stage4Cell, 'sentence' => $sentence, 'actionable' => false];
    }
    if ($winLoss === null) {
        $note = 'Awaiting a confirmed trade outcome to compare against the pre-trade confidence rating.';
        return ['stage3' => tl4Cell(null, $note), 'stage4' => tl4Cell(null, $note)];
    }
    $ticked = $checklist['ticked_pct'] ?? null;
    $confHigh = $confidence >= 4; $confLow = $confidence <= 2;
    $checklistHigh = $ticked !== null && $ticked >= 80;
    $checklistLow = $ticked !== null && $ticked < 60;
    $riskOverage = $s1['available'] && $s1['risk_overage'] !== null;

    // $actionable marks a genuine mismatch worth carrying into the next
    // trade (used by tl4CoachingSummary() below) — the two "matched
    // outcome"/generic branches are confirmatory, not corrective, so they
    // don't need a forward-looking coaching callout of their own.
    $actionable = true;
    if ($confHigh && $riskOverage) {
        $sentence = 'High confidence combined with excessive risk, worth checking whether confidence is being used to justify oversized positions.';
    } elseif ($confHigh && $checklistLow) {
        $sentence = 'High confidence conflicted with poor checklist completion (' . number_format((float)$ticked, 0) . '%). The setup may have felt strong without being fully verified.';
    } elseif ($confLow && $checklistHigh) {
        $sentence = 'Excellent checklist completion (' . number_format((float)$ticked, 0) . '%) despite low pre-trade confidence. The process was followed even though conviction was low.';
    } elseif ($confHigh && $winLoss === 'loss') {
        $sentence = 'High confidence did not protect against a loss, worth revisiting what this setup had in common with other losing trades.';
    } elseif ($confLow && $winLoss === 'win') {
        $sentence = 'Low confidence still produced a win. This setup may have been stronger than it felt going in.';
    } elseif (($confHigh && $winLoss === 'win') || ($confLow && $winLoss === 'loss')) {
        $sentence = 'Confidence matched outcome: pre-trade conviction lined up with how the trade actually played out.';
        $actionable = false;
    } else {
        $sentence = 'Confidence was moderate (' . $confidence . '/5); the trade closed a ' . $winLoss . '.';
        $actionable = false;
    }
    $stage3Cell = tl4Cell($sentence, 'Compares Stage 2\'s real pre-trade confidence rating against the confirmed checklist completion, risk discipline, and outcome at Draft Resolution, never a new rating.');
    if (!empty($s4['reconciled'])) {
        $stage4Cell = tl4Cell($sentence, 'Same confirmed comparison, broker data doesn\'t change confidence calibration.');
    } else {
        $stage4Cell = tl4Cell(null, 'Awaiting MT5 reconciliation.');
    }
    return ['stage3' => $stage3Cell, 'stage4' => $stage4Cell, 'sentence' => $sentence, 'actionable' => $actionable];
}

/**
 * Setup Confidence's Stage 3/4 cells (2026-08-04, per explicit instruction).
 * Draft Resolution and MT5 reconciliation never re-collect a confidence
 * rating — the schema has no such field, and none is invented here.
 * Instead, this computes whether EXECUTION matched the calculator's setup
 * with the same precision the trader's Stage 2 confidence implied: "was it
 * exact as calculator, over-leveraged, etc." — reusing the already-
 * computed adherence/executed-risk signals from Block 2's own rows
 * (entry_adherent/sl_adherent/lot_adherent/executed_risk_pct at Stage 3,
 * reconciled_adherent/instrument_confirmed/actual_broker_risk_pct at
 * Stage 4, gated on real MT5 reconciliation) — never a guessed value.
 */
function tl4DeriveExecutionFidelity(array $raw): array {
    $s2 = $raw['stage2']; $s3 = $raw['stage3']; $s4 = $raw['stage4'];

    $flags3 = ['Entry price' => $s3['entry_adherent'], 'Stop loss' => $s3['sl_adherent'], 'Position size' => $s3['lot_adherent']];
    $known3 = array_filter($flags3, fn($v) => $v !== null);
    if (empty($known3)) {
        $stage3Cell = tl4Cell(null, 'Not enough confirmed data yet to assess execution fidelity for this trade.');
    } else {
        $deviations3 = array_keys(array_filter($known3, fn($v) => $v === false));
        $riskNote3 = '';
        if ($s3['executed_risk_pct'] !== null && $s2['risk_pct'] !== null && abs($s3['executed_risk_pct'] - $s2['risk_pct']) > 0.01) {
            $direction3 = $s3['executed_risk_pct'] > $s2['risk_pct'] ? 'over-leveraged' : 'under-leveraged';
            $riskNote3 = ' Executed at ' . tl4Pct($s3['executed_risk_pct']) . ' risk vs. a planned ' . tl4Pct($s2['risk_pct']) . ', ' . $direction3 . ' relative to the calculator.';
        }
        $sentence3 = empty($deviations3)
            ? ('Executed exactly as planned: ' . strtolower(implode(', ', array_keys($known3))) . ' all matched the calculator within tolerance.' . $riskNote3)
            : (implode(' and ', $deviations3) . ' differed from the plan by more than the normal tolerance.' . $riskNote3);
        $stage3Cell = tl4Cell($sentence3, 'Compares the confirmed Draft Resolution entry/stop/lot size against Stage 2\'s plan, within the same tolerance used throughout this table, never a re-typed confidence rating.');
    }

    if (empty($s4['reconciled'])) {
        $stage4Cell = tl4Cell(null, 'Awaiting MT5 reconciliation.');
    } else {
        $flags4 = ['Position size' => $s4['reconciled_adherent'], 'Instrument' => $s4['instrument_confirmed']];
        $known4 = array_filter($flags4, fn($v) => $v !== null);
        if (empty($known4)) {
            $stage4Cell = tl4Cell(null, 'Reconciled, but no broker lot size or instrument match was captured for this trade yet.');
        } else {
            $deviations4 = array_keys(array_filter($known4, fn($v) => $v === false));
            $riskNote4 = '';
            if ($s4['actual_broker_risk_pct'] !== null && $s2['risk_pct'] !== null && abs($s4['actual_broker_risk_pct'] - $s2['risk_pct']) > 0.01) {
                $direction4 = $s4['actual_broker_risk_pct'] > $s2['risk_pct'] ? 'over-leveraged' : 'under-leveraged';
                $riskNote4 = ' Broker-verified risk was ' . tl4Pct($s4['actual_broker_risk_pct']) . ' vs. a planned ' . tl4Pct($s2['risk_pct']) . ', ' . $direction4 . ' relative to the calculator.';
            }
            $sentence4 = empty($deviations4)
                ? ('Broker-verified execution matched the plan: ' . strtolower(implode(' and ', array_keys($known4))) . ' confirmed.' . $riskNote4)
                : ('Broker data shows ' . strtolower(implode(' and ', $deviations4)) . ' differed from the plan.' . $riskNote4);
            $stage4Cell = tl4Cell($sentence4, 'The only broker-verifiable execution signals available for this trade. MT5 carries no direct "confidence" concept, so this is what "verified against the only source of truth" means at this stage.');
        }
    }

    return ['stage3' => $stage3Cell, 'stage4' => $stage4Cell];
}

/**
 * Real, computed compliance percentages — never a fabricated 25/25/25/25.
 * Each category is null (excluded from the pie/average) when that stage
 * genuinely has nothing computable yet, rather than defaulting to 0 or 100.
 */
function tl4ComplianceScores(array $raw): array {
    $s1 = $raw['stage1']; $s3 = $raw['stage3']; $s4 = $raw['stage4'];
    $gc = $raw['geometric_consistency'];

    $declaredEdge = null;
    if ($s1['available']) {
        $checks = 0; $passed = 0;
        $checks++; if (empty($s1['contract_constrained']) && $s1['risk_overage'] === null) $passed++;
        $checks++; if ($s1['rr_mismatch'] === null || abs($s1['rr_mismatch']) <= 0.15) $passed++;
        $declaredEdge = round(($passed / $checks) * 100, 1);
    }

    $calcAdherence = null;
    if ($gc['declared_rr'] !== null || $gc['declared_risk_pct'] !== null) {
        $checks = 0; $passed = 0;
        if ($gc['declared_rr'] !== null) { $checks++; if (!$gc['rr_mismatch_flag']) $passed++; }
        if ($gc['declared_risk_pct'] !== null) { $checks++; if (!$gc['risk_pct_mismatch_flag']) $passed++; }
        $calcAdherence = $checks > 0 ? round(($passed / $checks) * 100, 1) : null;
    }

    $manualExecution = null;
    $flags = array_filter([$s3['entry_adherent'], $s3['sl_adherent'], $s3['lot_adherent']], fn($v) => $v !== null);
    if (!empty($flags)) {
        $manualExecution = round((count(array_filter($flags)) / count($flags)) * 100, 1);
    }

    $brokerVerification = null;
    if (!empty($s4['reconciled'])) {
        $flags = array_filter([$s4['reconciled_adherent'], $s4['instrument_confirmed']], fn($v) => $v !== null);
        $brokerVerification = !empty($flags) ? round((count(array_filter($flags)) / count($flags)) * 100, 1) : null;
    }

    $scores = [
        'declared_edge' => ['label' => 'Declared Edge Integrity', 'score' => $declaredEdge],
        'calculator_adherence' => ['label' => 'Calculator Adherence', 'score' => $calcAdherence],
        'manual_execution' => ['label' => 'Manual Execution Quality', 'score' => $manualExecution],
        'broker_verification' => ['label' => 'Broker Verification Accuracy', 'score' => $brokerVerification],
    ];
    $available = array_filter(array_column($scores, 'score'), fn($v) => $v !== null);
    $scores['overall'] = !empty($available) ? round(array_sum($available) / count($available), 1) : null;
    return $scores;
}

/**
 * Stage Compliance Overview ring — a dependency-free inline SVG (2026-08-03
 * rebuild), replacing an earlier Chart.js/CDN implementation that (a) had
 * an external-network dependency that couldn't be guaranteed to load, and
 * (b) was conceptually wrong: it sized each arc by the category's SCORE,
 * when the approved prototype's ring always shows 4 EQUAL quadrants — one
 * per stage, structural, not score-weighted — with the score conveyed as
 * text (in the legend) and as the quadrant's opacity (a real, computed,
 * linear mapping from the score, never an arbitrary shade). A category
 * with no computable score yet renders as a faint, low-opacity quadrant
 * rather than being silently omitted, so the ring always shows all 4
 * stages regardless of how much data currently exists.
 */
function tl4RenderComplianceRing(array $compliance): void {
    $categories = ['declared_edge' => '#60A5FA', 'calculator_adherence' => '#A78BFA', 'manual_execution' => '#E09B3A', 'broker_verification' => '#26A69A'];
    $n = count($categories);
    $share = 100 / $n; // circle circumference is ~100 units at r=15.9155, so dasharray values map directly to percent
    $gap = 1.2; // small visual gap between quadrants
    $offset = 0;
    $overall = $compliance['overall'];
    ?>
    <svg viewBox="0 0 36 36" class="tl4-ring-svg" role="img" aria-label="Stage compliance overview">
        <?php foreach ($categories as $key => $color):
            $score = $compliance[$key]['score'];
            $opacity = $score === null ? 0.15 : max(0.3, min(1, $score / 100));
            $dash = max(0, $share - $gap);
        ?>
        <circle cx="18" cy="18" r="15.9155" fill="none" stroke="<?= $color ?>" stroke-width="3.4" stroke-opacity="<?= $opacity ?>"
            stroke-linecap="round" stroke-dasharray="<?= number_format($dash, 3) ?> <?= number_format(100 - $dash, 3) ?>"
            stroke-dashoffset="<?= number_format(-$offset, 3) ?>" transform="rotate(-90 18 18)" />
        <?php $offset += $share; endforeach; ?>
        <text x="18" y="17.5" text-anchor="middle" class="tl4-ring-value"><?= $overall !== null ? number_format($overall, 0) . '%' : '-' ?></text>
        <text x="18" y="22.5" text-anchor="middle" class="tl4-ring-label">Integrity</text>
    </svg>
    <?php
}

function tl4Money($v): ?string { return $v === null ? null : (($v >= 0 ? '$' : '-$') . number_format(abs($v), 2)); }
function tl4Pct($v, int $d = 2): ?string { return $v === null ? null : number_format($v, $d) . '%'; }
/** Target ratios only (Planned RR) — this app's stored convention is reward-per-1-unit-risk, so risk is always the fixed "1" (see AnalyticsEngine::fmtRr()'s doc comment) — never a bare "X:1". */
function tl4Rr($v): ?string { return $v === null ? null : '1 : ' . number_format($v, 2); }
/** Realized R-multiples (Actual RR) — a signed multiple of risk actually made/lost, not a target ratio, so it's formatted distinctly from tl4Rr(). */
function tl4RMultiple($v): ?string { return $v === null ? null : (($v >= 0 ? '+' : '') . number_format($v, 2) . 'R'); }
function tl4Price($v): ?string {
    if ($v === null || $v === '') return null;
    $s = number_format((float)$v, 5, '.', '');
    if (str_contains($s, '.')) $s = rtrim(rtrim($s, '0'), '.');
    return $s === '' ? '0' : $s;
}
function tl4PipsFmt($v): ?string { return $v === null ? null : (($v >= 0 ? '+' : '') . number_format($v, 1)); }

/**
 * Stage 3 cell for a TP level's PRICE (2026-08-03 correctness pass —
 * replaces the old tl4TpOutcomeCell(), a real data-lineage bug: it showed
 * "✓ Hit"/blank instead of the actual price, which meant a genuinely known
 * planned TP price disappeared from Stage 3 whenever it wasn't the exit
 * level. Draft Resolution DOES know the planned TP price — this schema has
 * no separate "actual TP price" capture, so it's always the same value
 * carried forward, never independently reset — whether this level was the
 * actual exit is conveyed via this cell's own note and Manual Exit Reason
 * (Block 3), not a separate row.
 */
function tl4TpPriceCellStage3(?float $tpPrice, int $level, array $s3, ?string $winLoss): array {
    if ($tpPrice === null) {
        return tl4Cell(null, "No TP{$level} target was set on this trade.");
    }
    if ($winLoss === null) {
        return tl4Cell(tl4Price($tpPrice), 'Same planned target, carried forward. Trade hasn\'t closed yet.');
    }
    if ($s3['tp_level_hit'] === $level) {
        return tl4Cell(tl4Price($tpPrice), 'This was the confirmed exit level. See Reward $ below.');
    }
    return tl4Cell(tl4Price($tpPrice), 'Same planned target, carried forward. The trade didn\'t exit at this level.');
}

/**
 * Item 1 (2026-08-04) — the TP3+ row's collapsed state shows nothing but a
 * "View ▾" toggle (no leaked TP3 price up front, per explicit instruction);
 * expanding reveals every configured target after TP2 — TP3, TP4, TP5, ...
 * — each on its own line. Shared by Stage 2 (planned), Stage 3 (confirmed,
 * carried forward), and Stage 4 (broker-confirmed, carried forward) so all
 * three read identically apart from their own note text. Never makes the
 * table taller until the trader clicks — same tl4ToggleDetail() mechanic
 * already used by Position Size's "View Calculation".
 */
function tl4Tp3PlusCell(array $levels, ?string $note): array {
    if (empty($levels)) return tl4Cell(null, 'No TP3+ targets were set on this trade.');
    $rows = implode('', array_map(
        fn($l) => '<span class="tl4-tp-expand-row">TP' . $l['level'] . ' ' . htmlspecialchars(tl4Price($l['price'])) . '</span>',
        $levels
    ));
    $html = '<button type="button" class="tl4-expand-btn" onclick="tl4ToggleDetail(this)">View &#9662;</button>'
        . '<span class="tl4-tp-expand" style="display:none">' . $rows . '</span>';
    return tl4Cell($html, $note, true);
}

/** Stage 4 cell for a TP level's PRICE — pending until reconciled (only Stage 4 remains gated on MT5), then the same planned price with a broker-verified hit/miss note. */
function tl4TpPriceCellStage4(?float $tpPrice, int $level, array $s4, ?string $mt5Note): array {
    if ($tpPrice === null) return tl4Cell(null, "No TP{$level} target was set on this trade.");
    if (empty($s4['reconciled'])) return tl4Cell(null, $mt5Note);
    if ($s4['broker_exit_price'] !== null && abs($s4['broker_exit_price'] - $tpPrice) <= abs($tpPrice) * AnalyticsEngine::CALC_PRICE_TOLERANCE_RATIO) {
        return tl4Cell(tl4Price($tpPrice), 'Broker-verified: this was the confirmed exit level.');
    }
    return tl4Cell(tl4Price($tpPrice), 'Broker-confirmed, but the exit didn\'t match this level.');
}

/** Real, computed breakdown of the calculator's OWN position-sizing math for Stage 2's "Position Size (Lots)" cell — literally the calculator's already-performed arithmetic, never recalculated or estimated. */
function tl4CalcDetailHtml(array $s2, array $trade): string {
    $balance = isset($trade['balance_at_entry']) && $trade['balance_at_entry'] !== null ? (float)$trade['balance_at_entry']
        : (isset($trade['account_size_at_entry']) && $trade['account_size_at_entry'] !== null ? (float)$trade['account_size_at_entry'] : null);
    // Guarded at >= 1 pip/point, not just > 0 — some instruments (e.g. this
    // app's raw-points convention for commodities/metals, see
    // rnPipsOrPoints()) legitimately store a sub-1 distance, and dividing
    // real risk $ by a near-zero distance produces a technically-correct
    // but meaningless-looking per-unit value. Below that floor, this row
    // is simply omitted rather than shown misleadingly.
    $pipValue = ($s2['max_loss'] !== null && $s2['max_loss_pips'] !== null && $s2['max_loss_pips'] >= 1)
        ? round($s2['max_loss'] / $s2['max_loss_pips'], 2) : null;
    $rows = [
        'Risk %' => $s2['risk_pct'] !== null ? tl4Pct($s2['risk_pct']) : null,
        'Account Balance' => $balance !== null ? tl4Money($balance) : null,
        'Risk Amount' => $s2['max_loss'] !== null ? tl4Money($s2['max_loss']) : null,
        'Stop Loss Distance' => $s2['max_loss_pips'] !== null ? (number_format($s2['max_loss_pips'], 1) . ' pips') : null,
        'Pip Value' => $pipValue !== null ? ('$' . number_format($pipValue, 2) . '/pip') : null,
        'Computed Lot Size' => $s2['lots'] !== null ? (tl4Price($s2['lots']) . ' lots') : null,
    ];
    $html = '';
    foreach ($rows as $label => $val) {
        if ($val === null) continue;
        $html .= '<span class="tl4-calc-row"><span class="tl4-calc-label">' . htmlspecialchars($label) . ':</span> ' . htmlspecialchars($val) . '</span>';
    }
    return $html;
}

/**
 * Assembles the single-trade 4-Stage Lifecycle as a CASCADE of four
 * progressively-narrower comparison tables ("blocks"), matching the
 * approved prototype (2026-08-03 redesign): each block only carries the
 * stage columns that still have something new to contribute — Stage 1
 * (Declared Edge) has exactly 5 rows and nothing else ("Nothing else" per
 * spec), so once those are shown, Stage 1 drops out of every later block
 * entirely rather than repeating empty cells. Each returned block is
 * ['stages' => [1,2,3,4] | [2,3,4] | [3,4] | [4], 'rows' => [['label'=>..,
 * 'cells'=>[...one per stage in 'stages', in order]]], 'banner' =>
 * ['title'=>.., 'sub'=>.., 'color'=>.., 'pending'=>bool]].
 *
 * A cell with a null value renders "Not available yet" — every cell in
 * this table carries a 'note' explaining WHY (awaiting MT5 reconciliation,
 * not applicable to this stage, no strategy attached, etc.) rather than
 * leaving that unexplained, per the "never dump a bare N/A" rule.
 */
function tl4BuildLifecycleBlocks(array $raw, array $trade, ?array $checklist): array {
    $s1 = $raw['stage1']; $s2 = $raw['stage2']; $s3 = $raw['stage3']; $s4 = $raw['stage4'];
    $direction = $raw['direction'] ?? 'long';
    $reconciled = !empty($s4['reconciled']);
    // 2026-08-19 fix: was 'Awaiting MT5 reconciliation.', repeated inline
    // down every single unreconciled Stage 4 cell in both tables below,
    // real clutter per direct instruction. The Stage 4 column header
    // already appends "(Pending)" once (tl4RenderBlockTable(), untouched),
    // and the Broker Reconciliation section directly beneath this table
    // already carries one clear "Broker Match Pending" banner + upload
    // link (tl4RenderMt5ReconciliationSection()). A null note here falls
    // back to a plain "-" per cell, per the existing render convention,
    // instead of repeating the same sentence a dozen times down one
    // column.
    $mt5Note = null;
    // Gated on trades.status, not just win_loss's own presence — bug found
    // during verification: computeFourStageRaw()'s effective_pnl cascade
    // falls back to projected_pnl (a hypothetical "if it hits target"
    // figure) even for a genuinely still-open trade, which would otherwise
    // render a fabricated win/loss outcome. See tl4IsClosed()'s doc comment.
    $winLoss = tl4IsClosed($trade) ? ($s3['win_loss'] ?? null) : null;

    // tl4DeriveExitReason()/tl4DeriveConfidenceCalibration()/
    // tl4DeriveEmotionalState()/tl4DeriveExecutionFidelity() no longer
    // called here. tl4CoachingSummary() still calls
    // tl4DeriveConfidenceCalibration() on its own.

    // ════════════════════════════════════════════════════════════
    // BLOCK 1 — DECLARED EDGE (all 4 stage columns). Exactly 5 rows,
    // nothing else — this stage is the strategy snapshot, not execution,
    // and ends the moment these are shown. Instrument(s)/Session(s)/
    // Timeframe(s) at Stage 1 show the strategy's FULL declared universe
    // (may be 2 or 100+ values) via tl4ExpandableList(); Stages 2-4 show
    // the one instrument/session/timeframe actually involved.
    // ════════════════════════════════════════════════════════════
    $rawTicker = strtoupper((string)($trade['instrument'] ?? ''));
    $c3Instrument = $s3['actual_instrument'] !== null ? strtoupper($s3['actual_instrument']) : null;

    $c1i = $s1['available']
        ? tl4ExpandableList(array_map('strtoupper', $s1['declared_instruments_list'] ?? []))
        : tl4Cell(null, 'No strategy attached to this trade.');
    if ($s1['available'] && $c1i['v'] === null) $c1i = tl4Cell(null, 'Instrument was not recorded.');
    $c2i = tl4Cell($rawTicker);
    $c3i = $c3Instrument !== null ? tl4Cell($c3Instrument) : tl4Cell($rawTicker, 'Not separately re-confirmed at Draft Resolution. Carried forward from the planned instrument.');
    if (!$reconciled) $c4i = tl4Cell(null, $mt5Note);
    elseif ($s4['instrument_confirmed'] === true) $c4i = tl4Cell($rawTicker, 'Broker-confirmed match.');
    elseif ($s4['instrument_confirmed'] === false) $c4i = tl4Cell($c3Instrument ?? $rawTicker, 'Broker data did not confirm the recorded instrument, worth a manual review.');
    else $c4i = tl4Cell($rawTicker, 'Reconciled, but no manually recorded instrument existed to cross-check against broker data.');

    $c1sess = $s1['available']
        ? tl4ExpandableList($s1['declared_sessions_list'] ?? [])
        : tl4Cell(null, 'No strategy attached to this trade.');
    if ($s1['available'] && $c1sess['v'] === null) $c1sess = tl4Cell(null, 'Session was not recorded.');
    $c2sess = !empty($s2['session']) ? tl4Cell($s2['session']) : tl4Cell(null, 'No session was recorded on this trade.');
    $c3sess = !empty($s2['session']) ? tl4Cell($s2['session'], 'Session is captured once at the calculator and never re-confirmed manually.') : tl4Cell(null, 'No session was recorded on this trade.');
    // Fixed 2026-08-13. Used to show Stage 2's session even while Pending.
    // Now gated on $reconciled like every other Stage 4 cell.
    $c4sess = $reconciled
        ? (!empty($s2['session'])
            ? tl4Cell($s2['session'], 'Same session as Stage 2/3. MT5 statements carry a raw timestamp only, not a named session, so this can\'t be independently broker-verified.')
            : tl4Cell(null, 'No session was recorded on this trade.'))
        : tl4Cell(null, $mt5Note);

    $c1tf = $s1['available']
        ? tl4ExpandableList($s1['declared_timeframes_list'] ?? [])
        : tl4Cell(null, 'No strategy attached to this trade.');
    if ($s1['available'] && $c1tf['v'] === null) $c1tf = tl4Cell(null, 'Timeframe was not recorded.');
    $c2tf = !empty($s2['timeframe']) ? tl4Cell($s2['timeframe']) : tl4Cell(null, 'No planned timeframe was recorded on this trade.');
    $c3tf = !empty($s3['actual_timeframe']) ? tl4Cell($s3['actual_timeframe'], 'Manually confirmed at Draft Resolution.')
        : (!empty($s2['timeframe']) ? tl4Cell($s2['timeframe'], 'Not separately re-confirmed at Draft Resolution. Carried forward from the planned timeframe.') : tl4Cell(null, 'No timeframe was recorded on this trade.'));
    if ($reconciled && !empty($s4['actual_timeframe'])) {
        $c4tf = tl4Cell($s4['actual_timeframe'], 'Manually captured. MT5 statements carry no chart-timeframe field to independently verify it against.');
    } elseif ($reconciled) {
        $c4tf = tl4Cell(null, 'No timeframe was manually recorded to carry forward. MT5 statements carry no chart-timeframe field of their own.');
    } else {
        $c4tf = tl4Cell(null, $mt5Note);
    }

    // 2026-08-19: R:R is mandatory at Edge Declaration going forward
    // (strategy.php's RULES.backtested_avg_rr, validated on every save),
    // same as timeframe — but a real, pre-existing legacy strategy can
    // still carry a null value (confirmed live: this exact "breakout"
    // strategy predates that rule). A genuinely missing declared R:R is a
    // real data gap, not a "hasn't happened yet" state like the Stage 4
    // pending cells above, so it still gets a short, honest, one-cell
    // note rather than a silent dash.
    $c1rr = $s1['available']
        ? tl4Cell(tl4Rr($s1['risk_reward']), $s1['risk_reward'] === null ? 'Risk:Reward was not recorded.' : null)
        : tl4Cell(null, 'No strategy attached to this trade.');
    $c2rr = tl4Cell(tl4Rr($s2['risk_reward']));
    $c3rr = $s2['risk_reward'] !== null ? tl4Cell(tl4Rr($s2['risk_reward'])) : tl4Cell(null, 'Risk:Reward was not recorded.');
    // Fixed 2026-08-13. Same leak as Session(s) above. Now gated on
    // $reconciled.
    $c4rr = $reconciled
        ? ($s2['risk_reward'] !== null
            ? tl4Cell(tl4Rr($s2['risk_reward']))
            : tl4Cell(null, 'Risk:Reward was not recorded.'))
        : tl4Cell(null, $mt5Note);

    $block1Rows = [
        ['label' => 'Instrument(s)', 'cells' => [$c1i, $c2i, $c3i, $c4i]],
        ['label' => 'Session(s)', 'cells' => [$c1sess, $c2sess, $c3sess, $c4sess]],
        ['label' => 'Timeframe(s)', 'cells' => [$c1tf, $c2tf, $c3tf, $c4tf]],
        ['label' => 'Risk %', 'cells' => [
            tl4Cell(tl4Pct($s1['available'] ? $s1['risk_pct'] : null), $s1['available'] ? null : 'No strategy attached to this trade.'),
            tl4Cell(tl4Pct($s2['risk_pct'])),
            tl4Cell(tl4Pct($s3['executed_risk_pct']), $s3['executed_risk_pct'] === null ? 'Needs both a planned lot size and a confirmed actual lot size to compute.' : 'Stage 2\'s planned risk %, scaled by the actual-vs-planned lot ratio.'),
            tl4Cell(tl4Pct($s4['actual_broker_risk_pct']), $reconciled ? 'Stage 2\'s planned risk %, scaled by the broker-verified lot ratio.' : $mt5Note),
        ]],
        ['label' => 'Risk : Reward', 'cells' => [$c1rr, $c2rr, $c3rr, $c4rr]],
    ];

    // ════════════════════════════════════════════════════════════
    // BLOCK 2 — CALCULATOR INTENT (Stage 2/3/4 only — Stage 1's edge
    // snapshot is fully declared above and has nothing further to say
    // about execution mechanics).
    // ════════════════════════════════════════════════════════════
    // Position Size (Lots) — 2026-08-19: the "View" expand (calculator's
    // own risk %/balance/SL-distance/pip-value math behind the lot size)
    // was removed per explicit instruction — it exposed internal
    // TraderAlign calculation detail the trader never needs to see here.
    // tl4CalcDetailHtml() itself is left in place, unused, rather than
    // deleted outright, in case a future dedicated "how was this
    // calculated" view wants it. Just the plain lot size shows now, same
    // as Stage 3/4 already did.
    $lotsS2Val = tl4Price($s2['lots']);
    $posSizeS2 = $lotsS2Val !== null
        ? tl4Cell($lotsS2Val . ' lots')
        : tl4Cell(null, 'No position size was recorded on this trade.');
    $block2Rows = [
        ['label' => 'Position Size (Lots)', 'cells' => [
            $posSizeS2,
            tl4Cell($s3['actual_lots'] !== null ? tl4Price($s3['actual_lots']) . ' lots' : null, $s3['actual_lots'] === null ? 'No lot size was confirmed at Draft Resolution.' : null),
            tl4Cell($s4['broker_lot_size'] !== null ? tl4Price($s4['broker_lot_size']) . ' lots' : null, $reconciled ? 'Broker-confirmed lot size.' : $mt5Note),
        ]],
        ['label' => 'Entry Price', 'cells' => [
            tl4Cell(tl4Price($s2['entry_price'])),
            tl4Cell(tl4Price($s3['actual_entry_price'])),
            tl4Cell(($reconciled && $s4['broker_entry_price'] !== null) ? tl4Price($s4['broker_entry_price']) : null, $reconciled ? ($s4['broker_entry_price'] !== null ? 'Broker-verified fill price.' : 'Reconciled, but no broker fill price was captured for this trade yet.') : $mt5Note),
        ]],
        ['label' => 'Stop Loss', 'cells' => [
            tl4Cell(tl4Price($s2['sl_price'])),
            tl4Cell(tl4Price($s3['actual_sl_price'])),
            tl4Cell(($reconciled && $s4['broker_sl_price'] !== null) ? tl4Price($s4['broker_sl_price']) : null, $reconciled ? ($s4['broker_sl_price'] !== null ? 'Broker-verified stop level.' : 'Reconciled, but no broker stop level was captured for this trade yet.') : $mt5Note),
        ]],
    ];

    // Take Profit (TP1) — real bug fixed (2026-08-03): previously showed
    // "✓ Hit"/blank instead of the actual planned price at Stage 3, which
    // made a genuinely known TP price disappear whenever it wasn't the
    // exit level. Draft Resolution always knows the same planned price
    // (this schema has no separate "actual TP price" capture) — only
    // Stage 4 remains gated on MT5, per "only MT5 should remain pending."
    $block2Rows[] = ['label' => 'Take Profit (TP1)', 'cells' => [
        tl4Cell(tl4Price($s2['tp_price']), $s2['tp_price'] === null ? 'No TP1 target was set on this trade.' : null),
        tl4TpPriceCellStage3($s2['tp_price'], 1, $s3, $winLoss),
        tl4TpPriceCellStage4($s2['tp_price'], 1, $s4, $mt5Note),
    ]];

    // TP2 / TP3-TP10 — same reasoning as TP1 above, extended per level.
    // 2026-08-19: TP1 always renders (a trade's primary target is never
    // hidden); TP2 and TP3+ now only render at all when the trader
    // actually set that level, per explicit instruction — no more empty
    // "No TP2/TP3+ target was set" rows padding out every trade that
    // only ever used a single target.
    $tpLevels = $s2['tp_levels'] ?? [];
    $tp2 = null; $tp3to10 = [];
    foreach ($tpLevels as $lvl) {
        if ($lvl['level'] === 2) $tp2 = $lvl['price'];
        elseif ($lvl['level'] >= 3) $tp3to10[] = $lvl;
    }
    if ($tp2 !== null) {
        $block2Rows[] = ['label' => 'TP2', 'cells' => [
            tl4Cell(tl4Price($tp2)),
            tl4TpPriceCellStage3($tp2, 2, $s3, $winLoss),
            tl4TpPriceCellStage4($tp2, 2, $s4, $mt5Note),
        ]];
    }

    if (!empty($tp3to10)) {
        $tp3plusHitLevel = ($s3['tp_level_hit'] !== null && $s3['tp_level_hit'] >= 3) ? $s3['tp_level_hit'] : null;
        if ($tp3plusHitLevel !== null) {
            $c3Note = "TP{$tp3plusHitLevel} was the confirmed exit level. See Reward \$ below for the realized profit.";
        } elseif ($winLoss === null) {
            $c3Note = 'This trade hasn\'t closed yet.';
        } else {
            $c3Note = 'None of these levels was the confirmed exit for this trade.';
        }
        $c3tp3 = tl4Tp3PlusCell($tp3to10, $c3Note);

        $brokerHitLevel = null;
        if ($reconciled && $s4['broker_exit_price'] !== null) {
            foreach ($tp3to10 as $lvl) {
                if (abs($s4['broker_exit_price'] - $lvl['price']) <= abs($lvl['price']) * AnalyticsEngine::CALC_PRICE_TOLERANCE_RATIO) { $brokerHitLevel = $lvl['level']; break; }
            }
        }
        if (!$reconciled) {
            $c4tp3 = tl4Cell(null, $mt5Note);
        } else {
            $c4Note = $brokerHitLevel !== null ? ('Broker-verified: TP' . $brokerHitLevel . ' was the confirmed exit level.') : 'Broker-confirmed, but the exit didn\'t match any of these levels.';
            $c4tp3 = tl4Tp3PlusCell($tp3to10, $c4Note);
        }
        $c2tp3 = tl4Tp3PlusCell($tp3to10, null);
        $block2Rows[] = ['label' => 'TP3+', 'cells' => [$c2tp3, $c3tp3, $c4tp3]];
    }
    // else: no TP3-TP10 levels were ever set on this trade — the row
    // doesn't render at all, per the same rule as TP2 above.

    // Risk:Reward (Block 2) — 2026-08-19, corrected: dropped "Actual" too,
    // not just "(Realized)", matching the same de-prefixing pass applied
    // everywhere else in this table. Stage 2 now shows the calculator's
    // own computed planned ratio (s2['risk_reward']) instead of a bare
    // dash — the calculator always computes this the moment entry/SL/TP
    // are set, so it's real, available data, not something still pending.
    // Stage 3/4 stay the genuinely different figure: the REAL realized
    // outcome once the trade actually closes, which is the whole reason
    // this row exists as its own thing alongside Block 1's declared-vs-
    // planned Risk:Reward row above.
    $s4RMultiple = null;
    if ($reconciled && $winLoss === 'win' && !empty($s2['max_loss']) && $s2['max_loss'] > 0 && $s3['effective_pnl'] !== null) {
        $s4RMultiple = round($s3['effective_pnl'] / $s2['max_loss'], 3);
    }
    $block2Rows[] = ['label' => 'Risk:Reward', 'cells' => [
        tl4Cell(tl4Rr($s2['risk_reward']), $s2['risk_reward'] === null ? 'Risk:Reward was not recorded.' : null),
        tl4Cell(tl4RMultiple($s3['r_multiple']), 'Confirmed outcome ÷ Stage 2\'s planned max loss, signed, so a loss shows as a negative R.'),
        tl4Cell(tl4RMultiple($s4RMultiple), $reconciled && $winLoss !== 'win' ? 'Broker-reconciled realized R is only tracked for winning trades.' : null),
    ]];

    // Risk $ / Reward $ — 2026-08-19: "Planned Risk $"/"Planned Reward $"
    // and the separate "Actual Reward $" row are merged into one row per
    // metric, per explicit instruction — the stage column already says
    // whether a figure is planned (Stage 2), confirmed (Stage 3), or
    // broker-verified (Stage 4), so the row label no longer needs to say
    // it too. Reward $ now shows Stage 3's REAL confirmed effective_pnl
    // (not the planned figure carried forward) — this is the actual
    // merge: what "Actual Reward $" already computed correctly, now
    // living under the unified label.
    //
    // Stage 4 Risk $ is a genuine, real, broker-derived figure (Stage 2's
    // planned max loss, scaled by the broker-verified lot ratio) — the
    // exact same scaling technique already used for Block 1's Risk % row
    // (actual_broker_risk_pct) and for Stage 3's own Executed Risk % —
    // never a copy of the planned figure pretending to be broker data.
    // Stage 4 Reward $ stays honestly empty even once reconciled: unlike
    // risk (fixed by lot size alone), realized reward depends on the
    // actual exit price, and this schema has no broker-native dollar P&L
    // field to verify it against (trades.broker_pnl is written by Draft
    // Resolution, not MT5 import — confirmed via dashboard.php's
    // resolve_trade_id handler). That's a real, permanent gap, not a
    // "hasn't happened yet" state, so it keeps one short factual note
    // rather than being silently blank or fabricated from the plan.
    $s4RiskDollar = ($reconciled && $s4['broker_lot_size'] !== null && !empty($s2['lots']) && $s2['lots'] > 0 && $s2['max_loss'] !== null)
        ? round($s2['max_loss'] * ((float)$s4['broker_lot_size'] / (float)$s2['lots']), 2)
        : null;
    $block2Rows[] = ['label' => 'Risk $', 'cells' => [
        tl4Cell(tl4Money($s2['max_loss'])),
        tl4Cell(tl4Money($s2['max_loss'])),
        tl4Cell(tl4Money($s4RiskDollar), $reconciled ? ($s4RiskDollar !== null ? 'Stage 2\'s planned risk $, scaled by the broker-verified lot ratio.' : 'No broker lot size captured yet.') : null),
    ]];

    // Second real bug fixed (2026-08-04, kept): shows the real signed
    // number every time a confirmed outcome exists (-$50.40, +$247.18),
    // not just for a win — the explanatory tone moves to the hover note.
    $c3Reward = ($winLoss !== null && $s3['effective_pnl'] !== null)
        ? tl4Cell(tl4Money($s3['effective_pnl']))
        : tl4Cell(null, $winLoss !== null ? 'No confirmed outcome was recorded for this trade.' : null);
    $block2Rows[] = ['label' => 'Reward $', 'cells' => [
        tl4Cell(tl4Money($s2['max_profit'])),
        $c3Reward,
        tl4Cell(null, $reconciled ? 'No broker-native $ P&L field exists to verify this against.' : null),
    ]];

    // Movement — real bugs fixed (2026-08-04). (a) The old display-layer
    // rnPipsOrPoints() heuristic guessed a scale from ticker length alone
    // (a bare "6 lowercase letters = forex, x10000" regex), which
    // misclassified real non-forex 6-letter tickers (XAUUSD, NATGAS,
    // COFFEE, COTTON, ...) as forex and multiplied them by 10000 — or, for
    // tickers that missed the regex, silently fell back to a 2-decimal raw
    // price diff. Replaced with AnalyticsEngine::movementValue()/
    // movementSpec(), which reuses the calculator's own real, MT5-verified
    // pipMultiplier per instrument (Forex → Pips, Metals/Indices/
    // Commodities → Points, Crypto/Stocks → raw $ price movement) — never
    // a new or guessed scale factor. (b) The old fixed 1-decimal format
    // collapsed small-but-real commodity moves (e.g. a real natgas move of
    // $0.021) down to a meaningless "+0.1" — decimal precision is now
    // asset-aware too, so the real stored price movement stays visible.
    // The row label itself now names the correct unit for this trade's
    // own instrument, per "make movement units asset-aware." (c) Caught
    // live during verification: $raw['instrument'] (computeFourStageRaw()'s
    // shared instrument variable) is a friendly display string like
    // "Bitcoin (BTC/USD)", not the raw ticker — classifyAssetClass()
    // couldn't match it against INSTRUMENT_SPECS/ASSET_CLASS_MAP, silently
    // falling back to a generic Points spec even for real crypto trades.
    // Fixed to use $trade['instrument'] (the raw ticker) directly, same
    // as the already-correct $rawTicker used for the Instrument(s) row
    // above.
    $moveSpec = AnalyticsEngine::movementSpec($trade['instrument']);
    $moveUnit = $moveSpec['unit'];
    $fmtMove = function ($v) use ($moveSpec) {
        if ($v === null) return null;
        if ($moveSpec['unit'] === '$') return ($v >= 0 ? '+$' : '-$') . number_format(abs($v), $moveSpec['decimals']);
        return ($v >= 0 ? '+' : '') . number_format($v, $moveSpec['decimals']);
    };
    $plannedMove = ($s2['entry_price'] !== null && $s2['tp_price'] !== null)
        ? AnalyticsEngine::movementValue($trade['instrument'], ($direction === 'short' ? -1 : 1) * ($s2['tp_price'] - $s2['entry_price']))
        : null;
    $moveResultS3 = null;
    if ($s3['actual_exit_price'] !== null && $s3['actual_entry_price'] !== null) {
        $diff = $s3['actual_exit_price'] - $s3['actual_entry_price'];
        if ($direction === 'short') $diff = -$diff;
        $moveResultS3 = AnalyticsEngine::movementValue($trade['instrument'], $diff);
    }
    $moveResultS4 = null;
    if ($reconciled && $s4['broker_exit_price'] !== null && $s4['broker_entry_price'] !== null) {
        $diff4 = $s4['broker_exit_price'] - $s4['broker_entry_price'];
        if ($direction === 'short') $diff4 = -$diff4;
        $moveResultS4 = AnalyticsEngine::movementValue($trade['instrument'], $diff4);
    }
    $block2Rows[] = ['label' => 'Movement (' . $moveUnit . ')', 'cells' => [
        tl4Cell($fmtMove($plannedMove), $plannedMove !== null ? 'Planned distance from entry to TP1.' : 'No planned movement was recorded. This trade didn\'t have both a stop loss and a TP1 target.'),
        tl4Cell($fmtMove($moveResultS3), 'Actual exit vs. actual entry, signed in the trade\'s favor.'),
        tl4Cell($fmtMove($moveResultS4), $reconciled ? ($moveResultS4 !== null ? 'Broker-verified exit vs. broker-verified entry.' : 'Reconciled, but no broker fill/exit prices were captured for this trade yet.') : $mt5Note),
    ]];

    // Duration, Classification, Checklist Completion, Setup Confidence,
    // and Psychology rows removed (2026-08-13). Already shown elsewhere.
    // tl4DeriveEmotionalState()/tl4DeriveExecutionFidelity()/
    // tl4PsychologyScore() left in place, unused.

    // ════════════════════════════════════════════════════════════
    // BLOCK 4. BROKER RECONCILIATION (Stage 4 only). Values come only
    // from broker-verified columns, gated on is_reconciled. Nothing is
    // copied from Stage 3. See tl4RenderMt5ReconciliationSection().
    // ════════════════════════════════════════════════════════════
    $slippage = null;
    if ($reconciled && $s4['broker_entry_price'] !== null && $s2['entry_price'] !== null) {
        $slippage = round($s4['broker_entry_price'] - $s2['entry_price'], 5);
    }
    $spreadPips = null;
    if ($reconciled && $s4['broker_spread_at_entry'] !== null) {
        $spreadPips = rnPipsOrPoints($trade['instrument'], $s4['broker_spread_at_entry']);
    }
    $block4Data = [
        'instrument' => $rawTicker,
        'direction' => $direction,
        'open_time' => $s4['broker_open_time'],
        'close_time' => $s4['broker_close_time'],
        'duration_minutes' => $s4['holding_time_minutes'],
        'lot_size' => $s4['broker_lot_size'],
        'entry' => $s4['broker_entry_price'],
        'exit' => $s4['broker_exit_price'],
        'sl' => $s4['broker_sl_price'],
        'slippage' => $slippage,
        'spread_pips' => $spreadPips,
        'commission' => $s4['broker_commission'],
        'swap' => $s4['broker_swap'],
        'mt5_position_id' => $s4['mt5_position_id'],
        // Plan side of the comparison. Stage 2's Calculator Intent, never
        // Stage 3.
        'planned_entry' => $s2['entry_price'],
        'planned_sl' => $s2['sl_price'],
        'planned_lots' => $s2['lots'],
        'planned_risk_pct' => $s2['risk_pct'],
        'broker_risk_pct' => $s4['actual_broker_risk_pct'],
        'lot_adherent' => $s4['reconciled_adherent'],
    ];

    return [
        ['stages' => [1, 2, 3, 4], 'rows' => $block1Rows, 'banner' => [
            'title' => 'Declared Edge Complete', 'color' => 'blue',
            'sub' => 'Strategy baseline has been declared. Execution comparison begins below.',
        ]],
        ['stages' => [2, 3, 4], 'rows' => $block2Rows, 'banner' => [
            'title' => 'Calculator Intent Complete', 'color' => 'purple',
            'sub' => 'The trade plan has been created. Awaiting manual resolution and MT5 verification.',
        ]],
        ['type' => 'broker_reconciliation', 'data' => $block4Data, 'banner' => $reconciled ? [
            'title' => 'MT5 Reconciliation Complete', 'color' => 'green',
            'sub' => 'Broker verification complete. This is the final source of truth.',
        ] : [
            'title' => 'Broker Match Pending', 'color' => 'green', 'pending' => true,
            'sub' => 'Once this trade is matched to your MT5 statement, you will see it compared here.',
        ]],
    ];
}

/**
 * Coaching summary. Short, forward-looking bullets, not a restatement of
 * every table row. Max 4 lines.
 */
function tl4CoachingSummary(array $raw, array $trade, ?array $checklist, array $compliance): array {
    $s1 = $raw['stage1']; $s4 = $raw['stage4'];
    $winLoss = tl4IsClosed($trade) ? ($raw['stage3']['win_loss'] ?? null) : null;

    if ($winLoss === null) {
        return ['This trade hasn\'t closed yet. Coaching appears once Draft Resolution confirms the outcome.'];
    }

    $lines = [];

    // Weakest compliance category becomes one forward-looking line.
    $categories = array_filter($compliance, fn($k) => $k !== 'overall', ARRAY_FILTER_USE_KEY);
    $weakest = null;
    foreach ($categories as $key => $c) {
        if ($c['score'] !== null && $c['score'] < 85 && ($weakest === null || $c['score'] < $weakest['score'])) {
            $weakest = $c + ['key' => $key];
        }
    }

    if ($weakest !== null) {
        // Short, direct versions of Stage 1's own consistency findings.
        // Built from the same raw fields (risk_overage, contract_
        // constrained, rr_mismatch), not a rewrite of engine.php's
        // consistency_notes text, which Reports also reuses verbatim.
        if ($weakest['key'] === 'declared_edge' && !empty($s1['contract_constrained']) && $s1['min_achievable_risk_pct'] !== null && $s1['risk_pct'] !== null) {
            $lines[] = 'This instrument\'s minimum lot size risks at least ' . number_format($s1['min_achievable_risk_pct'], 2) . '%, above the declared ' . number_format($s1['risk_pct'], 2) . '% limit.';
        } elseif ($weakest['key'] === 'declared_edge' && $s1['risk_overage'] !== null && $s1['risk_pct'] !== null) {
            $actualPct = $s1['risk_pct'] + $s1['risk_overage'];
            $lines[] = 'Risk was ' . number_format($actualPct, 2) . '%, above the declared ' . number_format($s1['risk_pct'], 2) . '% limit. Size and target from the declared risk % and R:R next time.';
        } elseif ($weakest['key'] === 'declared_edge' && $s1['rr_mismatch'] !== null && $s1['risk_reward'] !== null) {
            $actualRr = $s1['risk_reward'] + $s1['rr_mismatch'];
            $lines[] = 'The trade\'s prices imply a ' . number_format($actualRr, 2) . ':1 ratio, not the declared ' . number_format($s1['risk_reward'], 2) . ':1. Double-check entry, SL, and TP next time.';
        } elseif ($weakest['key'] === 'declared_edge' && !empty($s1['consistency_notes'])) {
            $lines[] = $s1['consistency_notes'][0];
        } else {
            $advice = match ($weakest['key']) {
                'calculator_adherence' => 'The saved entry, SL, TP, risk %, and RR didn\'t line up. Double-check the calculator\'s numbers before submitting.',
                'manual_execution' => 'Execution drifted from the plan. Confirm the fill matches the plan before treating it as executed.',
                'broker_verification' => 'Broker reconciliation found a mismatch against what was manually recorded. Review the broker statement.',
                default => 'This stage drifted from the one before it.',
            };
            $lines[] = ucfirst($weakest['label']) . ' was the weakest link (' . number_format($weakest['score'], 0) . '%). ' . $advice;
        }
    }

    // Only shown for a genuine, actionable mismatch.
    $calib = tl4DeriveConfidenceCalibration($raw, $trade, $checklist);
    if (!empty($calib['actionable']) && !empty($calib['sentence'])) {
        $lines[] = $calib['sentence'] . ' Watch for this in upcoming trades.';
    }

    if (!empty($s4['reconciled'])) {
        if ($s4['reconciled_adherent'] === false) {
            $lines[] = 'Broker-verified lot size differed from the plan by more than tolerance. Confirm position sizing against the broker statement next time.';
        }
        if ($s4['instrument_confirmed'] === false) {
            $lines[] = 'Broker reconciliation did not confirm the recorded instrument. Review this trade before trusting its other broker-verified figures.';
        }
    }

    if ($weakest === null) {
        array_unshift($lines, 'Every stage of this trade lined up with the one before it. Carry this standard into the next trade.');
    }

    if (empty($s4['reconciled'])) {
        $lines[] = 'Broker reconciliation is pending. This summary updates once an MT5 statement is matched.';
    }

    return array_slice(array_values(array_unique($lines)), 0, 4);
}

/** Fetches this trade's own checklist_results row (if any) — same trade_lifecycle_id join every other checklist read in this file already uses. */
function tl4FetchChecklist(array $trade, $db): ?array {
    if (empty($trade['trade_lifecycle_id'])) return null;
    $rows = $db->raw(
        "SELECT ticked_pct, items_ticked, items_required, status FROM checklist_results WHERE trade_lifecycle_id = :tlid AND user_id = :uid LIMIT 1",
        [':tlid' => $trade['trade_lifecycle_id'], ':uid' => $trade['user_id']]
    );
    return $rows[0] ?? null;
}

/**
 * Fetches the attached strategy's plain name (and its id, for the
 * "View Strategy Changes" panel below) for this trade, or null if none
 * attached. No "(vN)" — TRADERALIGN STRATEGY BUSINESS RULES redesign
 * (2026-08-11) §13: version numbers are internal bookkeeping, never
 * user-facing.
 */
function tl4FetchStrategyLabel(array $trade, $db): ?array {
    if (empty($trade['strategy_version_id'])) return null;
    $rows = $db->raw(
        "SELECT s.id AS strategy_id, s.name AS strategy_name FROM strategy_versions sv JOIN strategies s ON s.id = sv.strategy_id WHERE sv.id = :svid",
        [':svid' => $trade['strategy_version_id']]
    );
    if (empty($rows)) return null;
    return ['id' => $rows[0]['strategy_id'], 'name' => trim($rows[0]['strategy_name'])];
}

/** Stage number/name/CSS-class metadata shared by the block renderer below. */
function tl4StageMeta(): array {
    return [
        1 => ['num' => 'Stage 1', 'name' => 'Declared Edge', 'class' => 'tl4-stage-1'],
        2 => ['num' => 'Stage 2', 'name' => 'Calculator Intent', 'class' => 'tl4-stage-2'],
        3 => ['num' => 'Stage 3', 'name' => 'Manual Outcome', 'class' => 'tl4-stage-3'],
        4 => ['num' => 'Stage 4', 'name' => 'Broker Reconciliation', 'class' => 'tl4-stage-4'],
    ];
}

/** Renders one cascading block's <table> — column count matches $block['stages']. */
function tl4RenderBlockTable(array $block, bool $s4Reconciled): void {
    $meta = tl4StageMeta();
    ?>
    <div class="tl4-table-scroll">
    <table class="tl4-table">
        <thead>
            <tr>
                <th class="tl4-metric-col">Metric</th>
                <?php foreach ($block['stages'] as $stageNum): $m = $meta[$stageNum]; ?>
                <th class="tl4-stage-col <?= $m['class'] ?>"><span class="tl4-stage-num"><?= $m['num'] ?></span><span class="tl4-stage-name"><?= $m['name'] ?><?= ($stageNum === 4 && !$s4Reconciled) ? ' (Pending)' : '' ?></span></th>
                <?php endforeach; ?>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($block['rows'] as $row): ?>
            <tr>
                <td class="tl4-metric-col"><?= htmlspecialchars($row['label']) ?></td>
                <?php foreach ($row['cells'] as $i => $cell):
                    $stageClass = $meta[$block['stages'][$i]]['class'];
                    $isHtml = !empty($cell['html']);
                    // 2026-08-19, supersedes the 2026-08-03 correctness
                    // pass documented below: that pass deliberately showed
                    // a null cell's full explanatory sentence INLINE
                    // ("every absence must explain why, visibly, not just
                    // in a hover tooltip"). Per repeated, explicit
                    // instruction across this whole table's cleanup, that
                    // is now the exact opposite of what's wanted — a
                    // missing value shows a plain "-", and whatever real
                    // reason exists for it lives ONLY in the title=
                    // tooltip, never wrapped inline across a table cell.
                    $display = $cell['v'] !== null ? $cell['v'] : '-';
                    $cellClass = $cell['v'] !== null ? '' : ' tl4-na';
                ?>
                <td class="tl4-stage-col <?= $stageClass ?><?= $cellClass ?>"<?= $cell['note'] ? ' title="' . htmlspecialchars($cell['note']) . '"' : '' ?>><?= $isHtml ? $display : htmlspecialchars((string)$display) ?></td>
                <?php endforeach; ?>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
    </div>
    <?php
}

/**
 * Builds the single row this trade already occupies in the main Active
 * Journal trades table, in the exact shape renderRnTradeTable() expects
 * (['trade'=>.., 'pnl'=>.., 'balance'=>..]), so "Actual Outcome
 * Tracking" below is a real reuse of that one shared component, not a
 * second copy of its column logic. screenshot_count isn't present on a
 * plain $db->selectById('trades', ...) row (every OTHER caller of
 * renderRnTradeTable() already joins it in its own query), so it's
 * fetched here to keep this row byte-identical to what the trader already
 * sees in Active Journeys, not a degraded copy.
 */
function tl4BuildActualOutcomeRow(array $trade, array $raw, $db): array {
    $s3 = $raw['stage3'] ?? [];
    $pnl = $s3['effective_pnl'] ?? null;
    if ($pnl === null) {
        foreach (['challenge_pnl', 'broker_pnl', 'projected_pnl'] as $col) {
            if (isset($trade[$col]) && $trade[$col] !== null && $trade[$col] !== '') { $pnl = (float)$trade[$col]; break; }
        }
    }
    $pnl = $pnl ?? 0.0;

    // Real running balance isn't stored per-trade anywhere in this schema
    // (see the account-level Single Source of Truth Rule), the same
    // balance_at_entry + this trade's own P&L approximation Active
    // Journeys' own phase-card builders already use for a single row in
    // isolation, not a new formula invented here.
    $baseline = $trade['balance_at_entry'] ?? $trade['account_size_at_entry'] ?? null;
    $balance = $baseline !== null ? (float)$baseline + $pnl : 0.0;

    if (!empty($trade['id'])) {
        try {
            $scRow = $db->raw('SELECT COUNT(*) AS c FROM trade_screenshots WHERE trade_id = :id', [':id' => $trade['id']]);
            $trade['screenshot_count'] = (int)($scRow[0]['c'] ?? 0);
        } catch (\Throwable $e) { $trade['screenshot_count'] = 0; }
    }

    return ['trade' => $trade, 'pnl' => $pnl, 'balance' => $balance];
}

/**
 * The MT5 Trades panel -- rebuilt 2026-08-19 per direct clarification: this
 * tab is the ACCOUNT's whole imported broker log, every position from the
 * last statement upload, matched or not (40 positions in, 40 rows shown
 * here -- never just the one that happens to matter for whichever trade
 * "View Details" was clicked from), so a trader landing here from ANY
 * trade of this account sees the exact same full broker record. Two real
 * tables back this, not one: mql5_trades (a position TraderAlign actually
 * matched to a journaled trade) and unmatched_broker_trades (a real MT5
 * execution with no journal counterpart -- exactly the "trade you took
 * but never logged" case this whole feature exists to surface, so it
 * belongs here too, not hidden). Deduped by broker ticket, preferring the
 * matched (mql5_trades) copy when a ticket exists in both -- this happens
 * when a low/medium-confidence match gets manually confirmed later via
 * mt5-resolve.php, which marks the old unmatched_broker_trades row
 * resolved rather than deleting it.
 *
 * The row that corresponds to the trade currently being viewed is
 * highlighted and reuses that trade's own already-computed state label
 * (never a second classification system, per explicit instruction) --
 * every OTHER row reuses whichever OTHER real journaled trade it's linked
 * to, or is honestly flagged "Not Journaled" (or "Dismissed" if the
 * trader already reviewed and dismissed it) when nothing in this app was
 * ever logged for it. Column parity with renderRnTradeTable()'s own
 * header is deliberate: same metric, same position, so toggling tabs
 * never remaps a column. Broker-only fields (commission/swap) are
 * appended after the shared columns, never interleaved with them.
 */
function tl4RenderMt5TradesPanel(array $trade, array $data, bool $reconciled, array $stateInfo, string $plannedRr, $db): void {
    $accountId = $trade['account_id'] ?? null;
    $rows = [];
    $seenTickets = [];
    if ($accountId) {
        try {
            $matchedRaw = $db->raw(
                'SELECT mt5_position_id, event_payload, linked_trade_id
                 FROM mql5_trades
                 WHERE user_id = :uid AND account_id = :aid
                 ORDER BY captured_at DESC
                 LIMIT 1000',
                [':uid' => $db->userId(), ':aid' => $accountId]
            );
            foreach ($matchedRaw as $r) {
                $p = json_decode($r['event_payload'] ?? '', true);
                if (!is_array($p)) continue;
                $p['mt5_position_id'] = $r['mt5_position_id'];
                $p['linked_trade_id'] = $r['linked_trade_id'];
                $p['broker_resolution'] = null;
                $rows[] = $p;
                if (!empty($r['mt5_position_id'])) $seenTickets[(string)$r['mt5_position_id']] = true;
            }

            $unmatchedRaw = $db->raw(
                'SELECT raw_broker_row, resolution, resolved_trade_id
                 FROM unmatched_broker_trades
                 WHERE user_id = :uid AND account_id = :aid
                 ORDER BY created_at DESC
                 LIMIT 1000',
                [':uid' => $db->userId(), ':aid' => $accountId]
            );
            foreach ($unmatchedRaw as $r) {
                $raw = json_decode($r['raw_broker_row'] ?? '', true);
                if (!is_array($raw)) continue;
                $ticket = (string)($raw['ticket'] ?? '');
                if ($ticket !== '' && isset($seenTickets[$ticket])) continue; // already have the matched/richer copy
                $rows[] = [
                    'mt5_position_id' => $ticket !== '' ? $ticket : null,
                    'symbol' => $raw['symbol'] ?? null,
                    'type' => $raw['type'] ?? null,
                    'open_price' => $raw['price'] ?? null,
                    'close_price' => $raw['close_price'] ?? null,
                    'sl' => $raw['sl'] ?? null,
                    'tp' => $raw['tp'] ?? null,
                    'open_time' => $raw['time'] ?? null,
                    'close_time' => $raw['close_time'] ?? null,
                    'commission' => $raw['commission'] ?? null,
                    'swap' => $raw['swap'] ?? null,
                    'raw_profit' => $raw['profit'] ?? null,
                    'netted_pnl' => (float)($raw['profit'] ?? 0) + (float)($raw['commission'] ?? 0) + (float)($raw['swap'] ?? 0),
                    'linked_trade_id' => $r['resolution'] === 'linked_manually' ? ($r['resolved_trade_id'] ?? null) : null,
                    'broker_resolution' => $r['resolution'] ?? 'unresolved',
                ];
                if ($ticket !== '') $seenTickets[$ticket] = true;
            }
        } catch (\Throwable $e) { $rows = []; }
    }

    if (empty($rows)) {
        $uploadUrl = '/traderalign/mt5-upload' . ($accountId ? '?account_id=' . urlencode($accountId) : '');
        ?>
        <div class="tl4-mt5-empty">
            <span class="tl4-mt5-empty-icon">📄</span>
            <div>
                <div class="tl4-mt5-empty-title">No broker statement imported yet</div>
                <div class="tl4-mt5-empty-sub">Once you upload one, your MT5 trades for this account will appear here.</div>
                <a href="<?= htmlspecialchars($uploadUrl) ?>" class="tl4-mt5-upload-link">📤 Upload broker data</a>
            </div>
        </div>
        <?php
        return;
    }

    usort($rows, function ($a, $b) {
        $ta = strtotime($a['open_time'] ?? '') ?: 0;
        $tb = strtotime($b['open_time'] ?? '') ?: 0;
        return $tb <=> $ta;
    });

    // State labels for rows linked to OTHER journaled trades on this
    // account -- one batch fetch, not one query per row.
    $otherIds = [];
    foreach ($rows as $p) {
        $lid = $p['linked_trade_id'] ?? null;
        if ($lid && $lid !== ($trade['id'] ?? null)) $otherIds[] = $lid;
    }
    $otherStates = [];
    if (!empty($otherIds)) {
        $otherIds = array_values(array_unique($otherIds));
        $placeholders = [];
        $params = [':uid' => $db->userId()];
        foreach ($otherIds as $i => $id) { $ph = ':id' . $i; $placeholders[] = $ph; $params[$ph] = $id; }
        try {
            $linkedRows = $db->raw('SELECT id, trade_classification FROM trades WHERE user_id = :uid AND id IN (' . implode(',', $placeholders) . ')', $params);
            foreach ($linkedRows as $lr) { $otherStates[$lr['id']] = $lr['trade_classification']; }
        } catch (\Throwable $e) { /* fall through to Matched/neutral below */ }
    }
    ?>
    <p class="tl4-mt5-label">All Imported MT5 Trades, This Account (<?= count($rows) ?>)</p>
    <div class="tl4-mt5-table-scroll">
    <table class="tl4-mt5-table tl4-mt5-parity-table">
        <thead>
            <tr>
                <th>Date</th><th>Instrument</th><th>Bias</th><th>Entry</th><th>SL</th><th>TP</th>
                <th>Pips / Points</th><th>P&amp;L (USD)</th><th>Balance</th><th>R:R</th><th>Duration</th><th>State</th>
                <th>Commission</th><th>Swap</th>
            </tr>
        </thead>
        <tbody>
        <?php foreach ($rows as $p):
            $isThisTrade = !empty($p['linked_trade_id']) && $p['linked_trade_id'] === ($trade['id'] ?? null);
            $type = $p['type'] ?? '';
            $direction = $type === 'long' ? 'Long' : ($type === 'short' ? 'Short' : '-');
            $entry = $p['open_price'] ?? null;
            $sl = $p['sl'] ?? null;
            $tp = $p['tp'] ?? null;
            $exit = $p['close_price'] ?? null;
            $pips = null;
            if ($exit !== null && $entry !== null && $type !== '') {
                $rawDiff = (float)$exit - (float)$entry;
                if ($type === 'short') $rawDiff = -$rawDiff;
                $pips = rnPipsOrPoints($p['symbol'] ?? '', $rawDiff);
            }
            $netted = $p['netted_pnl'] ?? ((float)($p['raw_profit'] ?? 0) + (float)($p['commission'] ?? 0) + (float)($p['swap'] ?? 0));
            $durMin = (!empty($p['open_time']) && !empty($p['close_time'])) ? (strtotime($p['close_time']) - strtotime($p['open_time'])) / 60 : null;

            if ($isThisTrade) {
                $rowClass = $stateInfo['class']; $rowLabel = $stateInfo['label']; $rowTip = $stateInfo['tooltip'];
            } elseif (!empty($p['linked_trade_id']) && array_key_exists($p['linked_trade_id'], $otherStates)) {
                [$rowLabel, $rowClass] = rnStateInfo($otherStates[$p['linked_trade_id']]);
                $rowTip = 'This execution is matched to a different journaled trade on this account.';
            } elseif (in_array($p['broker_resolution'] ?? null, ['dismissed_ghost', 'dismissed_other_account'], true)) {
                $rowLabel = 'Dismissed'; $rowClass = 'rn-state-neutral';
                $rowTip = 'Reviewed and dismissed as not belonging to this account\'s journal.';
            } else {
                $rowLabel = 'Not Journaled'; $rowClass = 'rn-state-warn';
                $rowTip = 'The broker executed this trade, but it was never journaled in TraderAlign.';
            }
        ?>
            <tr class="<?= $isThisTrade ? 'tl4-mt5-row-current' : '' ?>">
                <td><?= !empty($p['close_time']) ? date('d M Y', strtotime($p['close_time'])) : (!empty($p['open_time']) ? date('d M Y', strtotime($p['open_time'])) : '-') ?></td>
                <td><?= htmlspecialchars(strtoupper($p['symbol'] ?? '-')) ?></td>
                <td class="<?= $direction === 'Long' ? 'positive' : ($direction === 'Short' ? 'negative' : '') ?>"><?= $direction ?></td>
                <td><?= $entry !== null ? number_format((float)$entry, 4) : '-' ?></td>
                <td><?= $sl !== null ? number_format((float)$sl, 4) : '-' ?></td>
                <td><?= $tp !== null ? number_format((float)$tp, 4) : '-' ?></td>
                <td class="<?= $pips === null ? '' : ($pips >= 0 ? 'positive' : 'negative') ?>"><?= $pips === null ? '-' : (($pips >= 0 ? '+' : '') . $pips) ?></td>
                <td class="<?= pnlClass($netted) ?>"><?= pnlFormat($netted) ?></td>
                <td><?= $isThisTrade ? ('$' . number_format(($trade['balance_at_entry'] ?? $trade['account_size_at_entry'] ?? 0) + $netted, 2)) : '-' ?></td>
                <td><?= $isThisTrade ? htmlspecialchars($plannedRr) : '-' ?></td>
                <td><?= $durMin !== null ? rnFormatDuration($durMin) : '-' ?></td>
                <td><span class="rn-state <?= $rowClass ?>" title="<?= htmlspecialchars($rowTip) ?>"><?= htmlspecialchars($rowLabel) ?></span><?php if ($isThisTrade): ?> <span class="rn-badge rn-badge-blue" title="This is the broker execution matched to the trade you're viewing.">THIS TRADE</span><?php endif; ?></td>
                <td><?= isset($p['commission']) && $p['commission'] !== null ? tl4Money((float)$p['commission']) : '-' ?></td>
                <td><?= isset($p['swap']) && $p['swap'] !== null ? tl4Money((float)$p['swap']) : '-' ?></td>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>
    </div>
    <?php
}

/**
 * Trade-specific comparison between what was journaled (Stage 3, Manual
 * Outcome) and what the broker actually confirms (Stage 4), not the
 * existing Planned-vs-Broker table above, which compares Calculator
 * Intent (Stage 2) instead. This is the real replacement for the deleted
 * Coaching Insight / Stage Compliance Overview: findings about THIS
 * trade, never a generic score.
 */
function tl4RenderComparisonVerdict(array $trade, array $raw, array $data, bool $reconciled): void {
    $s3 = $raw['stage3'] ?? [];
    ?>
    <div class="tl4-verdict">
        <p class="tl4-mt5-label">Journal vs. Broker, This Trade</p>
        <?php if (!$reconciled): ?>
        <div class="tl4-verdict-empty">
            <span class="tl4-verdict-empty-icon">🔍</span>
            <div>
                <div class="tl4-verdict-empty-title">Not matched to a broker record yet</div>
                <div class="tl4-verdict-empty-sub">Once this trade is matched, you will see how your journal entry compares to what MT5 actually executed.</div>
            </div>
        </div>
        <?php else:
            $findings = [];
            $entryDiff = ($s3['actual_entry_price'] ?? null) !== null && $data['entry'] !== null
                ? round((float)$data['entry'] - (float)$s3['actual_entry_price'], 5) : null;
            $findings[] = ['label' => 'Entry Price', 'match' => $entryDiff !== null && abs($entryDiff) < 0.0000005,
                'detail' => $entryDiff === null ? 'Missing data on one side.' : (abs($entryDiff) < 0.0000005 ? 'Matched exactly.' : 'Off by ' . tl4Price(abs($entryDiff)) . ' (broker vs. journaled).')];

            $slDiff = ($s3['actual_sl_price'] ?? null) !== null && $data['sl'] !== null
                ? round((float)$data['sl'] - (float)$s3['actual_sl_price'], 5) : null;
            $findings[] = ['label' => 'Stop Loss', 'match' => $slDiff !== null && abs($slDiff) < 0.0000005,
                'detail' => $slDiff === null ? 'Missing data on one side.' : (abs($slDiff) < 0.0000005 ? 'Matched exactly.' : 'Off by ' . tl4Price(abs($slDiff)) . ' (broker vs. journaled).')];

            $journaledPnl = $s3['effective_pnl'] ?? null;
            $brokerPnl = $trade['broker_pnl'] ?? null;
            $pnlDiff = ($journaledPnl !== null && $brokerPnl !== null) ? round((float)$brokerPnl - (float)$journaledPnl, 2) : null;
            $findings[] = ['label' => 'P&L', 'match' => $pnlDiff !== null && abs($pnlDiff) < 0.01,
                'detail' => $pnlDiff === null ? 'Missing data on one side.' : (abs($pnlDiff) < 0.01 ? 'Matched exactly.' : (($pnlDiff >= 0 ? '+' : '') . tl4Money($pnlDiff) . ' variance (broker vs. journaled).'))];

            $journaledDurMin = (!empty($trade['opened_at']) && !empty($trade['closed_at'])) ? (strtotime($trade['closed_at']) - strtotime($trade['opened_at'])) / 60 : null;
            $durDiffMin = ($journaledDurMin !== null && $data['duration_minutes'] !== null) ? round($data['duration_minutes'] - $journaledDurMin) : null;
            $findings[] = ['label' => 'Duration', 'match' => $durDiffMin !== null && abs($durDiffMin) < 5,
                'detail' => $durDiffMin === null ? 'Missing data on one side.' : (abs($durDiffMin) < 5 ? 'Matched closely.' : (($durDiffMin >= 0 ? '+' : '') . round($durDiffMin) . ' min variance (broker vs. journaled).'))];

            $matchCount = count(array_filter($findings, fn($f) => $f['match']));
        ?>
        <div class="tl4-verdict-grid">
            <ul class="tl4-verdict-list">
                <?php foreach ($findings as $f): ?>
                <li class="<?= $f['match'] ? 'tl4-verdict-ok' : 'tl4-verdict-off' ?>">
                    <span class="tl4-verdict-icon"><?= $f['match'] ? '✓' : '⚠' ?></span>
                    <span class="tl4-verdict-label"><?= htmlspecialchars($f['label']) ?></span>
                    <span class="tl4-verdict-detail"><?= htmlspecialchars($f['detail']) ?></span>
                </li>
                <?php endforeach; ?>
            </ul>
            <?php if ($journaledPnl !== null && $brokerPnl !== null):
                $maxAbs = max(abs($journaledPnl), abs($brokerPnl), 0.01);
                $jPct = max(2, round((abs($journaledPnl) / $maxAbs) * 100));
                $bPct = max(2, round((abs($brokerPnl) / $maxAbs) * 100));
            ?>
            <div class="tl4-verdict-chart">
                <div class="tl4-verdict-bar-row">
                    <span class="tl4-verdict-bar-label">Journaled P&amp;L</span>
                    <div class="tl4-verdict-bar-track"><div class="tl4-verdict-bar-fill <?= $journaledPnl >= 0 ? 'tl4-bar-pos' : 'tl4-bar-neg' ?>" style="width:<?= $jPct ?>%"></div></div>
                    <span class="tl4-verdict-bar-value <?= pnlClass($journaledPnl) ?>"><?= pnlFormat($journaledPnl) ?></span>
                </div>
                <div class="tl4-verdict-bar-row">
                    <span class="tl4-verdict-bar-label">Broker P&amp;L</span>
                    <div class="tl4-verdict-bar-track"><div class="tl4-verdict-bar-fill <?= $brokerPnl >= 0 ? 'tl4-bar-pos' : 'tl4-bar-neg' ?>" style="width:<?= $bPct ?>%"></div></div>
                    <span class="tl4-verdict-bar-value <?= pnlClass($brokerPnl) ?>"><?= pnlFormat($brokerPnl) ?></span>
                </div>
            </div>
            <?php endif; ?>
        </div>
        <div class="tl4-verdict-summary"><?= $matchCount ?> of <?= count($findings) ?> checks matched exactly between what was journaled and what the broker confirms.</div>
        <?php endif; ?>
    </div>
    <?php
}

/**
 * Stage 4 content -- rebuilt 2026-08-19 per direct instruction: two real,
 * data-backed tabs (this trade's own already-journaled row, and the
 * broker's own record once imported) plus a trade-specific verdict,
 * replacing the old single MT5-only table and the generic Coaching
 * Insight / Stage Compliance Overview footer entirely. Values still come
 * only from trades.broker_* and is_reconciled -- nothing here is
 * estimated or carried forward before real broker data exists.
 */
function tl4RenderMt5ReconciliationSection(array $data, bool $reconciled, array $trade, array $raw, $db, array $banner): void {
    $s2 = $raw['stage2'] ?? [];
    $data['planned_tp'] = $s2['tp_price'] ?? ($trade['tp_price'] ?? null);
    $plannedRr = ($trade['risk_reward_tp1'] ?? null) !== null && $trade['risk_reward_tp1'] !== '' ? '1 : ' . number_format((float)$trade['risk_reward_tp1'], 2) : '-';

    $actualRow = tl4BuildActualOutcomeRow($trade, $raw, $db);
    $tActual = $actualRow['trade'];
    $stateInfo = [];
    if (!empty($tActual['trade_classification'])) {
        [$stateLabel, $stateClass] = rnStateInfo($tActual['trade_classification']);
        $stateInfo = ['label' => $stateLabel, 'class' => $stateClass, 'tooltip' => rnClassificationTooltip($tActual['trade_classification'], $actualRow['pnl'], $tActual)];
    } else {
        $stateInfo = ['label' => 'Unclassified', 'class' => 'rn-state-neutral', 'tooltip' => 'This trade has not been classified yet.'];
    }

    $panelId = 'tl4-s4-' . preg_replace('/[^a-z0-9]/i', '', (string)($trade['id'] ?? uniqid()));
    ?>
    <div class="tl4-mt5-module">
        <div class="tl4-mt5-section-head">
            <span class="tl4-stage-num tl4-stage-4">Stage 4</span>
            <span class="tl4-stage-name tl4-stage-4">Broker Reconciliation</span>
        </div>

        <div class="tl4-mt5-tabs" role="tablist">
            <button type="button" class="tl4-mt5-tab active" data-target="<?= $panelId ?>-outcome" onclick="tl4SwitchS4Tab(this, '<?= $panelId ?>')">📓 Actual Outcome Tracking</button>
            <button type="button" class="tl4-mt5-tab" data-target="<?= $panelId ?>-mt5" onclick="tl4SwitchS4Tab(this, '<?= $panelId ?>')">📡 MT5 Trades</button>
        </div>

        <div id="<?= $panelId ?>-outcome" class="tl4-mt5-panel">
            <p class="tl4-mt5-label">Journaled Trade (from Active Journeys)</p>
            <?php // hideDetailsColumn=true: this row already IS the "View
            // Details" page for this exact trade. A link back to itself
            // is backwards reasoning, not a real navigation. View Trade,
            // Screenshot, and Notes stay, since those remain genuinely
            // useful here.
            renderRnTradeTable([$actualRow], $panelId . '-outcome-table', false, null, null, null, false, null, true); ?>

            <?php
            // Every piece of single-trade comparison content
            // (verdict, the legacy Calculator-vs-Broker table, and the
            // Stage 4 completion/pending banner) lives inside THIS
            // panel only, per explicit instruction: MT5 Trades is the
            // account's whole broker log and must never show
            // comparison language or "this trade" wording; Actual
            // Outcome Tracking is the only place a per-trade verdict
            // belongs, and it must change per trade, never leak
            // between them.
            tl4RenderComparisonVerdict($trade, $raw, $data, $reconciled);
            ?>

            <?php if ($reconciled):
            $entryC = tl4Cell(tl4Price($data['entry']), $data['entry'] === null ? 'No broker fill price captured yet.' : 'Broker-verified fill price.');
            $slC = tl4Cell(tl4Price($data['sl']), $data['sl'] === null ? 'No broker stop level captured yet.' : 'Broker-verified stop level.');
            $lotC = tl4Cell($data['lot_size'] !== null ? tl4Price($data['lot_size']) . ' lots' : null, $data['lot_size'] === null ? 'No broker lot size captured yet.' : 'Broker-verified lot size.');
        ?>
        <?php
            $rows = [];

            // Entry Price. Long fills better lower, short fills better
            // higher. Reuses $data['slippage'], not recomputed.
            $entryDevClass = ''; $entryDevVal = null; $entryDevNote = null;
            if ($data['entry'] !== null && $data['planned_entry'] !== null) {
                $diff = $data['slippage'] ?? round($data['entry'] - $data['planned_entry'], 5);
                if (abs($diff) < 0.0000005) {
                    $entryDevVal = 'Matched exactly';
                } else {
                    $favorable = $data['direction'] === 'long' ? ($diff < 0) : ($diff > 0);
                    $entryDevClass = $favorable ? 'positive' : 'negative';
                    $entryDevVal = ($diff >= 0 ? '+' : '') . tl4Price($diff) . ($favorable ? ' (favorable fill)' : ' (adverse fill)');
                }
            } else {
                $entryDevNote = $data['planned_entry'] === null ? 'No planned entry recorded.' : 'No broker fill price captured yet.';
            }
            $rows[] = [
                'label' => 'Entry Price',
                'planned' => tl4Cell(tl4Price($data['planned_entry']), $data['planned_entry'] === null ? 'No planned entry recorded.' : null),
                'broker' => $entryC,
                'dev' => tl4Cell($entryDevVal, $entryDevNote), 'devClass' => $entryDevClass,
            ];

            // Stop Loss. Plain difference, no favorable/unfavorable tag.
            $slDevVal = null; $slDevNote = null;
            if ($data['sl'] !== null && $data['planned_sl'] !== null) {
                $slDiff = round($data['sl'] - $data['planned_sl'], 5);
                $slDevVal = abs($slDiff) < 0.0000005 ? 'Matched exactly' : (($slDiff >= 0 ? '+' : '') . tl4Price($slDiff));
            } else {
                $slDevNote = $data['planned_sl'] === null ? 'No planned stop loss recorded.' : 'No broker stop level captured yet.';
            }
            $rows[] = [
                'label' => 'Stop Loss',
                'planned' => tl4Cell(tl4Price($data['planned_sl']), $data['planned_sl'] === null ? 'No planned stop loss recorded.' : null),
                'broker' => $slC,
                'dev' => tl4Cell($slDevVal, $slDevNote), 'devClass' => '',
            ];

            // Lot Size. Reuses the same tolerance check already computed
            // for Stage 4 (reconciled_adherent).
            $lotDevVal = null; $lotDevClass = ''; $lotDevNote = null;
            if ($data['lot_size'] !== null && $data['planned_lots'] !== null) {
                $lotDiff = round($data['lot_size'] - $data['planned_lots'], 4);
                if ($data['lot_adherent'] === true) { $lotDevVal = 'Within tolerance'; $lotDevClass = 'positive'; }
                elseif ($data['lot_adherent'] === false) { $lotDevVal = ($lotDiff >= 0 ? '+' : '') . tl4Price($lotDiff) . ' lots (outside tolerance)'; $lotDevClass = 'negative'; }
                else { $lotDevVal = ($lotDiff >= 0 ? '+' : '') . tl4Price($lotDiff) . ' lots'; }
            } else {
                $lotDevNote = $data['planned_lots'] === null ? 'No planned lot size recorded.' : 'No broker lot size captured yet.';
            }
            $rows[] = [
                'label' => 'Lot Size',
                'planned' => tl4Cell($data['planned_lots'] !== null ? tl4Price($data['planned_lots']) . ' lots' : null, $data['planned_lots'] === null ? 'No planned lot size recorded.' : null),
                'broker' => $lotC,
                'dev' => tl4Cell($lotDevVal, $lotDevNote), 'devClass' => $lotDevClass,
            ];

            // Risk %. Planned risk vs. the broker-lot-scaled actual risk
            // already computed for Stage 4 (actual_broker_risk_pct).
            $riskDevVal = null; $riskDevNote = null;
            if ($data['broker_risk_pct'] !== null && $data['planned_risk_pct'] !== null) {
                $riskDiff = round($data['broker_risk_pct'] - $data['planned_risk_pct'], 2);
                $riskDevVal = abs($riskDiff) < 0.005 ? 'Matched exactly' : (($riskDiff >= 0 ? '+' : '') . number_format($riskDiff, 2) . ' pp');
            } else {
                $riskDevNote = $data['planned_risk_pct'] === null ? 'No planned risk % recorded.' : 'Needs a broker-verified lot size.';
            }
            $rows[] = [
                'label' => 'Risk %',
                'planned' => tl4Cell(tl4Pct($data['planned_risk_pct']), $data['planned_risk_pct'] === null ? 'No planned risk % recorded.' : null),
                'broker' => tl4Cell(tl4Pct($data['broker_risk_pct']), $data['broker_risk_pct'] === null ? 'Needs a broker-verified lot size.' : 'Broker-verified risk, scaled by broker lot size.'),
                'dev' => tl4Cell($riskDevVal, $riskDevNote), 'devClass' => '',
            ];
        ?>
        <p class="tl4-mt5-label">Calculator Plan vs. Broker-Executed</p>
        <div class="tl4-mt5-table-scroll">
        <table class="tl4-mt5-compare-table">
            <thead>
                <tr><th>Metric</th><th>Planned (Calculator Intent)</th><th>Broker-Executed</th><th>Deviation</th></tr>
            </thead>
            <tbody>
                <?php foreach ($rows as $r): ?>
                <tr>
                    <td class="tl4-mt5-compare-label"><?= htmlspecialchars($r['label']) ?></td>
                    <?= tl4TdFromCell($r['planned']) ?>
                    <?= tl4TdFromCell($r['broker']) ?>
                    <?= tl4TdFromCell($r['dev'], $r['devClass']) ?>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
        </div>
        <?php endif; ?>

            <?php tl4RenderBanner($banner); ?>
        </div>

        <div id="<?= $panelId ?>-mt5" class="tl4-mt5-panel" style="display:none">
            <?php tl4RenderMt5TradesPanel($trade, $data, $reconciled, $stateInfo, $plannedRr, $db); ?>
        </div>
    </div>
    <?php
}

/**
 * Renders the milestone divider between cascading blocks (2026-08-19
 * redesign): a centered label on a quiet rule instead of the old
 * full-width flat-color banner, so a section boundary reads as a
 * confident marker rather than another alert box.
 */
function tl4RenderBanner(array $banner): void {
    $milestoneClass = !empty($banner['pending']) ? 'tl4-milestone-pending' : 'tl4-milestone-' . $banner['color'];
    ?>
    <div class="tl4-milestone">
        <span class="tl4-milestone-line"></span>
        <span class="tl4-milestone-chip <?= $milestoneClass ?>">
            <span class="tl4-milestone-icon"><?= !empty($banner['pending']) ? '⏳' : '✓' ?></span>
            <?= htmlspecialchars(strtoupper($banner['title'])) ?>
        </span>
        <span class="tl4-milestone-line"></span>
    </div>
    <p class="tl4-milestone-sub"><?= htmlspecialchars($banner['sub']) ?></p>
    <?php
}

/**
 * "This trade was taken under X" narration (TRADERALIGN STRATEGY BUSINESS
 * RULES.docx §20) — built from the strategy's real recorded change events,
 * scoped to only those that happened before this trade was captured. Never
 * a hardcoded sentence; genuinely empty (no prior changes) is its own
 * honest branch, not padded with invented text.
 */
function tl4StrategyChangeNarration(array $allEvents, ?string $tradeCapturedAt): array {
    $prior = [];
    foreach ($allEvents as $e) {
        if ($tradeCapturedAt === null || $e['changed_at'] <= $tradeCapturedAt) $prior[] = $e;
    }
    $count = count($prior);
    if ($count === 0) {
        return ['count' => 0, 'text' => 'This strategy had not been changed yet when this trade was taken.'];
    }
    $lastPrior = $prior[$count - 1];
    $times = $count === 1 ? 'once' : "{$count} times";
    $text = "This strategy had been changed {$times} before this trade.";
    if (!empty($lastPrior['fields'])) {
        $f = $lastPrior['fields'][0];
        $label = StrategyChangeHistory::fieldLabel($f['field_name']);
        if (count($lastPrior['fields']) > 1) {
            $text .= ' The most recent change before this trade updated ' . count($lastPrior['fields']) . " fields, including {$label}.";
        } else {
            $text .= " The most recent change before this trade was {$label}, from " . StrategyChangeHistory::displayValue($f['previous_value']) . ' to ' . StrategyChangeHistory::displayValue($f['new_value']) . '.';
        }
    }
    $text .= " This trade was taken under the strategy's declaration as of that point.";
    return ['count' => $count, 'text' => $text];
}

/**
 * Renders the "4-Stage Trade Lifecycle" section — the single-trade
 * counterpart to Reports' period-level 4-Stage Summary. See the block
 * comment above tl4Cell() for the architecture note, and above
 * tl4BuildLifecycleBlocks() for the cascading-table structure (2026-08-03
 * redesign, replacing the old single flat table).
 */
function renderTradeFourStageLifecycle(array $trade, $db): void {
    // Maximum Loss Model comment doesn't apply — this is the real,
    // reported "View Details" bug (2026-08-18 correction):
    // AnalyticsEngine::getFourStageRaw() used to unconditionally treat
    // every is_validation=1 trade as "not found," regardless of which
    // workflow was viewing it — correct for a main-workflow caller, wrong
    // here, since $trade has ALREADY passed the caller's own workflow-
    // scope check ($jtTradeInScope, above in this file) by the time this
    // function ever runs. Passing $allowValidation = !empty($trade
    // ['is_validation']) only ever unlocks this specific, already-verified
    // trade's own real data — never a blanket bypass.
    $raw = AnalyticsEngine::getFourStageRaw($trade['id'], !empty($trade['is_validation']));
    if (isset($raw['error'])) {
        echo '<div class="rn-empty-note">This trade\'s 4-Stage data could not be loaded.</div>';
        return;
    }
    $checklist = tl4FetchChecklist($trade, $db);
    $strategyLabel = tl4FetchStrategyLabel($trade, $db);
    $strategyChangeNarration = null;
    if ($strategyLabel !== null) {
        $scEvents = StrategyChangeHistory::fetchEvents($db, $strategyLabel['id']);
        $strategyChangeNarration = tl4StrategyChangeNarration($scEvents, $trade['captured_at'] ?? null);
    }
    $blocks = tl4BuildLifecycleBlocks($raw, $trade, $checklist);
    $compliance = tl4ComplianceScores($raw);
    $coaching = tl4CoachingSummary($raw, $trade, $checklist, $compliance);
    $s4Reconciled = !empty($raw['stage4']['reconciled']);
    ?>
    <div class="tl4-section">
        <div class="tl4-head">
            <div>
                <div class="tl4-title">4-Stage Trade Lifecycle</div>
                <div class="tl4-sub">How this trade moved from declared edge to broker reconciliation. Each row compares one metric across all four stages.</div>
            </div>
            <div>
                <div class="tl4-strategy-chip"><?= $strategyLabel !== null ? htmlspecialchars($strategyLabel['name']) : 'No strategy attached' ?></div>
                <?php if ($strategyChangeNarration !== null && $strategyChangeNarration['count'] > 0): ?>
                <details class="tl4-strategy-history">
                    <summary>View Strategy Changes (<?= (int)$strategyChangeNarration['count'] ?>)</summary>
                    <p class="tl4-strategy-history-text"><?= htmlspecialchars($strategyChangeNarration['text']) ?></p>
                </details>
                <?php endif; ?>
            </div>
        </div>

        <?php foreach ($blocks as $i => $block):
            if (($block['type'] ?? 'metric') === 'broker_reconciliation') {
                // Stage 4's own completion/pending banner renders INSIDE
                // tl4RenderMt5ReconciliationSection() now, scoped to the
                // Actual Outcome Tracking tab only -- it must never show
                // under the MT5 Trades tab (that tab is the account's
                // whole broker log, with no per-trade language at all).
                tl4RenderMt5ReconciliationSection($block['data'], $s4Reconciled, $trade, $raw, $db, $block['banner']);
            } else {
                tl4RenderBlockTable($block, $s4Reconciled);
                tl4RenderBanner($block['banner']);
            }
        endforeach; ?>
    </div>
    <?php
}

/** Journal Trades — single-trade view. Header + the 4-Stage Trade Lifecycle comparison (see block above). */
function renderJournalTradesSingleView(array $trade, $db): void {
    $accountLabel = '-';
    if (!empty($trade['account_id'])) {
        $acct = $db->selectById('trading_accounts', $trade['account_id']);
        if ($acct) $accountLabel = $acct['label'];
    }
    ?>
    <a href="<?= activeJourneysUrl() ?>" class="back-link">← Back to Active Journeys</a>
    <div class="section-label">
        <span><?= htmlspecialchars($trade['instrument'] ?? 'Unknown') ?> <span class="<?= ($trade['direction'] ?? '') === 'long' ? 'positive' : 'negative' ?>"><?= strtoupper($trade['direction'] ?? '') ?></span></span>
        <span><?= htmlspecialchars($accountLabel) ?></span>
    </div>
    <?php renderTradeFourStageLifecycle($trade, $db); ?>
    <?php
}


/**
 * TRADING ARCHIVE — moved out to public/trading-archive.php (2026-08-06)
 * to keep this file from growing further; renderTradingArchive() and
 * everything it calls (the taRender-prefixed and taSuccess-prefixed
 * functions) now live there. Nothing about the URL or behavior changed —
 * this is a require, not a redirect, so ?workspace=archive still renders
 * exactly as it always did. See that file's own header comment for the
 * full rationale.
 */
require_once __DIR__ . '/trading-archive.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Journal - 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">
<!-- Trading Center redesign: the account picker (search/first-5/Show More,
     badges, glow indicator) is now a shared component — see
     src/AccountPicker.php's doc comment. Loaded here so this page's own
     .rn-account-picker* / .rn-badge* classes still resolve; the inline
     <style>/<script> blocks below still declare byte-identical copies of
     these same rules (left in place, not deleted, to keep this the
     smallest possible edit to this file) — harmless duplication, not a
     behavioral difference. -->
<link rel="stylesheet" href="/traderalign/assets/css/account-picker.css">
<script src="/traderalign/assets/js/account-picker.js"></script>
<link rel="stylesheet" href="/traderalign/assets/css/account-summary.css?v=<?= @filemtime(__DIR__ . '/../assets/css/account-summary.css') ?: time() ?>">
<script src="/traderalign/assets/js/account-summary.js"></script>
<link rel="stylesheet" href="/traderalign/assets/css/trade-screenshots.css">
<script src="/traderalign/assets/js/trade-screenshots.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--font-sans);background:var(--background);color:var(--foreground);-webkit-font-smoothing:antialiased}
.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:calc(100vw - 4rem)}
.page-title{font-size:1.5rem;font-weight:800;color:var(--foreground);letter-spacing:-.02em}
.page-sub{font-size:.78rem;color:var(--foreground-dim);margin-top:.15rem;margin-bottom:1.1rem}
.zone-tabs{display:flex;gap:.4rem;margin-bottom:.75rem;flex-wrap:wrap}
.zone-tab{padding:.55rem 1.1rem;border-radius:var(--radius-md);font-size:.8rem;font-weight:700;text-decoration:none;color:var(--foreground-dim);background:var(--background-elevated);border:1px solid var(--border)}
.zone-tab.active{background:var(--primary);color:#fff;border-color:var(--primary)}
.category-tabs{display:flex;gap:.4rem;margin-bottom:1.5rem;flex-wrap:wrap;padding-bottom:1rem;border-bottom:1px solid var(--border)}
.category-tab{padding:.4rem .85rem;border-radius:var(--radius-full);font-size:.72rem;font-weight:600;text-decoration:none;color:var(--foreground-dim);background:var(--background-surface);border:1px solid var(--border)}
.category-tab.active{background:color-mix(in srgb,var(--primary) 15%,transparent);color:var(--primary);border-color:color-mix(in srgb,var(--primary) 40%,transparent)}
.category-tab .count{opacity:.7;margin-left:.3rem}
.section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:var(--foreground-dim);margin-bottom:.85rem;padding-bottom:.5rem;border-bottom:2px solid var(--border);display:flex;justify-content:space-between;align-items:center}
.section-label a{font-size:.68rem;font-weight:600;color:var(--primary);text-decoration:none;text-transform:none;letter-spacing:0}
.trade-row{display:flex;align-items:center;justify-content:space-between;background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-lg);padding:.85rem 1.1rem;margin-bottom:.55rem;text-decoration:none;color:inherit}
.acct-row-left{display:flex;flex-direction:column;gap:.2rem}
.row-name{font-weight:700;font-size:.85rem}
.row-meta{font-size:.68rem;color:var(--foreground-dim)}
.row-amount{font-family:var(--font-mono);font-weight:600;font-size:.8rem}
.status-pill{font-size:.6rem;font-weight:700;text-transform:uppercase;padding:.15rem .5rem;border-radius:var(--radius-full);background:var(--background-surface);color:var(--foreground-dim);border:1px solid var(--border)}
.status-pill.open,.status-pill.draft{color:var(--primary);border-color:color-mix(in srgb,var(--primary) 40%,transparent)}
.empty-state{text-align:center;padding:4rem 1rem;color:var(--foreground-dim)}
.back-link{font-size:.82rem;color:var(--primary);text-decoration:none;margin-bottom:1rem;display:inline-block}
.positive{color:var(--positive)} .negative{color:var(--negative)}
.filter-bar{display:flex;flex-wrap:wrap;gap:.6rem;margin-bottom:1.25rem;padding:.9rem 1rem;background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-lg)}
.filter-bar select,.filter-bar input{background:var(--background-surface);border:1px solid var(--border);color:var(--foreground);padding:.4rem .6rem;border-radius:var(--radius-md);font-size:.78rem}
.filter-bar .filter-date-label{font-size:.72rem;color:var(--foreground-dim);align-self:center;cursor:default}
/* 2026-08-12 — .btn-sm was never defined anywhere this page can see
   (assets/tokens.css only has .btn-lg/.btn-md; this file had no local
   .btn-sm of its own), so the Filter/Clear buttons above silently fell
   back to .btn's own loose default sizing (font-size:.95rem, no height
   constraint) instead of a compact button — reading as oversized next to
   the same filter bar on Dashboard, which already defines this rule
   locally. Copied verbatim from dashboard.php's own .btn-sm so the two
   pages' filter bars render as the same component. Confirmed scoped
   safely — .btn-sm is used in exactly these two buttons (Filter/Clear)
   anywhere in this file. */
.btn-sm{height:1.55rem;padding:0 .6rem;font-size:.7rem;border-radius:var(--radius-sm);border:none;cursor:pointer;font-weight:600}
@media(max-width:768px){.main{padding:1rem}}

/* ═══════════════════════════════════════════════════════════
   RIGHT NOW — approved prototype implementation (2026-07-29)
   ═══════════════════════════════════════════════════════════ */
:root{
    --rn-green: #22C55E;
    --rn-green-dim: rgba(34,197,94,.14);
    --rn-gold: #E0A64A;
    --rn-gold-dim: rgba(224,166,74,.14);
    --rn-purple: #8B7CF6;
    --rn-purple-dim: rgba(139,124,246,.10);
    --rn-blue: #60A5FA;
    --rn-red: #EF5350;
}
@keyframes rn-pulse{0%,100%{opacity:1}50%{opacity:.35}}

.rn-header{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem}
.rn-title{font-size:1.6rem;font-weight:800;color:var(--foreground);letter-spacing:-.02em}
.rn-subtitle{font-size:.8rem;color:var(--foreground-dim);margin-top:.2rem}
.rn-header-right{display:flex;align-items:center;gap:1rem;flex-wrap:wrap}
/* ── Journal redesign (2026-07-31): merged Trade Journal + collapsible Active Coaching ── */
.rn-trade-journal{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);padding:1.4rem;margin-bottom:1.5rem}
.rn-collapsible{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);margin:1.5rem 0;overflow:hidden}
.rn-collapsible-summary{cursor:pointer;list-style:none;padding:1.1rem 1.4rem;font-size:1.05rem;font-weight:800;color:var(--foreground);display:flex;align-items:center;gap:.5rem}
.rn-collapsible-summary::-webkit-details-marker{display:none}
.rn-collapsible-summary::before{content:"▸";color:var(--rn-purple);transition:transform .15s ease}
.rn-collapsible[open] .rn-collapsible-summary::before{transform:rotate(90deg)}
.rn-collapsible-summary:hover{color:var(--rn-purple)}
.rn-collapsible-body{padding:0 1.4rem 1.4rem}
/* ── Item 6: clickable coaching→trade-table filters ──────────────── */
.rn-filter-banner{display:flex;align-items:center;justify-content:space-between;gap:1rem;background:color-mix(in srgb,var(--rn-purple) 14%,transparent);border:1px solid var(--rn-purple);border-radius:var(--radius-md);padding:.7rem 1rem;margin-bottom:1.2rem;font-size:.82rem;font-weight:700;color:var(--foreground)}
.rn-filter-chip{display:inline-flex;align-items:center;background:none;border:none;padding:0;margin-left:.35rem;color:var(--rn-purple);font-size:inherit;font-weight:700;text-decoration:underline;text-underline-offset:2px;cursor:pointer;font-family:inherit}
.rn-filter-chip:hover{color:var(--rn-gold)}
.rn-account-selector{display:flex;align-items:center;gap:.65rem;background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-md);padding:.5rem .9rem}
.rn-account-selector label{font-size:.62rem;font-weight:700;letter-spacing:.08em;color:var(--foreground-dim);text-transform:uppercase}
.rn-account-selector select{background:transparent;border:none;color:var(--foreground);font-size:.85rem;font-weight:600;min-width:16rem;cursor:pointer}
.rn-account-selector select:focus{outline:none}
.rn-btn-gold{display:inline-flex;align-items:center;gap:.4rem;border:1px solid var(--rn-gold);color:var(--rn-gold);background:transparent;padding:.55rem 1rem;border-radius:var(--radius-md);font-size:.8rem;font-weight:700;text-decoration:none;transition:background var(--t-fast)}
.rn-btn-gold:hover{background:var(--rn-gold-dim)}
.rn-btn-outline{display:inline-flex;align-items:center;gap:.35rem;border:1px solid var(--rn-purple);color:var(--rn-purple);background:transparent;padding:.5rem .9rem;border-radius:var(--radius-md);font-size:.76rem;font-weight:700;text-decoration:none;white-space:nowrap;transition:background var(--t-fast)}
.rn-btn-outline:hover{background:var(--rn-purple-dim)}

.rn-phase-card{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);margin-bottom:1.5rem;overflow:hidden}
.rn-phase-card.rn-card-passed{border-color:rgba(34,197,94,.35)}
.rn-phase-card.rn-card-active{border-color:rgba(96,165,250,.35)}
.rn-phase-card.rn-card-breached{border-color:rgba(239,83,80,.4)}
.rn-phase-card.rn-card-funded{border-color:rgba(224,166,74,.45);background:linear-gradient(180deg,rgba(224,166,74,.05),var(--background-elevated) 12rem)}

.rn-phase-head{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1.5rem;padding:1.35rem 1.6rem}
.rn-phase-head-left{flex:1;min-width:16rem}
.rn-phase-title-row{display:flex;align-items:center;gap:.65rem;flex-wrap:wrap}
.rn-phase-title{font-size:1.05rem;font-weight:800;color:var(--foreground)}
.rn-phase-stats-line{font-size:.74rem;color:var(--foreground-dim);margin-top:.5rem;line-height:1.6}
/* Item 1 (2026-08-06) — rnProgramConfigCards()'s stat-chip grid, replacing
   a single run-on text line inside .rn-phase-stats-line with a proper,
   scannable card layout. Chips wrap to their own row rather than
   shrinking below a readable width on a narrow viewport. */
.rn-config-grid{display:flex;flex-wrap:wrap;gap:.55rem;margin-top:.65rem}
.rn-config-chip{background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:.5rem .8rem;flex:1 1 8rem;min-width:7.5rem}
.rn-config-chip-label{font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.055em;color:var(--foreground-dim);white-space:nowrap}
.rn-config-chip-value{font-size:.92rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground);margin-top:.28rem}
.rn-config-chip-sub{font-size:.64rem;color:var(--foreground-dim);margin-top:.15rem}
.rn-config-chip-positive{border-color:color-mix(in srgb,var(--rn-green) 30%,var(--border))}
.rn-config-chip-positive .rn-config-chip-value{color:var(--rn-green)}
.rn-config-chip-negative{border-color:color-mix(in srgb,var(--rn-red) 30%,var(--border))}
.rn-config-chip-negative .rn-config-chip-value{color:var(--rn-red)}
@media(max-width:640px){.rn-config-chip{flex:1 1 45%}}

.rn-badge{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;padding:.22rem .6rem;border-radius:var(--radius-full)}
.rn-badge-green{background:var(--rn-green-dim);color:var(--rn-green)}
.rn-badge-blue{background:color-mix(in srgb,var(--rn-blue) 16%,transparent);color:var(--rn-blue)}
.rn-badge-red{background:color-mix(in srgb,var(--rn-red) 16%,transparent);color:var(--rn-red)}
.rn-badge-gold{background:var(--rn-gold-dim);color:var(--rn-gold)}
.rn-badge-neutral{background:var(--background-surface);color:var(--foreground-dim);border:1px solid var(--border)}

.rn-phase-head-right{display:flex;align-items:center;gap:1.85rem;flex-wrap:wrap}
.rn-metric-ring{display:flex;flex-direction:column;align-items:center;gap:.4rem}
.rn-ring{--pct:0;width:4.2rem;height:4.2rem;border-radius:50%;position:relative;display:flex;align-items:center;justify-content:center;background:conic-gradient(var(--rn-green) calc(var(--pct)*1%), var(--border) 0)}
.rn-ring::before{content:'';position:absolute;inset:.4rem;border-radius:50%;background:var(--background-elevated)}
.rn-ring span{position:relative;font-size:.72rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground)}
.rn-metric{text-align:center;min-width:4rem}
.rn-metric-value{font-size:1rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground);white-space:nowrap}
.rn-metric-label{font-size:.6rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em;margin-top:.3rem}
.rn-score-tag-inline{font-size:.6rem;font-weight:800;text-transform:uppercase;margin-top:.25rem}
.rn-score-good{color:var(--rn-green)} .rn-score-warn{color:var(--rn-gold)} .rn-score-bad{color:var(--rn-red)} .rn-score-neutral{color:var(--foreground-dim)}
.rn-goal-metric .rn-metric-value{font-size:.82rem;white-space:normal;line-height:1.35}
.rn-goal-metric .rn-metric-value.rn-metric-waiting{font-style:italic}
/* Item 13: "Reach $X" / "Y% Complete" two-line header tile format */
.rn-goal-metric-pct{font-size:.68rem;font-weight:700;color:var(--foreground-dim);font-family:var(--font-sans)}

.rn-table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;padding:0 1.6rem 1.4rem}
.rn-table{width:100%;min-width:1640px;border-collapse:collapse}
.rn-table th{white-space:nowrap;text-align:left;font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--foreground-dim);padding:.6rem .8rem;border-bottom:2px solid var(--border)}
.rn-table td{white-space:nowrap;padding:.65rem .8rem;font-size:.8rem;color:var(--foreground);border-bottom:1px solid rgba(255,255,255,.04);font-family:var(--font-mono)}
.rn-table tbody tr:hover{background:rgba(255,255,255,.025)}
.rn-tp-badge{display:inline-block;margin-left:.3rem;font-size:.6rem;font-weight:800;color:var(--rn-green);background:var(--rn-green-dim);padding:.05rem .35rem;border-radius:var(--radius-full);cursor:default;font-family:var(--font-sans)}
.rn-state{font-size:.66rem;font-weight:700;padding:.18rem .55rem;border-radius:var(--radius-full);white-space:nowrap;font-family:var(--font-sans)}
.rn-state-good{background:var(--rn-green-dim);color:var(--rn-green)}
.rn-state-neutral{background:var(--background-surface);color:var(--foreground-dim);border:1px solid var(--border)}
.rn-state-warn{background:var(--rn-gold-dim);color:var(--rn-gold)}
.rn-state-bad{background:color-mix(in srgb,var(--rn-red) 16%,transparent);color:var(--rn-red)}
.rn-view-btn{display:inline-block;font-family:var(--font-sans);font-size:.68rem;font-weight:600;color:var(--foreground-dim);border:1px solid var(--border);border-radius:var(--radius-md);padding:.25rem .65rem;text-decoration:none;transition:color var(--t-fast),border-color var(--t-fast)}
.rn-view-btn:hover{color:var(--foreground);border-color:var(--foreground-dim)}
/* View Trade column (renamed from "Screenshot," 2026-08-06) — the real
   feature, an interactive TradingView-based chart replay of the trade's
   actual setup/execution, is designed but not yet built (see
   docs/architecture/TRADERALIGN_MT5_SYNC_ENGINE_AND_TRADE_REVIEW_
   ARCHITECTURE.md, which names it exactly "View Trade" and describes it
   as the eventual replacement for the old "Upload Screenshot" entry
   point). It previously linked into the old single-trade 4-Stage view
   instead, which was never the right destination. Non-interactive until
   the real feature ships — same base look as .rn-view-btn, just visibly
   inert (dimmer, no hover response, no pointer cursor). */
.rn-view-btn-disabled{cursor:default;opacity:.55}
.rn-view-btn-disabled:hover{color:var(--foreground-dim);border-color:var(--border)}
.rn-notes{font-family:var(--font-sans);font-size:.72rem;color:var(--foreground-dim);font-style:italic;max-width:9rem;overflow:hidden;text-overflow:ellipsis}
.rn-details-link{font-family:var(--font-sans);font-size:.72rem;font-weight:700;color:var(--rn-gold);text-decoration:none}
.rn-details-link:hover{text-decoration:underline}
.rn-empty-note{margin:0 1.6rem 1.4rem;padding:1rem;text-align:center;color:var(--foreground-dim);font-size:.78rem;background:var(--background-surface);border-radius:var(--radius-md)}

.rn-passed-banner,.rn-breached-banner{margin:0 1.6rem 1.5rem;border-radius:var(--radius-lg);padding:1rem 1.3rem;display:flex;gap:.9rem;align-items:flex-start}
.rn-passed-banner{background:var(--rn-green-dim);border:1px solid rgba(34,197,94,.35)}
.rn-breached-banner{background:color-mix(in srgb,var(--rn-red) 10%,transparent);border:1px solid rgba(239,83,80,.4)}
.rn-trophy{font-size:1.3rem;line-height:1}
.rn-passed-title{font-weight:800;color:var(--rn-green);font-size:.88rem}
.rn-passed-detail{font-size:.78rem;color:var(--foreground-muted);margin-top:.3rem;line-height:1.6}
.rn-breached-title{font-weight:800;color:var(--rn-red);font-size:.88rem}
.rn-breached-detail{font-size:.78rem;color:var(--foreground-muted);margin-top:.3rem;line-height:1.6}

.rn-funded-strip{display:flex;flex-wrap:wrap;gap:1.75rem;padding:1.1rem 1.6rem;border-top:1px solid var(--border);background:var(--rn-gold-dim);font-size:.78rem}
.rn-funded-strip-label{color:var(--foreground-dim);font-weight:700;margin-right:.35rem}

.rn-coaching{background:var(--rn-purple-dim);border:1px solid color-mix(in srgb,var(--rn-purple) 35%,transparent);border-radius:var(--radius-xl);padding:1.6rem;margin-top:1rem}
.rn-coaching-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.4rem}
.rn-coaching-title{font-size:1.05rem;font-weight:800;color:var(--rn-purple)}
.rn-coaching-sub{font-size:.78rem;color:var(--foreground-dim);font-weight:600;margin-left:.35rem}
.rn-live-badge{display:inline-flex;align-items:center;gap:.4rem;background:var(--rn-purple);color:#fff;font-size:.62rem;font-weight:800;letter-spacing:.05em;padding:.28rem .7rem;border-radius:var(--radius-full)}
.rn-live-dot{width:.4rem;height:.4rem;border-radius:50%;background:#fff;animation:rn-pulse 1.4s infinite}
.rn-coaching-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:1.25rem;margin-bottom:1.4rem}
.rn-coaching-stat{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem 1.1rem}
.rn-coaching-stat-label{font-size:.62rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.5rem}
.rn-coaching-stat-value{font-size:1.15rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground)}
.rn-coaching-stat-delta{font-size:.7rem;font-weight:700;margin-top:.25rem}
.rn-coaching-row{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:1.25rem;margin-bottom:1.25rem}
.rn-coaching-card{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.15rem 1.3rem}
.rn-rec-card{border-left:3px solid var(--rn-purple)}
.rn-coaching-card-title{font-size:.85rem;font-weight:700;color:var(--rn-purple);margin-bottom:.6rem}
.rn-coaching-card-text{font-size:.8rem;color:var(--foreground-muted);line-height:1.6}
.rn-coaching-card-label{font-size:.62rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.55rem}
.rn-coaching-card-big{font-size:1.5rem;font-weight:800;font-family:var(--font-mono);color:var(--rn-gold)}
.rn-recovery-card .rn-coaching-card-big{color:var(--foreground)}
.rn-coaching-card-sub{font-size:.7rem;color:var(--foreground-dim);margin-top:.5rem;line-height:1.5}
.rn-coaching-footer{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;padding-top:1.1rem;border-top:1px solid color-mix(in srgb,var(--rn-purple) 25%,transparent)}
.rn-coaching-footer span{font-size:.76rem;color:var(--foreground-muted);line-height:1.6}

/* Risk Panel (2026-08-17) — 3 independent cards (Overall Loss Risk /
   Loss Streak / Daily Loss Guard), each colored ONLY from its own
   status_color — never blended with a sibling card's state. */
.rp-panel{display:grid;grid-template-columns:repeat(3,1fr);gap:1.25rem;margin-bottom:1.25rem}
.rp-card{background:var(--background-elevated);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:var(--radius-lg);padding:1.15rem 1.3rem;display:flex;flex-direction:column;gap:.5rem}
.rp-card-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem}
.rp-card-title{font-size:.68rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em}
.rp-pill{display:inline-block;font-size:.6rem;font-weight:700;letter-spacing:.03em;padding:.2rem .5rem;border-radius:var(--radius-full);white-space:nowrap}
/* 2026-08-18 correction: was 1.55rem/800 — visually dominated the whole
   section (e.g. "$200.00 left today" reading heavier than anything else
   on the page). Brought down to match .rn-coaching-stat-value's own
   established 1.15rem/800 scale, so this reads as part of the same
   Active Coaching component family, not a separate oversized headline. */
.rp-big{font-size:1.15rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground);line-height:1.2}
.rp-big-unit{font-size:.66rem;font-weight:600;color:var(--foreground-dim);font-family:var(--font-sans);margin-left:.25rem}
.rp-sub{font-size:.74rem;color:var(--foreground-muted);line-height:1.5}
.rp-next{font-size:.74rem;font-weight:600;color:var(--foreground);line-height:1.5}
.rp-probation{font-size:.72rem;font-weight:700;color:var(--rn-gold);background:var(--rn-gold-dim);border-radius:var(--radius-md);padding:.45rem .6rem;line-height:1.4}
.rp-secondary{display:flex;flex-direction:column;gap:.3rem;margin-top:.15rem;padding-top:.6rem;border-top:1px solid var(--border)}
.rp-secondary-item{display:flex;justify-content:space-between;gap:.6rem;font-size:.7rem}
.rp-secondary-label{color:var(--foreground-dim)}
.rp-secondary-value{color:var(--foreground-muted);font-weight:700;font-family:var(--font-mono)}
.rp-review-btn{margin-top:.2rem;background:transparent;border:1px solid var(--rn-red);color:var(--rn-red);font-size:.74rem;font-weight:700;padding:.5rem .8rem;border-radius:var(--radius-md);cursor:pointer;transition:background var(--t-fast)}
.rp-review-btn:hover{background:color-mix(in srgb,var(--rn-red) 12%,transparent)}
.rp-color-green{border-left-color:var(--rn-green)} .rp-pill-green{background:var(--rn-green-dim);color:var(--rn-green)}
.rp-color-yellow{border-left-color:var(--rn-gold)} .rp-pill-yellow{background:var(--rn-gold-dim);color:var(--rn-gold)}
.rp-color-orange{border-left-color:#E08A3A} .rp-pill-orange{background:color-mix(in srgb,#E08A3A 16%,transparent);color:#E08A3A}
.rp-color-red{border-left-color:var(--rn-red)} .rp-pill-red{background:color-mix(in srgb,var(--rn-red) 16%,transparent);color:var(--rn-red)}
.rp-color-gray{border-left-color:var(--border)} .rp-pill-gray{background:var(--background-surface);color:var(--foreground-dim)}

@media(max-width:900px){
    .rn-coaching-stats{grid-template-columns:repeat(2,1fr)}
    .rn-coaching-row{grid-template-columns:1fr}
    .rp-panel{grid-template-columns:1fr}
}

.rn-table-more-wrap{padding:0 1.6rem 1.4rem;text-align:center}
.rn-show-more-btn{background:transparent;border:1px solid var(--border);color:var(--foreground-dim);font-size:.76rem;font-weight:700;padding:.5rem 1.1rem;border-radius:var(--radius-md);cursor:pointer;transition:color var(--t-fast),border-color var(--t-fast)}
.rn-show-more-btn:hover{color:var(--foreground);border-color:var(--foreground-dim)}

/* ── Item 10: simplified navigation ─────────────────────────── */
.rn-back-link{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;font-weight:700;color:var(--primary);text-decoration:none;margin-bottom:1.25rem}
.rn-back-link:hover{text-decoration:underline}

/* ── Item 2: Prop Firms / Live Accounts switch, + MT5 upload entry point ── */
.rn-toolbar-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.6rem;margin-bottom:1.1rem}
.rn-type-switch{display:flex;gap:.4rem}
.rn-type-tab{padding:.5rem 1.1rem;border-radius:var(--radius-full);font-size:.78rem;font-weight:700;text-decoration:none;color:var(--foreground-dim);background:var(--background-elevated);border:1px solid var(--border)}
.rn-type-tab.active{background:color-mix(in srgb,var(--rn-blue) 16%,transparent);color:var(--rn-blue);border-color:color-mix(in srgb,var(--rn-blue) 45%,transparent)}
.rn-mt5-link{display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1.1rem;border-radius:var(--radius-full);font-size:.78rem;font-weight:700;text-decoration:none;color:var(--rn-green);background:color-mix(in srgb,var(--rn-green) 12%,transparent);border:1px solid color-mix(in srgb,var(--rn-green) 32%,transparent);transition:filter var(--t-fast)}
.rn-mt5-link:hover{filter:brightness(1.12)}
.rn-mt5-link-icon{font-size:.85rem}

/* ── Item 3: account selector redesign ──────────────────────── */
/* display:inline-block (2026-08-06 fix, kept in sync with the identical
   fix in assets/css/account-picker.css — this file carries its own
   byte-identical copy of these rules, see that file's own comment for
   the full root cause): without it, this wrapper stretches to its
   parent row's full width while the trigger button only occupies its
   own natural width at the left, so the dropdown panel's right:0
   anchored to the wrapper's far edge instead of the button's — opening
   far away from where the button visually is. */
.rn-account-picker{position:relative;display:inline-block}
.rn-account-picker-btn{display:flex;align-items:center;gap:.65rem;background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-md);padding:.55rem .9rem;font-size:.85rem;font-weight:600;color:var(--foreground);cursor:pointer;min-width:14rem;justify-content:space-between}
.rn-account-picker-btn:hover{border-color:var(--foreground-dim)}
.rn-account-picker-current{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:16rem}
.rn-account-picker-chev{color:var(--foreground-dim);font-size:.7rem}
/* position:fixed, computed by rnToggleAccountPicker() at open time —
   kept in sync with the identical fix in assets/css/account-picker.css
   (see that file's own comment for the full root cause). This inline
   copy was the ONE place that fix was missed the first time: since this
   whole <style> block loads after the external stylesheet <link>, this
   stale position:absolute;right:0 rule was winning the cascade, silently
   reinterpreting the JS-computed left/top (meant to be viewport-relative
   for a fixed element) as relative to .rn-account-picker's own position
   instead — producing an on-screen offset equal to the button's own
   left position added on top of the intended value. */
.rn-account-picker-panel{position:fixed;z-index:210;width:22rem;max-width:90vw;background:var(--popover);border:1px solid var(--border);border-radius:var(--radius-lg);padding:.75rem;box-shadow:0 12px 32px rgba(0,0,0,.35)}
.rn-account-search{width:100%;background:var(--background-surface);border:1px solid var(--border);color:var(--foreground);padding:.5rem .7rem;border-radius:var(--radius-md);font-size:.8rem;margin-bottom:.6rem}
.rn-account-list{max-height:18rem;overflow-y:auto;display:flex;flex-direction:column;gap:.3rem}
.rn-account-item{display:flex;flex-direction:column;gap:.2rem;padding:.55rem .7rem;border-radius:var(--radius-md);text-decoration:none;color:var(--foreground);background:var(--background-surface);border:1px solid transparent}
.rn-account-item:hover{border-color:var(--border)}
.rn-account-item-selected{border-color:var(--primary);background:color-mix(in srgb,var(--primary) 10%,transparent)}
.rn-account-item-label{font-size:.82rem;font-weight:700}
.rn-account-item-meta{font-size:.66rem;color:var(--foreground-dim);display:flex;align-items:center;gap:.3rem}
.rn-account-item-meta .rn-badge{font-size:.56rem;padding:.1rem .4rem}
.rn-show-more-accts{width:100%;margin-top:.6rem;background:transparent;border:1px solid var(--border);color:var(--foreground-dim);font-size:.74rem;font-weight:700;padding:.45rem;border-radius:var(--radius-md);cursor:pointer}
.rn-show-more-accts:hover{color:var(--foreground);border-color:var(--foreground-dim)}

/* ── Item 7: honest "waiting for data" states (never a bare dash) ── */
.rn-metric-waiting{font-size:.72rem !important;font-weight:600 !important;color:var(--foreground-dim) !important;font-style:italic;white-space:normal !important;line-height:1.3}
.rn-pending-note{font-size:.72rem;color:var(--foreground-dim);font-style:italic;white-space:normal}

/* ── Item 2: Live Account card accent ───────────────────────── */
.rn-phase-card.rn-card-live{border-color:rgba(96,165,250,.35)}

/* ── Item 12: TP2-10 click-to-expand ────────────────────────── */
.rn-tp-cell{display:inline-flex;align-items:center;gap:.3rem;flex-wrap:wrap}
.rn-tp-badge{cursor:pointer}
.rn-tp-expand{display:inline-flex;gap:.4rem;flex-wrap:nowrap}
.rn-tp-extra{font-size:.68rem;font-weight:600;color:var(--foreground-dim);background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.1rem .4rem;white-space:nowrap}

/* ── Item 11: editable trade notes ──────────────────────────── */
.rn-notes-cell{font-family:var(--font-sans);max-width:13rem}
.rn-notes-display{display:inline-flex;align-items:flex-start;gap:.35rem;max-width:100%}
.rn-notes-text-inner{max-width:10rem}
.rn-note-add-btn,.rn-note-edit-btn{background:transparent;border:1px dashed var(--border);color:var(--foreground-dim);font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:var(--radius-sm);cursor:pointer;font-family:var(--font-sans);flex-shrink:0}
.rn-note-edit-btn{border-style:solid;padding:.15rem .4rem}
.rn-note-add-btn:hover,.rn-note-edit-btn:hover{color:var(--foreground);border-color:var(--foreground-dim)}
.rn-notes-edit{display:inline-flex;flex-direction:column;gap:.35rem;width:13rem}
.rn-notes-input{width:100%;min-height:3.2rem;background:var(--background-surface);border:1px solid var(--border);color:var(--foreground);font-size:.72rem;font-family:var(--font-sans);border-radius:var(--radius-sm);padding:.35rem;resize:vertical}
.rn-notes-hint{font-size:.62rem;color:var(--foreground-dim);font-style:italic}
.rn-note-save-btn,.rn-note-cancel-btn{font-size:.66rem;font-weight:700;padding:.2rem .5rem;border-radius:var(--radius-sm);cursor:pointer;font-family:var(--font-sans);border:1px solid var(--border)}
.rn-note-save-btn{background:var(--primary);color:#fff;border-color:var(--primary)}
.rn-note-cancel-btn{background:transparent;color:var(--foreground-dim)}

/* Item 8 (2026-08-04): Trader Notes real Show More/Show Less truncation —
   shared markup/CSS for both the Trades table's notes cell
   (.rn-notes-text-inner wraps this) and the single-trade 4-Stage
   Lifecycle's Trader Notes row (tl4-note-* context). Real truncation of
   the actual text (never CSS ellipsis alone), so the full note is always
   reachable inline without growing the page layout until asked for. */
.rn-note-short,.rn-note-full{font-size:.72rem;color:var(--foreground-dim);font-style:italic;white-space:normal;word-break:break-word}
.tl4-table .rn-note-short,.tl4-table .rn-note-full{font-size:.8rem;color:var(--foreground);font-style:normal}
.rn-note-more,.rn-note-less{margin-left:.3rem}

/* ── Items 5 & 6: expandable workspaces (Full Coaching Analysis / Account Summary) ── */
.rn-workspace{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);padding:1.6rem;margin-top:1.25rem}
.rn-workspace-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.1rem;padding-bottom:.9rem;border-bottom:1px solid var(--border)}
.rn-workspace-head h3{font-size:1.05rem;font-weight:800;color:var(--foreground)}
.rn-workspace-close{background:transparent;border:1px solid var(--border);color:var(--foreground-dim);font-size:.74rem;font-weight:700;padding:.35rem .8rem;border-radius:var(--radius-md);cursor:pointer}
.rn-workspace-close:hover{color:var(--foreground);border-color:var(--foreground-dim)}
.rn-workspace-confidence{font-size:.8rem;color:var(--foreground-dim);font-style:italic;margin-bottom:1.2rem}
.rn-workspace-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1.2rem}
.rn-workspace-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1.2rem}
.rn-workspace-card{background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.1rem}
.rn-workspace-card-head{display:flex;justify-content:space-between;align-items:center;font-size:.82rem;font-weight:700;color:var(--foreground);margin-bottom:.5rem}
.rn-workspace-score{font-family:var(--font-mono);color:var(--rn-purple)}
.rn-workspace-trend{font-size:.68rem;font-weight:700;margin-bottom:.55rem}
.rn-trend-improving{color:var(--rn-green)} .rn-trend-declining{color:var(--rn-red)} .rn-trend-stable{color:var(--foreground-dim)} .rn-trend-insufficient_data{color:var(--foreground-dim)}
.rn-workspace-card p{font-size:.78rem;color:var(--foreground-muted);line-height:1.6;margin:0}
.rn-workspace-list,.rn-workspace-checklist{list-style:none;display:flex;flex-direction:column;gap:.5rem}
.rn-workspace-list li{font-size:.78rem;color:var(--foreground-muted);line-height:1.5;padding-left:1rem;position:relative}
.rn-workspace-list li::before{content:'-';position:absolute;left:0;color:var(--rn-red)}
.rn-workspace-checklist li{font-size:.8rem;color:var(--foreground);line-height:1.5;padding:.5rem .7rem;background:var(--background-elevated);border-radius:var(--radius-md);border:1px solid var(--border)}
.rn-checklist-cat{font-size:.6rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--rn-purple);margin-right:.5rem}
.rn-req-tag{font-size:.58rem;font-weight:800;color:var(--rn-gold);background:var(--rn-gold-dim);padding:.05rem .4rem;border-radius:var(--radius-full);margin-left:.3rem}

/* ── Item 1 (2026-07-29 mission-alignment pass): Account Summary polish —
   presentation only, no field added/removed/reworded. The prior 2-column
   grid packed two independent label/value flex rows side by side, which is
   exactly what made labels and values visually collide at narrower widths.
   Replaced with a single aligned column of label|value rows per section —
   reads like a settings page, not a database dump. ── */
.rn-summary-section{margin-bottom:1.85rem}
.rn-summary-section:last-child{margin-bottom:0}
.rn-summary-section-title{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:var(--rn-gold);margin-bottom:.8rem;padding-bottom:.55rem;border-bottom:1px solid color-mix(in srgb,var(--rn-gold) 30%,transparent)}
.rn-summary-grid{display:flex;flex-direction:column}
.rn-summary-row{display:grid;grid-template-columns:minmax(9rem,1fr) minmax(0,1.3fr);align-items:baseline;column-gap:1.5rem;padding:.8rem .1rem;border-bottom:1px solid var(--border)}
.rn-summary-row:last-child{border-bottom:none}
.rn-summary-label{font-size:.76rem;font-weight:600;color:var(--foreground-dim);letter-spacing:.01em}
.rn-summary-value{font-size:.92rem;font-weight:700;color:var(--foreground);text-align:right;font-family:var(--font-mono);line-height:1.4;word-break:break-word}
/* ── Items 9/10: editable Configuration group ─────────────────────── */
.rn-summary-section-title-row{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:.8rem;padding-bottom:.55rem;border-bottom:1px solid color-mix(in srgb,var(--rn-gold) 30%,transparent)}
.rn-summary-section-title-row .rn-summary-section-title{margin-bottom:0;padding-bottom:0;border-bottom:none}
.rn-meta-edit-btn{background:none;border:none;color:var(--rn-gold);font-size:.9rem;cursor:pointer;padding:0 0 0 .5rem;line-height:1;vertical-align:middle}
.rn-meta-edit-btn:hover{color:var(--rn-purple)}
.rn-tag-pill{display:inline-block;background:var(--rn-purple-dim);color:var(--rn-purple);border-radius:var(--radius-full);padding:.15rem .65rem;font-size:.72rem;font-weight:700;margin:0 .25rem .25rem 0;font-family:var(--font-sans)}

@media(max-width:900px){
    .rn-workspace-grid{grid-template-columns:repeat(2,1fr)}
    .rn-workspace-row{grid-template-columns:1fr}
}
@media(max-width:560px){
    .rn-summary-row{grid-template-columns:1fr;row-gap:.25rem}
    .rn-summary-value{text-align:left}
}

/* ── Items 5 & 6: modal overlays (Full Coaching Analysis / Account Summary) ──
   Converts the previously-inline-at-bottom-of-page workspaces into real
   fixed-position modals — opening one never scrolls the trader away from
   where they were. Content/markup inside .rn-workspace is unchanged. ── */
.rn-modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200;display:none;align-items:flex-start;justify-content:center;padding:4vh 1rem;overflow-y:auto}
.rn-modal-panel{width:100%;max-width:58rem;max-height:92vh;overflow-y:auto;box-shadow:0 24px 64px rgba(0,0,0,.5);margin-top:0}
.rn-modal-panel-sm{max-width:28rem}

/* Account Summary modal — scoped to this one modal's id only, never the
   shared .rn-workspace/.rn-modal-panel classes, so the Full Coaching
   Analysis workspace and the Set Goal modal (both also built on those
   classes) are completely unaffected. Content inside is the shared
   src/AccountSummaryView.php component, styled by
   assets/css/account-summary.css — this only tightens the surrounding
   modal chrome (width, outer padding, header) to match. */
#rn-account-summary.rn-modal-panel{max-width:36rem}
#rn-account-summary{padding:1.05rem 1.3rem}
#rn-account-summary .rn-workspace-head{margin-bottom:.7rem;padding-bottom:.55rem}
#rn-account-summary .rn-workspace-head h3{font-size:.92rem}
#rn-account-summary .rn-workspace-close{padding:.28rem .6rem;font-size:.68rem}

/* ── Item 1: Current Goal ────────────────────────────────────── */
.rn-goal-section{background:var(--background-elevated);border:1px solid color-mix(in srgb,var(--rn-gold) 30%,transparent);border-radius:var(--radius-xl);padding:1.5rem 1.6rem;margin-top:1.25rem}
.rn-goal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.1rem}
.rn-goal-title{font-size:1.02rem;font-weight:800;color:var(--rn-gold)}
.rn-goal-empty{display:flex;justify-content:space-between;align-items:center;gap:1.5rem;flex-wrap:wrap;background:var(--background-surface);border-radius:var(--radius-lg);padding:1.2rem 1.4rem}
.rn-goal-empty-title{font-size:.88rem;font-weight:700;color:var(--foreground)}
.rn-goal-empty-sub{font-size:.78rem;color:var(--foreground-dim);margin-top:.25rem}
.rn-goal-progress-row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;flex-wrap:wrap}
.rn-goal-reach-label{font-size:.62rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em}
.rn-goal-reach{font-size:1.15rem;font-weight:800;color:var(--foreground)}
.rn-goal-reason{font-size:.76rem;color:var(--foreground-dim);font-style:italic;margin-top:.25rem}
.rn-goal-pct{font-size:.95rem;font-weight:800;color:var(--rn-gold);font-family:var(--font-mono)}
.rn-goal-pct.positive{color:var(--rn-green)}
.rn-goal-bar{height:.6rem;border-radius:var(--radius-full);background:var(--background-surface);margin:.7rem 0 .5rem;overflow:hidden}
.rn-goal-bar-fill{height:100%;background:linear-gradient(90deg,var(--rn-gold),var(--rn-green));border-radius:var(--radius-full);transition:width .3s ease}
/* Item 12: the dollar amount remaining sits right next to the bar, not only in the stats grid below */
.rn-goal-remaining-note{font-size:.85rem;font-weight:700;color:var(--rn-gold);margin-bottom:1.1rem;font-family:var(--font-mono)}
.rn-goal-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1.2rem}
.rn-goal-stat{background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:.8rem;text-align:center}
.rn-goal-stat-value{font-size:.95rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground)}
.rn-goal-stat-label{font-size:.6rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em;margin-top:.3rem}
.rn-goal-achieved-banner{background:var(--rn-green-dim);border:1px solid rgba(34,197,94,.35);border-radius:var(--radius-md);padding:.7rem 1rem;font-size:.82rem;font-weight:700;color:var(--rn-green);margin-bottom:1.1rem}
.rn-goal-coaching{display:flex;flex-direction:column;gap:.85rem}
.rn-goal-coach-row{background:var(--background-surface);border-radius:var(--radius-md);padding:.75rem .95rem;border-left:3px solid var(--border)}
.rn-goal-coach-label{display:block;font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;margin-bottom:.35rem}
.rn-goal-coach-good{color:var(--rn-green)} .rn-goal-coach-warn{color:var(--rn-gold)} .rn-goal-coach-bad{color:var(--rn-red)}
.rn-goal-coach-row p{font-size:.78rem;color:var(--foreground-muted);line-height:1.6;margin:0}

/* ── Item 8: Live Account Capital Movements ─────────────────── */
.rn-capital-panel{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);padding:1.4rem 1.6rem;margin-top:1rem}
.rn-capital-actions{display:flex;gap:.6rem}
.rn-capital-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin:.9rem 0}
.rn-capital-stat{background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:.85rem;text-align:center}
.rn-capital-stat-value{font-size:1rem;font-weight:800;font-family:var(--font-mono)}
.rn-capital-stat-label{font-size:.6rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em;margin-top:.3rem}
.rn-capital-note{font-size:.72rem;color:var(--foreground-dim);line-height:1.5}

/* ── Item 1/8 modal form (Set Goal / Deposit / Withdraw) ────── */
.rn-modal-error{background:color-mix(in srgb,var(--rn-red) 12%,transparent);border:1px solid rgba(239,83,80,.4);color:var(--rn-red);border-radius:var(--radius-md);padding:.6rem .85rem;font-size:.78rem;margin-bottom:1rem}
.rn-modal-form{display:flex;flex-direction:column;gap:.3rem}
.rn-modal-label{font-size:.68rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.05em;margin-top:.7rem}
.rn-modal-label:first-child{margin-top:0}
.rn-modal-static{font-size:.95rem;font-weight:700;color:var(--foreground);font-family:var(--font-mono)}
.rn-modal-input,.rn-modal-textarea{background:var(--background-surface);border:1px solid var(--border);color:var(--foreground);padding:.55rem .7rem;border-radius:var(--radius-md);font-size:.85rem;font-family:var(--font-sans)}
.rn-modal-textarea{min-height:4rem;resize:vertical}
.rn-modal-actions{display:flex;justify-content:flex-end;gap:.6rem;margin-top:1.3rem}
.rn-modal-actions .rn-note-cancel-btn,.rn-modal-actions .rn-note-save-btn{font-size:.8rem;padding:.5rem 1.1rem}

/* ── Item 2: Next Trade Checklist -> coaching blocks ────────── */
.rn-coach-block{margin-bottom:1.1rem}
.rn-coach-block-last{margin-bottom:0}
.rn-coach-block-title{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--rn-purple);margin-bottom:.35rem}
.rn-coach-block p{font-size:.8rem;color:var(--foreground-muted);line-height:1.65;margin:0}

/* ── Item 4: Challenge Program filter ───────────────────────── */
.rn-challenge-filter{display:flex;gap:.4rem;margin-bottom:1rem;flex-wrap:wrap}
.rn-challenge-tab{padding:.4rem .85rem;border-radius:var(--radius-full);font-size:.72rem;font-weight:700;text-decoration:none;color:var(--foreground-dim);background:var(--background-surface);border:1px solid var(--border)}
.rn-challenge-tab.active{background:color-mix(in srgb,var(--rn-gold) 16%,transparent);color:var(--rn-gold);border-color:color-mix(in srgb,var(--rn-gold) 45%,transparent)}
/* 2026-08-10 — Trading Archive's Lessons From Success/Failure lens tabs
   (and, scoped the same way, their "1 Step/2 Step/3 Step/Instant Funding"
   program tabs) get a muted, sophisticated status color instead of the
   generic gold above — same color-mix(16%/45%) treatment already used
   for .active everywhere else on this page, just swapped to --rn-green/
   --rn-red so it reads as success/failure, not "just another selection."
   Higher specificity (.rn-lens-* + .rn-challenge-tab.active) than the
   plain .rn-challenge-tab.active rule above, so it wins without !important. */
.rn-lens-success .rn-challenge-tab.active{background:color-mix(in srgb,var(--rn-green) 16%,transparent);color:var(--rn-green);border-color:color-mix(in srgb,var(--rn-green) 45%,transparent)}
.rn-lens-failure .rn-challenge-tab.active{background:color-mix(in srgb,var(--rn-red) 16%,transparent);color:var(--rn-red);border-color:color-mix(in srgb,var(--rn-red) 45%,transparent)}

/* ── Items 2-10 (2026-07-29 mission-alignment pass): Psychology Meets
   Strategy — TraderAlign's behavioural intelligence layer. Distinct teal
   accent so it reads as its own "mindset" voice, separate from the purple
   Right Now coaching panel and the gold goal panel it sits between. ── */
.rn-psych{background:var(--background-elevated);border:1px solid color-mix(in srgb,#2DD4BF 32%,transparent);border-radius:var(--radius-xl);padding:1.5rem 1.6rem;margin-top:1.25rem}
.rn-psych-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:.5rem;margin-bottom:1.2rem}
.rn-psych-title{font-size:1.02rem;font-weight:800;color:#2DD4BF}
.rn-psych-sub{font-size:.74rem;color:var(--foreground-dim)}
.rn-psych-flow{display:flex;align-items:stretch;gap:.6rem;margin-bottom:1.2rem;flex-wrap:wrap}
.rn-psych-step{flex:1;min-width:7rem;background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:.7rem .8rem;text-align:center}
.rn-psych-step-label{font-size:.6rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.06em;margin-bottom:.35rem}
.rn-psych-step-val{font-size:.8rem;font-weight:700;color:var(--foreground)}
.rn-psych-arrow{display:flex;align-items:center;color:var(--foreground-dim);font-weight:700}
.rn-psych-gauge{background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:.9rem 1.1rem;margin-bottom:1.2rem}
.rn-psych-gauge-row{display:grid;grid-template-columns:9.5rem 1fr 4.5rem;align-items:center;gap:.7rem;margin-bottom:.55rem}
.rn-psych-gauge-label{font-size:.72rem;font-weight:700;color:var(--foreground-dim);cursor:help}
.rn-psych-gauge-track{height:.55rem;border-radius:var(--radius-full);background:var(--background-elevated);overflow:hidden}
.rn-psych-gauge-fill{height:100%;border-radius:var(--radius-full);transition:width .3s ease}
.rn-psych-gauge-fill-psych{background:linear-gradient(90deg,#2DD4BF,#60A5FA)}
.rn-psych-gauge-fill-exec{background:linear-gradient(90deg,var(--rn-gold),var(--rn-green))}
.rn-psych-gauge-pct{font-size:.76rem;font-weight:700;font-family:var(--font-mono);color:var(--foreground);text-align:right}
.rn-psych-gauge-gap{font-size:.78rem;color:var(--foreground-muted);line-height:1.6;margin:.5rem 0 0;padding-top:.55rem;border-top:1px solid var(--border)}
@media(max-width:640px){.rn-psych-gauge-row{grid-template-columns:1fr;gap:.3rem}.rn-psych-gauge-pct{text-align:left}}
.rn-psych-lesson{background:color-mix(in srgb,#2DD4BF 8%,transparent);border-left:3px solid #2DD4BF;border-radius:var(--radius-md);padding:.9rem 1.1rem;margin-bottom:1rem}
.rn-psych-lesson-label{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:#2DD4BF;margin-bottom:.4rem}
.rn-psych-lesson p{font-size:.82rem;color:var(--foreground-muted);line-height:1.65;margin:0}
.rn-psych-note{font-size:.78rem;color:var(--foreground-muted);line-height:1.6;background:var(--background-surface);border-radius:var(--radius-md);padding:.7rem .9rem;margin-bottom:.7rem}
.rn-psych-note-good{border-left:3px solid var(--rn-green)}
.rn-psych-evidence{margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border)}
.rn-psych-evidence-title{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--foreground-dim);margin-bottom:.6rem}
.rn-psych-evidence ul{list-style:none;display:flex;flex-direction:column;gap:.55rem}
.rn-psych-evidence li{font-size:.78rem;color:var(--foreground-muted);line-height:1.6;padding-left:1rem;position:relative}
.rn-psych-evidence li::before{content:'◆';position:absolute;left:0;top:.05rem;font-size:.55rem;color:#2DD4BF}

@media(max-width:900px){
    .rn-goal-stats{grid-template-columns:repeat(2,1fr)}
    .rn-capital-grid{grid-template-columns:repeat(2,1fr)}
    .rn-coaching-stats-live{grid-template-columns:repeat(2,1fr)}
    .rn-psych-flow{flex-direction:column}
    .rn-psych-arrow{transform:rotate(90deg);justify-content:center;padding:.1rem 0}
}

/* ── Trading Archive — historical intelligence engine (2026-07-31) ── */
.ta-stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.4rem 1.5rem}
.ta-stat-row{display:flex;justify-content:space-between;align-items:center;padding:.55rem 0;border-bottom:1px solid rgba(255,255,255,.05);gap:1rem}
.ta-stat-label{font-size:.76rem;color:var(--foreground-dim)}
.ta-stat-value{font-size:.85rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground);white-space:nowrap}
.ta-difficulty-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));gap:1rem}
/* Modifier (2026-08-07) — see taSuccessRiskBehavior()'s own comment for
   why: a fixed 2-column density, independent of the card's own width,
   for the one caller that needs Lessons From Failure's exact tile
   proportions at full page width instead of the shared class's default
   auto-fit behavior. */
.ta-difficulty-grid-2col{grid-template-columns:repeat(2,1fr)}
@media(max-width:640px){.ta-difficulty-grid-2col{grid-template-columns:1fr}}
.ta-difficulty-tile{background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem 1.1rem}
.ta-difficulty-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--foreground-dim);margin-bottom:.5rem}
.ta-difficulty-score{font-size:1.6rem;font-weight:800;font-family:var(--font-mono);color:var(--rn-gold)}
.ta-difficulty-explain{font-size:.7rem;color:var(--foreground-muted);line-height:1.55;margin-top:.5rem}
.ta-cause-row{display:grid;grid-template-columns:9rem 1fr 5.5rem;align-items:center;gap:.75rem;margin-bottom:.6rem}
.ta-cause-label{font-size:.76rem;color:var(--foreground-dim)}
.ta-cause-bar{height:.55rem;border-radius:var(--radius-full);background:var(--background-surface);overflow:hidden}
.ta-cause-fill{height:100%;background:linear-gradient(90deg,var(--rn-red),var(--rn-gold));border-radius:var(--radius-full)}
.ta-cause-count{font-size:.74rem;font-family:var(--font-mono);color:var(--foreground);text-align:right}
.ta-compare-table{width:100%;border-collapse:collapse;margin-bottom:.5rem}
.ta-compare-table th{text-align:left;font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--foreground-dim);padding:.5rem .7rem;border-bottom:2px solid var(--border)}
.ta-compare-table th:not(:first-child),.ta-compare-table td:not(:first-child){text-align:right;font-family:var(--font-mono)}
.ta-compare-table td{padding:.5rem .7rem;font-size:.78rem;color:var(--foreground);border-bottom:1px solid rgba(255,255,255,.04)}
.ta-compare-table td:first-child{color:var(--foreground-dim);font-size:.74rem}
@media(max-width:768px){.ta-stats-grid{grid-template-columns:1fr}}

/* ── Lessons From Success — dedicated learning view (2026-08-04) ──
   Reuses this page's own established tokens/components (.rn-trade-journal
   cards, .rn-badge*, .rn-table*, the .ta-stat-row/.ta-cause-* primitives
   above) throughout; the rules below are only the genuinely new shapes
   this view needed (KPI cards, donut/bar/timeline charts, the habit
   list, the goal-achievement grid). */
.ta-success-header{margin-bottom:1.6rem}
.ta-success-title{font-size:1.2rem;font-weight:800;color:var(--foreground);letter-spacing:-.01em}
.ta-success-subtitle{font-size:.82rem;color:var(--foreground-dim);margin-top:.25rem}
.ta-card-h3{font-size:1rem;font-weight:800;color:var(--foreground);margin-bottom:.9rem}
.ta-card-h3-note{font-weight:600;font-size:.74rem;color:var(--foreground-dim)}
.ta-empty-note{font-size:.8rem;color:var(--foreground-dim);line-height:1.6}

/* Item 5: slightly more generous section rhythm and card padding than the
   first pass, and statistic values pushed more visually dominant — all
   within TraderAlign's existing token set (radius-xl, --background-elevated,
   --foreground, var(--font-mono) for numerals), not a new visual language. */
.ta-kpi-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1.1rem;margin-bottom:1.85rem}
.ta-kpi-card{background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);padding:1.5rem 1.6rem;box-shadow:0 1px 2px rgba(0,0,0,.18)}
.ta-kpi-label{display:flex;align-items:center;gap:.45rem;font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-dim);margin-bottom:.7rem}
/* 2026-08-05 polish pass: the original 1.95rem size read as oversized/
   heavy once real multi-word values ("Instant Funding", "Closed
   (Profit)") started wrapping inside a 5-across grid — reduced and given
   graceful word-wrapping so a long real value never stretches or
   crowds its card, while staying one consistent size across all 5 KPI
   cards (never varied per-card). */
.ta-kpi-value{font-size:1.5rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground);letter-spacing:-.01em;line-height:1.25;overflow-wrap:break-word;word-break:break-word}
.ta-kpi-sub{font-size:.7rem;color:var(--foreground-dim);margin-top:.5rem;line-height:1.4}
.ta-kpi-footnote{font-size:.66rem;color:var(--foreground-dim);margin-top:.4rem;line-height:1.4;opacity:.8}
.ta-trend-up{color:var(--rn-green)}
.ta-trend-down{color:var(--rn-red)}
.ta-trend-flat{color:var(--foreground-dim)}
@media(max-width:1100px){.ta-kpi-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:680px){.ta-kpi-grid{grid-template-columns:repeat(2,1fr)}}

/* Item 16: color hierarchy made visible — a small colored-circle icon per
   KPI card (green=success, blue=informational, purple=timing,
   orange/gold=performance, teal=a 5th informational accent already
   established elsewhere on this page for the donut charts). */
.ta-kpi-icon{width:2.15rem;height:2.15rem;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:.85rem}
.ta-kpi-icon-svg{width:1.15rem;height:1.15rem}
.ta-kpi-icon-green{background:var(--rn-green-dim);color:var(--rn-green)}
.ta-kpi-icon-purple{background:var(--rn-purple-dim);color:var(--rn-purple)}
.ta-kpi-icon-blue{background:color-mix(in srgb,var(--rn-blue) 16%,transparent);color:var(--rn-blue)}
.ta-kpi-icon-gold{background:var(--rn-gold-dim);color:var(--rn-gold)}
.ta-kpi-icon-teal{background:color-mix(in srgb,#26A69A 16%,transparent);color:#26A69A}

.ta-section-grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:1.3rem;margin-bottom:1.85rem;align-items:stretch}
.ta-section-grid-3>.rn-trade-journal{margin-bottom:0}
@media(max-width:1100px){.ta-section-grid-3{grid-template-columns:1fr}}

/* Item 6: donut polish — slightly larger ring, a touch thinner stroke for a
   cleaner look (matches the prototype's lighter ring weight), tighter value/
   label vertical stacking, legend rows explicitly space-between so the
   percentage always sits flush right regardless of label length. */
.ta-donut-wrap{display:flex;align-items:center;gap:1.6rem;flex-wrap:wrap}
.ta-donut-svg{width:10.75rem;height:10.75rem;flex-shrink:0}
.ta-donut-value{font-size:6.4px;font-weight:800;font-family:var(--font-mono);fill:var(--foreground)}
.ta-donut-label{font-size:2.1px;font-weight:700;fill:var(--foreground-dim);text-transform:uppercase;letter-spacing:.04em}
.ta-donut-legend{flex:1;min-width:11.5rem;display:flex;flex-direction:column;gap:.65rem}
.ta-donut-legend-row{display:flex;align-items:center;gap:.55rem;font-size:.79rem}
.ta-donut-swatch{width:.62rem;height:.62rem;border-radius:.15rem;flex-shrink:0}
.ta-donut-legend-label{color:var(--foreground-muted);flex:1}
.ta-donut-legend-value{font-family:var(--font-mono);font-weight:700;color:var(--foreground);text-align:right;white-space:nowrap}
.ta-donut-legend-frac{color:var(--foreground-dim);font-weight:500}

.ta-survival-rate{display:flex;flex-direction:column;align-items:center;gap:.55rem;padding:1.2rem 0}
.ta-survival-value{font-size:2.8rem;font-weight:800;font-family:var(--font-mono);color:var(--rn-green)}
.ta-survival-sub{font-size:.8rem;color:var(--foreground-dim)}

/* Item 7: thicker bars, a bit more row spacing, a wider value column so a
   natural-language duration ("1 week 3 days") never clips. */
.ta-bars-wrap{display:flex;flex-direction:column;gap:1.05rem}
/* 2026-08-05: last column widened from a fixed 5.5rem to minmax(5.5rem,
   max-content) — a longer real value ("Ready to Promote") was
   overflowing past the card edge at a fixed width; max-content lets the
   column grow to fit whatever text this specific row actually has,
   never bigger than needed, so short values ("6 Trading Days") render
   exactly as before. */
.ta-bar-row{display:grid;grid-template-columns:5.5rem 1fr minmax(5.5rem, max-content);align-items:center;gap:.8rem}
.ta-bar-label{font-size:.76rem;color:var(--foreground-dim)}
.ta-bar-track{height:.75rem;border-radius:var(--radius-full);background:var(--background-surface);overflow:hidden}
.ta-bar-fill{height:100%;background:linear-gradient(90deg,var(--rn-blue),var(--rn-purple));border-radius:var(--radius-full)}
.ta-bar-value{font-size:.74rem;font-family:var(--font-mono);color:var(--foreground);text-align:right;white-space:nowrap}

.ta-habit-list{list-style:none;display:flex;flex-direction:column;gap:1rem}
.ta-habit-item{display:flex;align-items:flex-start;gap:.8rem}
.ta-habit-icon{flex-shrink:0;width:1.7rem;height:1.7rem;border-radius:50%;background:var(--rn-green-dim);color:var(--rn-green);display:flex;align-items:center;justify-content:center;font-size:.85rem;font-weight:800}
.ta-habit-title{font-size:.83rem;font-weight:700;color:var(--foreground)}
.ta-habit-detail{font-size:.74rem;color:var(--foreground-dim);margin-top:.15rem}

.ta-goal-top-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1.1rem;margin-bottom:1.5rem}
.ta-goal-stat{background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.05rem 1.1rem}
.ta-goal-stat-label{font-size:.64rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-dim);margin-bottom:.5rem}
.ta-goal-stat-value{font-size:1.45rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground);line-height:1.15}
.ta-goal-stat-sub{font-size:.68rem;color:var(--foreground-dim);margin-top:.3rem}
.ta-goal-bottom-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.5rem;padding-top:1.3rem;border-top:1px solid var(--border)}
.ta-goal-subhead{font-size:.76rem;font-weight:700;color:var(--foreground);margin-bottom:.75rem}
@media(max-width:1100px){.ta-goal-top-grid{grid-template-columns:repeat(3,1fr)}.ta-goal-bottom-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:680px){.ta-goal-top-grid{grid-template-columns:repeat(2,1fr)}.ta-goal-bottom-grid{grid-template-columns:1fr}}

.ta-timeline-svg{width:100%;height:auto;max-height:12.5rem}
.ta-timeline-axis{stroke:var(--border);stroke-width:1}
.ta-timeline-tick{font-size:6.5px;fill:var(--foreground-dim);font-family:var(--font-mono)}
/* Item 5: stronger line/dot visual weight — the real progress line should
   read as the clear focal point of the card, not a faint hairline. */
.ta-timeline-line{fill:none;stroke:var(--rn-blue);stroke-width:2.4;stroke-linejoin:round;stroke-linecap:round}
.ta-timeline-dot{fill:var(--rn-blue);stroke:var(--background-elevated);stroke-width:.8}
/* Item 3/8: the chart frame always renders at full size — when there
   isn't enough data yet, the whole SVG dims and a flat dashed reference
   line + centered helper text take the place of a real polyline, never a
   fabricated trend shape. */
.ta-timeline-svg-muted{opacity:.55}
.ta-timeline-placeholder-line{stroke:var(--foreground-dim);stroke-width:1;stroke-dasharray:3 3;opacity:.6}
.ta-timeline-placeholder-text{font-size:6px;font-weight:700;fill:var(--foreground-dim)}
.ta-timeline-placeholder-sub{font-size:5px;fill:var(--foreground-dim);opacity:.8}

/* Item 15: motivating Goal Timeline empty state — real ■■□-style progress
   dots from the real achieved-goal count, not a bare "need N more". */
.ta-timeline-progress-row{display:flex;align-items:center;gap:.9rem;flex-wrap:wrap}
.ta-timeline-progress-dots{display:flex;gap:.35rem}
.ta-timeline-progress-dot{width:.9rem;height:.9rem;border-radius:.25rem;background:var(--background-surface);border:1px solid var(--border)}
.ta-timeline-progress-dot-filled{background:var(--rn-purple);border-color:var(--rn-purple)}
.ta-timeline-progress-label{font-size:.76rem;font-weight:700;color:var(--foreground);font-family:var(--font-mono)}

/* Item 13: Winning Edge value/sub-line — real value on top, its own
   percentage/R-value/version as a smaller line beneath, so a combined
   fact like "London + New York" + "69.1% of total profit" always wraps
   cleanly instead of overflowing one long nowrap string. */
.ta-stat-row-wrap{align-items:flex-start}
.ta-winning-edge-value{display:flex;flex-direction:column;align-items:flex-end;gap:.35rem;max-width:60%;text-align:right}
.ta-winning-edge-value .ta-stat-value{white-space:normal;word-break:break-word;text-align:right}
.ta-winning-edge-sub{font-size:.68rem;color:var(--foreground-dim);font-family:var(--font-mono)}
/* 2026-08-10 — the same green checkmark badge .ta-habit-icon used on
   Strongest Success Habits' list, reused at a smaller size that fits a
   dense stat-row instead of a single-column list. Requested directly:
   "add the green ticks that were previously there... they were
   beautiful." Same colors (--rn-green/--rn-green-dim), just scaled down. */
.ta-stat-label-icon{display:flex;align-items:center;gap:.55rem}
.ta-stat-check{flex-shrink:0;width:1.15rem;height:1.15rem;border-radius:50%;background:var(--rn-green-dim);color:var(--rn-green);display:flex;align-items:center;justify-content:center;font-size:.6rem;font-weight:800}

.ta-success-table{min-width:1080px}
/* Star-rating gold. First pass used the universally-recognized bright
   Material Amber (#FFC107) — correct hue for the "star rating" concept,
   but confirmed live (real screenshot, not a guess) to read as too
   saturated/shouty against this app's dark theme. Switched to this
   app's own --rn-gold token (#E0A64A) — a muted bronze already
   calibrated for this exact dark background elsewhere on this page
   (e.g. "Funded" badges) — same gold hue family, just toned down to
   match the rest of the page's palette instead of a raw, light-theme-
   tuned value.
   Real bug, found live (not a caching issue): `.rn-table td` (defined
   above, an element+class selector — specificity 0,1,1) was silently
   overriding a plain `.ta-impact-stars` class rule (specificity 0,1,0)
   regardless of source order, since specificity always wins ties before
   source order is ever considered — the stars rendered in the table's
   base white text color instead of gold. Fixed by matching/exceeding
   that specificity (`.rn-table td.ta-impact-stars`, 0,2,1) rather than
   reaching for !important — this fix stands regardless of which exact
   gold value is used above it. */
.rn-table td.ta-impact-stars{color:var(--rn-gold);letter-spacing:.05em;font-family:var(--font-sans)}

.ta-impact-info{position:relative;display:inline-block}
/* position:fixed, not absolute (2026-08-06 fix) — the trigger button
   lives inside .rn-table-scroll, which sets overflow-x:auto; per the CSS
   overflow spec, declaring overflow-x without overflow-y forces the
   computed overflow-y to 'auto' too (a real, well-known CSS gotcha, not a
   bug in that rule — every horizontally-scrollable table in this app
   relies on that exact overflow-x:auto). That meant a position:absolute
   popover was clipped to the scroll container's own box the moment the
   table had few enough rows to be shorter than the popover, forcing a
   confusing inner scrollbar instead of the popover floating freely.
   position:fixed escapes every scrolling/overflow ancestor entirely
   (nothing between this element and the viewport sets a transform/
   filter/will-change that would re-trap it); its top/left are computed
   from the trigger button's real on-screen position by
   taToggleImpactInfo() at open time, not by CSS alone, since fixed
   positioning has no "anchor to my parent" concept the way absolute
   does. Shared by both Prop Firms and Live Accounts' Lessons From
   Success — taSuccessEvidenceTrades() is one function, not duplicated
   per type, so this fix covers both automatically. */
.ta-impact-popover{position:fixed;z-index:200;width:15rem;background:linear-gradient(180deg,color-mix(in srgb,var(--popover) 94%,white 6%),var(--popover));border:1px solid color-mix(in srgb,var(--rn-gold) 22%,var(--border));border-radius:var(--radius-lg);box-shadow:0 16px 40px rgba(0,0,0,.45),0 0 0 1px rgba(0,0,0,.15);padding:.7rem .85rem .75rem;text-align:left;text-transform:none;font-weight:400;letter-spacing:normal;font-size:.72rem;line-height:1.5;color:var(--foreground);animation:ta-impact-pop-in .15s ease-out}
.ta-impact-popover-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem;padding-bottom:.45rem;border-bottom:1px solid var(--border)}
.ta-impact-popover-title{font-size:.66rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--rn-gold)}
.ta-impact-popover-close{background:none;border:none;color:var(--foreground-dim);cursor:pointer;font-size:.72rem;line-height:1;padding:.15rem;border-radius:var(--radius-sm)}
.ta-impact-popover-close:hover{color:var(--foreground);background:rgba(255,255,255,.06)}
.ta-impact-popover-row{display:flex;align-items:center;gap:.55rem;padding:.16rem 0;white-space:nowrap}
.ta-impact-popover-star{display:inline-flex;align-items:center;justify-content:center;min-width:2.5rem;padding:.08rem .35rem;border-radius:999px;background:color-mix(in srgb,var(--rn-gold) 16%,transparent);color:var(--rn-gold);font-weight:700;font-size:.68rem;letter-spacing:.02em}
.ta-impact-popover-desc{color:var(--foreground-dim)}
.ta-impact-popover-cap{margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--border);color:var(--foreground-dim);white-space:normal;display:flex;gap:.45rem;align-items:flex-start}
.ta-impact-popover-cap b{color:var(--rn-gold);flex-shrink:0;font-size:.75rem}
@keyframes ta-impact-pop-in{from{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}

/* Single-account Lessons From Success rebuild (2026-08-05) — account
   selector row + "Coming Next" (not-yet-reached level) bar state. */
.ta-account-selector-row{margin-bottom:.3rem}
.ta-account-selector-label{font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--foreground-dim);margin-bottom:.4rem}
.ta-bar-track-upcoming{opacity:.35}
.ta-bar-value-upcoming{color:var(--foreground-dim);font-style:italic}
.ta-bar-track-active{background:color-mix(in srgb,var(--rn-blue) 18%,transparent)}
.ta-bar-value-active{color:var(--rn-blue);font-weight:700}
/* The universal "this is genuinely live/active right now" indicator —
   same pulsing-dot convention already established for account badges
   elsewhere in this app (src/AccountPicker.php's .ap-glow-dot), reused
   here rather than inventing a second one. Always the same green,
   regardless of the surrounding text's own color (blue "In Progress"
   bar text, purple/gold Journey Status value, etc.) — green is the
   fixed, universal signal for "active," not tied to context color. */
.ta-live-dot{display:inline-block;width:.45rem;height:.45rem;border-radius:50%;background:var(--rn-green);box-shadow:0 0 5px var(--rn-green);animation:rn-pulse 1.4s infinite;margin-right:.4rem;vertical-align:middle}

/* ── Live Accounts single-account rebuild (2026-08-06) — matches the
   approved prototype. Reuses every existing Trading Archive class
   (ta-kpi-grid, ta-card-h3, ta-donut-wrap, ta-goal-top-grid, ta-cause-row,
   ta-habit-list, rn-trade-journal, etc.) wherever the shape is identical;
   only genuinely new components (the equity curve chart, Money
   Management's 6-tile grid, the compact inline stat rows used by
   Deposit/Withdrawal History and Trading Longevity, the info-dot
   tooltip marker) get new rules here. Nothing in this block is referenced by
   any Prop Firms markup. Note for future edits to this comment: never
   write a class-name-plus-asterisk immediately followed by a forward
   slash anywhere in here, in prose or in an example — that exact two
   character combination closes this CSS comment early and silently
   discards every rule after it, which is exactly the bug this block
   shipped with once already before this note was added. */
.ta-kpi-grid.ta-kpi-grid-6{grid-template-columns:repeat(6,1fr)}
@media(max-width:1300px){.ta-kpi-grid.ta-kpi-grid-6{grid-template-columns:repeat(3,1fr)}}
@media(max-width:680px){.ta-kpi-grid.ta-kpi-grid-6{grid-template-columns:repeat(2,1fr)}}
.ta-info-dot{font-size:.72rem;color:var(--foreground-dim);cursor:help;font-style:normal}
.ta-live-growth-row{display:grid;grid-template-columns:1fr 2fr;gap:1.3rem;margin-bottom:1.85rem;align-items:stretch}
.ta-live-growth-row>.rn-trade-journal{margin-bottom:0}
@media(max-width:1100px){.ta-live-growth-row{grid-template-columns:1fr}}
.ta-live-praise{font-size:.78rem;color:var(--rn-green);margin-top:.9rem;font-weight:600}
.ta-live-goal-bottom-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1.5rem;padding-top:1.3rem;border-top:1px solid var(--border);margin-top:1.3rem}
@media(max-width:680px){.ta-live-goal-bottom-grid{grid-template-columns:1fr}}
.ta-section-grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:1.3rem;margin-bottom:1.85rem;align-items:stretch}
.ta-section-grid-2>.rn-trade-journal{margin-bottom:0}
@media(max-width:900px){.ta-section-grid-2{grid-template-columns:1fr}}

.ta-live-growth-total{font-size:.78rem;color:var(--foreground-dim)}
.ta-live-growth-total b{font-size:.95rem;margin-left:.35rem}
.ta-equity-svg{width:100%;height:auto;display:block}
.ta-equity-gridline{stroke:var(--border);stroke-width:.5;opacity:.6}
.ta-equity-tick{fill:var(--foreground-dim);font-size:9px;font-family:var(--font-mono)}
.ta-equity-area{fill:color-mix(in srgb,var(--rn-green) 14%,transparent);stroke:none}
.ta-equity-line{fill:none;stroke:var(--rn-green);stroke-width:2.2;stroke-linejoin:round;stroke-linecap:round}
.ta-equity-dot{fill:var(--rn-green);stroke:var(--background);stroke-width:1}
.ta-equity-stats{display:flex;justify-content:space-between;gap:1rem;margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border);flex-wrap:wrap}
.ta-equity-stats>div{display:flex;flex-direction:column;gap:.2rem}
.ta-equity-stat-label{font-size:.68rem;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.04em}
.ta-equity-stat-value{font-size:1rem;font-weight:700;font-family:var(--font-mono)}

/* ════════════════════════════════════════════════════════════════════
   LESSONS FROM FAILURE — single-account rebuild (2026-08-06)
   Reuses this file's existing .rn-trade-journal card / .ta-card-h3 /
   .ta-stat-row / .ta-cause-row / .ta-difficulty-grid / .rn-goal-bar /
   .ta-donut-* / .ta-equity-* / .rn-workspace-list / .rn-badge* / .ta-
   impact-stars components wherever the same visual concept already
   exists — the rules below only cover the handful of genuinely new
   widgets this page introduces (the breach status card, the milestone
   timeline, the six-column stat strip, the ranked-reason row, the
   avoidability card, and the four-column goals grid).
   ════════════════════════════════════════════════════════════════════ */
/* minmax(0, …) — not a bare 1.5fr/1fr — is required here, not decorative:
   a bare fr track has an implicit min-width:auto, so a wide intrinsic-
   content child (the Trades That Led To The Breach table, which reuses
   .ta-success-table's own min-width:1080px) "blows out" its track past
   its fr-share, skewing the row's real proportions and squeezing the
   Breaches By Reason donut down far narrower than intended — at some
   real widths this pushed the donut's track below its own min-content
   size entirely, which showed as the donut appearing to be missing.
   minmax(0, …) caps the track at 0, so .rn-table-scroll's own
   overflow-x:auto (already on the table) is what handles overflow —
   scrolling the table horizontally in place — instead of the grid track
   itself stretching. Confirmed via a real measured DOM check (Puppeteer)
   before/after this fix. align-items:stretch (was start) + the two rules
   below it make both cards in a row match the taller one's real height,
   instead of leaving a visible gap of bare page background under
   whichever card happens to have less real content that row. */
.ta-fail-2col{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:1.3rem;align-items:stretch;margin-bottom:1.85rem}
.ta-fail-2col-tight{display:grid;grid-template-columns:minmax(0,2.2fr) minmax(0,1fr);gap:1.3rem;align-items:stretch;margin-bottom:1.85rem}
.ta-fail-2col>.rn-trade-journal,.ta-fail-2col-tight>.rn-trade-journal{margin-bottom:0;display:flex;flex-direction:column}
/* Kept side-by-side down to a genuinely narrow width — this app's fixed
   4rem icon sidebar plus normal window/OS scaling routinely leaves the
   real content area under 1000px even on an ordinary laptop, which an
   earlier, more conservative breakpoint collapsed to single-column far
   more often than intended (confirmed live via real screenshots at
   several widths). 760px is a genuine small-tablet/mobile cutoff. */
@media(max-width:760px){.ta-fail-2col,.ta-fail-2col-tight{grid-template-columns:1fr}}

.ta-fail-selector-inner{display:flex;align-items:center;gap:2rem;flex-wrap:wrap}
.ta-fail-mini-stat{display:flex;flex-direction:column;gap:.35rem;min-width:6.5rem}
.ta-fail-mini-label{font-size:.64rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--foreground-dim)}
.ta-fail-mini-value{font-size:1.05rem;font-weight:800;color:var(--foreground)}

.ta-breach-glow-card{border-color:color-mix(in srgb,var(--rn-red) 55%,var(--border));box-shadow:0 0 0 1px color-mix(in srgb,var(--rn-red) 25%,transparent),0 0 22px -6px color-mix(in srgb,var(--rn-red) 45%,transparent);display:flex;align-items:center;justify-content:center}
.ta-breach-status{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.3rem}
.ta-breach-status-icon{font-size:2.1rem;color:var(--rn-red);line-height:1;margin-bottom:.2rem}
.ta-breach-status-title{font-size:1.4rem;font-weight:800;letter-spacing:.04em;color:var(--rn-red)}
.ta-breach-status-reason{font-size:.82rem;color:var(--foreground-muted);margin-bottom:.5rem}
.ta-breach-status-divider{width:100%;border-top:1px solid color-mix(in srgb,var(--rn-red) 25%,var(--border));margin:.4rem 0}
.ta-breach-status-date-label{font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--foreground-dim)}
.ta-breach-status-date{font-size:1.05rem;font-weight:800;color:var(--foreground)}

.ta-fail-stat-strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;margin-bottom:1.85rem}
.ta-fail-stat-col{padding:0 1.1rem;text-align:center}
.ta-fail-stat-col:not(:first-child){border-left:1px solid var(--border)}
.ta-fail-stat-label{font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--foreground-dim);margin-bottom:.5rem}
.ta-fail-stat-value{font-size:1.35rem;font-weight:800;font-family:var(--font-mono);color:var(--foreground)}
.ta-fail-stat-sub{font-size:.7rem;color:var(--foreground-dim);margin-top:.4rem;line-height:1.4}
@media(max-width:1000px){.ta-fail-stat-strip{grid-template-columns:repeat(3,1fr);gap:1.1rem 0}.ta-fail-stat-col:nth-child(3n+1){border-left:none}.ta-fail-stat-col{border-top:1px solid var(--border);padding-top:1rem}.ta-fail-stat-col:nth-child(-n+3){border-top:none}}
@media(max-width:560px){.ta-fail-stat-strip{grid-template-columns:repeat(2,1fr)}.ta-fail-stat-col:nth-child(2n+1){border-left:none}.ta-fail-stat-col:nth-child(-n+2){border-top:none}}

/* Item 6 (2026-08-06) — Breach At A Glance's own consistent typography
   scale: every label is the same small-caps weight/size, every value the
   same base size/weight — severity is carried by color only (negative/
   positive), never by an ad hoc bigger font on one row. */
.ta-glance-list{display:flex;flex-direction:column}
.ta-glance-row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;padding:.7rem 0;border-bottom:1px solid rgba(255,255,255,.05)}
.ta-glance-row:last-child{border-bottom:none;padding-bottom:.2rem}
.ta-glance-label{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.045em;color:var(--foreground-dim);flex-shrink:0}
.ta-glance-value{font-size:.86rem;font-weight:700;color:var(--foreground);text-align:right;font-family:var(--font-mono)}
.ta-glance-value-sub{display:block;font-size:.66rem;font-weight:500;color:var(--foreground-dim);margin-top:.15rem;font-family:var(--font-sans)}
.ta-glance-value-negative{color:var(--rn-red)}
.ta-glance-value-positive{color:var(--rn-green)}

.ta-timeline{display:flex;align-items:flex-start;justify-content:space-between;gap:.5rem;position:relative;padding-top:.6rem}
.ta-timeline::before{content:'';position:absolute;top:1.35rem;left:5%;right:5%;height:2px;background:var(--border);z-index:0}
.ta-timeline-node{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:.3rem;flex:1;min-width:5.5rem}
.ta-timeline-dot{width:.85rem;height:.85rem;border-radius:50%;border:2px solid var(--background-elevated);box-shadow:0 0 0 1px var(--border)}
.ta-timeline-dot-neutral{background:var(--foreground-dim)}
.ta-timeline-dot-green{background:var(--rn-green)}
.ta-timeline-dot-gold{background:var(--rn-gold)}
.ta-timeline-dot-red{background:var(--rn-red);box-shadow:0 0 0 1px var(--rn-red)}
.ta-timeline-date{font-size:.66rem;color:var(--foreground-dim);margin-top:.4rem}
.ta-timeline-label{font-size:.78rem;font-weight:700;color:var(--foreground)}
.ta-timeline-explain{text-align:center;font-size:.8rem;color:var(--foreground-muted);margin-top:1.3rem;padding-top:1rem;border-top:1px solid var(--border)}
@media(max-width:700px){.ta-timeline{flex-wrap:wrap}.ta-timeline::before{display:none}}

.ta-reason-row{display:grid;grid-template-columns:1.5rem 9rem 1fr 3rem;align-items:center;gap:.7rem;margin-bottom:.5rem}
.ta-reason-rank{display:flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:50%;background:var(--background-surface);border:1px solid var(--border);font-size:.72rem;font-weight:800;color:var(--foreground-dim)}
.ta-reason-detail{grid-column:2/-1;font-size:.72rem;color:var(--foreground-dim);margin:-.1rem 0 .6rem;line-height:1.5}
@media(max-width:700px){.ta-reason-row{grid-template-columns:1.5rem 1fr 3rem}.ta-reason-row .ta-cause-label{grid-column:2/3}.ta-reason-row .ta-cause-bar{grid-column:2/4;order:3}}

/* Item 8 (2026-08-06) — Primary Reasons For The Breach, card layout
   (supersedes the dense .ta-reason-row grid above, kept unused per the
   additive-only rule). Each reason gets its own icon badge, a clear
   Low/Medium/High impact pill, and full-width room for a real,
   beginner-friendly explanation sentence instead of a caption squeezed
   under a bar. */
.ta-reason-list{display:flex;flex-direction:column;gap:1.1rem}
.ta-reason-card{display:flex;gap:.9rem;align-items:flex-start}
.ta-reason-card-icon{flex-shrink:0;width:2.3rem;height:2.3rem;border-radius:50%;background:var(--background-surface);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:1.05rem}
.ta-reason-card-body{flex:1;min-width:0}
.ta-reason-card-head{display:flex;justify-content:space-between;align-items:center;gap:.7rem;flex-wrap:wrap;margin-bottom:.5rem}
.ta-reason-card-label{font-size:.86rem;font-weight:700;color:var(--foreground)}
.ta-reason-card-bar{margin:0 0 .55rem}
.ta-reason-card-detail{font-size:.78rem;color:var(--foreground-muted);line-height:1.65;margin:0}

.ta-avoid-body{display:flex;gap:1rem;align-items:flex-start}
.ta-avoid-icon{flex-shrink:0;width:2.6rem;height:2.6rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.3rem}
.ta-avoid-icon-yes{background:var(--rn-green-dim);color:var(--rn-green)}
.ta-avoid-icon-no{background:var(--background-surface);color:var(--foreground-dim)}
.ta-avoid-headline{font-size:1.05rem;font-weight:800;color:var(--foreground);margin-bottom:.5rem}
.ta-avoid-explain{font-size:.82rem;color:var(--foreground-muted);line-height:1.6;margin-bottom:.7rem}
.ta-avoid-calc{font-size:.74rem;color:var(--foreground-dim)}
.ta-avoid-calc summary{cursor:pointer;color:var(--rn-gold);font-weight:700;list-style:none}
.ta-avoid-calc summary::-webkit-details-marker{display:none}
.ta-avoid-calc summary::after{content:' →'}
.ta-avoid-calc p{margin-top:.5rem;line-height:1.55}

.ta-fail-goals-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.3rem}
@media(max-width:900px){.ta-fail-goals-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.ta-fail-goals-grid{grid-template-columns:1fr}}
.ta-fail-goal-col{display:flex;flex-direction:column}
.ta-fail-goal-label{font-size:.78rem;font-weight:700;color:var(--foreground);margin-bottom:.3rem}
.ta-fail-goal-target{font-size:.7rem;color:var(--foreground-dim);margin-bottom:.5rem}
.ta-fail-goal-actual{font-size:.82rem;font-weight:700;margin-top:.4rem}

.ta-equity-legend{display:flex;gap:1.3rem;flex-wrap:wrap;margin-bottom:.7rem;font-size:.72rem;color:var(--foreground-muted)}
.ta-equity-legend-swatch{display:inline-block;width:.85rem;height:.4rem;border-radius:2px;margin-right:.35rem;vertical-align:middle}
.ta-equity-legend-dot{display:inline-block;width:.5rem;height:.5rem;border-radius:50%;background:var(--rn-red);margin-right:.4rem;vertical-align:middle}
/* Item 7 (2026-08-06) — the simplified single-line chart: one clean
   "Below Breach Level" danger zone (a flat shaded rectangle, not a
   region computed from a second "peak" line a newcomer has to first
   understand), a dashed breach-level threshold line with its own dollar
   label, and a dashed starting-balance reference line for scale. */
.ta-fail-danger-zone{fill:color-mix(in srgb,var(--rn-red) 14%,transparent);stroke:none}
.ta-fail-breach-line{stroke:var(--rn-red);stroke-width:1.4;stroke-dasharray:4 3}
.ta-fail-breach-line-label{fill:var(--rn-red);font-size:8.5px;font-weight:700;font-family:var(--font-sans)}
.ta-fail-start-line{stroke:var(--rn-green);stroke-width:1;stroke-dasharray:2 3;opacity:.55}
.ta-fail-breach-dot{fill:var(--rn-red);stroke:var(--background);stroke-width:1.5}
.ta-fail-breach-label{fill:var(--rn-red);font-size:9px;font-weight:700;font-family:var(--font-sans)}
/* Chart annotation redesign (2026-08-06 refinement pass) — see
   taFailureEquityChart()'s own doc comment for the full reasoning: the
   breach-level reference line gets a compact axis-anchored badge on the
   left (never competing with the always-right-edge breach point), and
   the breach point itself gets a real floating callout box with a
   leader line instead of raw <text> that had nowhere to grow. Both
   backgrounds are intentionally opaque (background-elevated, matching
   the card itself) so they can sit cleanly on top of a gridline, tick
   label, or the balance line passing underneath without visual noise. */
.ta-fail-breach-badge-bg{fill:var(--background-elevated);stroke:var(--rn-red);stroke-width:1}
.ta-fail-breach-badge-text{fill:var(--rn-red);font-size:8px;font-weight:800;font-family:var(--font-mono)}
.ta-fail-breach-leader{stroke:var(--rn-red);stroke-width:1;stroke-dasharray:2 2;opacity:.6}
.ta-fail-breach-callout-bg{fill:var(--background-elevated);stroke:var(--rn-red);stroke-width:1.2;filter:drop-shadow(0 2px 4px rgba(0,0,0,.35))}
.ta-fail-breach-callout-title{fill:var(--rn-red);font-size:8px;font-weight:800;letter-spacing:.03em;font-family:var(--font-sans)}
.ta-fail-breach-callout-sub{fill:var(--foreground-dim);font-size:7.5px;font-weight:600;font-family:var(--font-sans)}

.ta-fail-breach-row{background:color-mix(in srgb,var(--rn-red) 12%,transparent)!important;box-shadow:inset 3px 0 0 var(--rn-red)}
/* .rn-row-breach — renderRnTradeTable()'s own additive breach-trade highlight (see that function's $breachTradeId param), used exclusively by renderBreachTradesView() below. Same visual treatment as .ta-fail-breach-row above, just scoped to the shared .rn-table component's own row instead of this page's compact evidence-table preview. */
.rn-table tr.rn-row-breach{background:color-mix(in srgb,var(--rn-red) 14%,transparent)!important;box-shadow:inset 3px 0 0 var(--rn-red)}
.rn-table tr.rn-row-breach td{border-color:color-mix(in srgb,var(--rn-red) 25%,var(--border))}

.ta-fail-footer{text-align:center;font-size:.85rem;color:var(--foreground-muted);border:1px solid color-mix(in srgb,var(--rn-red) 30%,var(--border));border-radius:var(--radius-xl);padding:1.1rem 1.5rem;margin-top:.5rem}
.ta-fail-footer-icon{color:var(--rn-red);margin-right:.4rem}

/* Lessons From Failure's own page header (item 5, 2026-08-06 refinement
   pass) — upgraded from a plain left-accent-bar card to a genuinely
   designed banner: a soft red radial glow background, the warning icon
   in its own circular badge (matching the visual weight of every other
   circular icon badge already used elsewhere on this page, e.g.
   .ta-avoid-icon), and a larger, heavier title — reads as an
   intentionally designed section opener, not text placed on a dark
   background. Still quieter than the account-specific BREACHED status
   card's full alarm glow (.ta-breach-glow-card) — that one is reserved
   for "this specific account failed," this one just sets the page's
   overall tone. */
.ta-fail-page-header{display:flex;align-items:center;gap:1.3rem;padding:1.5rem 1.75rem;border:1px solid color-mix(in srgb,var(--rn-red) 28%,var(--border));background:radial-gradient(120% 140% at 0% 0%,color-mix(in srgb,var(--rn-red) 10%,transparent),transparent 65%),var(--background-elevated);position:relative;overflow:hidden}
.ta-fail-page-header::before{content:'';position:absolute;left:0;top:0;bottom:0;width:5px;background:linear-gradient(180deg,var(--rn-red),color-mix(in srgb,var(--rn-red) 40%,transparent))}
.ta-fail-page-header-icon{flex-shrink:0;width:3.2rem;height:3.2rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.55rem;color:var(--rn-red);background:color-mix(in srgb,var(--rn-red) 16%,transparent);border:1px solid color-mix(in srgb,var(--rn-red) 35%,transparent)}
.ta-fail-page-header .rn-subtitle{margin-top:.2rem}

/* Lessons From Success (Single Account)'s own page header/footer
   (2026-08-07) — the exact same treatment as Lessons From Failure's
   .ta-fail-page-header/.ta-fail-footer above, byte-identical structure,
   recolored to this page's own green identity rather than a reskinned
   red one. Deliberately its own pair of classes, not a --tone CSS
   variable swapped into the .ta-fail-* rules — this page's real accent
   color is a small, fixed set (subtle green success identity), not an
   arbitrary runtime-computed one, so a second, parallel set of rules is
   simpler and safer than parameterizing the shared ones. */
.ta-success-page-header{display:flex;align-items:center;gap:1.3rem;padding:1.5rem 1.75rem;border:1px solid color-mix(in srgb,var(--rn-green) 28%,var(--border));background:radial-gradient(120% 140% at 0% 0%,color-mix(in srgb,var(--rn-green) 10%,transparent),transparent 65%),var(--background-elevated);position:relative;overflow:hidden}
.ta-success-page-header::before{content:'';position:absolute;left:0;top:0;bottom:0;width:5px;background:linear-gradient(180deg,var(--rn-green),color-mix(in srgb,var(--rn-green) 40%,transparent))}
.ta-success-page-header-icon{flex-shrink:0;width:3.2rem;height:3.2rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.55rem;color:var(--rn-green);background:color-mix(in srgb,var(--rn-green) 16%,transparent);border:1px solid color-mix(in srgb,var(--rn-green) 35%,transparent)}
.ta-success-page-header .rn-subtitle{margin-top:.2rem}
.ta-success-footer{text-align:center;font-size:.85rem;color:var(--foreground-muted);border:1px solid color-mix(in srgb,var(--rn-green) 30%,var(--border));border-radius:var(--radius-xl);padding:1.1rem 1.5rem;margin-top:.5rem}
.ta-success-footer-icon{color:var(--rn-green);margin-right:.4rem}
/* Tasteful "green scent" (2026-08-07, correction pass) — a subtle
   left-edge accent so a card reads as part of a successful journey at a
   glance, without repainting its whole background. Deliberately a thin
   border, not a glow/fill (Lessons From Failure reserves its own strong
   visual alarm — .ta-breach-glow-card — for the one card that's actually
   a warning; Success has no equivalent "danger" card to escalate from,
   so nothing here should compete with that for visual weight). */
.ta-success-tint{border-left:3px solid color-mix(in srgb,var(--rn-green) 45%,var(--border))}

.ta-fin-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.3rem}
@media(max-width:900px){.ta-fin-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.ta-fin-grid{grid-template-columns:1fr}}
.ta-fin-stat{padding:.9rem 1rem;background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-md)}
.ta-fin-label{font-size:.7rem;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.04em;margin-bottom:.4rem}
.ta-fin-value{font-size:1.3rem;font-weight:800;font-family:var(--font-mono)}
.ta-fin-sub{font-size:.72rem;color:var(--foreground-dim);margin-top:.3rem}
.ta-fin-stat-score{display:flex;flex-direction:column;align-items:flex-start}
.ta-fin-score-ring{margin:.2rem 0}

.ta-live-substats{display:flex;flex-direction:column;gap:.55rem;margin-bottom:1.1rem}
.ta-live-substats>div{display:flex;justify-content:space-between;align-items:baseline;gap:.6rem}
.ta-live-substat-label{font-size:.78rem;color:var(--foreground-dim)}
.ta-live-substat-value{font-size:.88rem;font-weight:700;font-family:var(--font-mono)}
.ta-longevity-headline{font-size:1.8rem;font-weight:800;color:var(--rn-blue);margin-bottom:.7rem}
.ta-live-instrument-bars{display:flex;flex-direction:column;gap:.6rem}

/* Shared empty-state block (2026-08-06, taEmptyStateBlock()) — a centered
   icon + heading + explanation for a card with genuinely nothing to show
   yet, replacing a bare one-line message that left a large unexplained
   blank rectangle. Used by Trading Consistency and Your Winning Edge
   (the latter shared with Prop Firms, so both benefit). */
.ta-empty-state-block{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:2.5rem 1.5rem;gap:.6rem;min-height:12rem}
.ta-empty-state-icon{width:3rem;height:3rem;color:var(--foreground-dim);opacity:.5}
.ta-empty-state-heading{font-size:.92rem;font-weight:700;color:var(--foreground-dim)}
.ta-empty-state-text{font-size:.78rem;color:var(--foreground-dim);opacity:.85;max-width:22rem;line-height:1.5}

/* ── 4-Stage Trade Lifecycle (single-trade view, Active Journeys →
   Trade → View Details) — the exact same row-per-metric, four-stage
   comparison philosophy as Reports' "four-stage-summary" section
   (.fs-stage-row/.fs-stage-card), scaled down to one trade. Reuses this
   page's own already-established blue/purple/gold/green stage palette
   (--rn-blue/--rn-purple/--rn-gold/--rn-green, defined above) rather
   than importing Reports' token names — same four-color scheme, this
   page's own tokens, no new visual language introduced. ── */
/* ── 2026-08-19 visual redesign: quiet, muted stage palette (scoped to
   this section only — the bright --rn-* brand colors are still used
   correctly everywhere else on this page, e.g. badges/pills; this table
   deliberately reads calmer than the rest of the app, per explicit
   instruction that this specific page should feel like the most
   considered surface in the product). Soft tinted header backgrounds +
   a small colored dot next to the stage name replace the old harsh 3px
   top-border bars; section "COMPLETE" banners become a centered-label
   milestone rule instead of a full-width flat color band; empty states
   move from dashed "warning box" borders to a solid, softly-tinted
   invite card. Nothing here changes what data renders or where — this
   whole block is presentation only. ── */
.tl4-section{
    --tl4-1:#6E9BD6; --tl4-1-bg:color-mix(in srgb,var(--tl4-1) 7%,transparent);
    --tl4-2:#9C90DE; --tl4-2-bg:color-mix(in srgb,var(--tl4-2) 7%,transparent);
    --tl4-3:#CDA15F; --tl4-3-bg:color-mix(in srgb,var(--tl4-3) 7%,transparent);
    --tl4-4:#5FAE8C; --tl4-4-bg:color-mix(in srgb,var(--tl4-4) 7%,transparent);
    background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-xl);
    padding:2rem;margin-top:1.75rem;
    box-shadow:0 24px 48px -32px rgba(0,0,0,.4);
}
.tl4-head{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:1.6rem}
.tl4-title{font-size:1.2rem;font-weight:800;color:var(--foreground);letter-spacing:-.01em}
.tl4-sub{font-size:.78rem;color:var(--foreground-dim);margin-top:.3rem;max-width:42rem;line-height:1.55}
.tl4-strategy-chip{font-size:.72rem;font-weight:700;color:var(--foreground-dim);background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-full);padding:.35rem .8rem;white-space:nowrap}
.tl4-strategy-history{margin-top:.5rem;max-width:20rem}
.tl4-strategy-history summary{font-size:.7rem;font-weight:700;color:var(--tl4-2);cursor:pointer;text-align:right;list-style:none}
.tl4-strategy-history summary::-webkit-details-marker{display:none}
.tl4-strategy-history-text{font-size:.74rem;color:var(--foreground-muted);line-height:1.5;margin-top:.4rem;padding:.6rem .75rem;background:var(--background-surface);border-radius:var(--radius-md)}
.tl4-table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--border);border-radius:var(--radius-lg)}
.tl4-table{width:100%;min-width:860px;border-collapse:collapse}
.tl4-table th,.tl4-table td{padding:.75rem .9rem;font-size:.8rem;text-align:left;vertical-align:top}
.tl4-metric-col{width:13rem;min-width:11rem;font-weight:500;color:var(--foreground-dim);font-size:.73rem;white-space:nowrap}
.tl4-table td.tl4-metric-col{color:var(--foreground-muted);font-weight:500}
.tl4-stage-col{font-family:var(--font-mono);font-weight:600;color:var(--foreground)}
.tl4-table thead .tl4-stage-col{font-family:var(--font-sans);font-weight:400;border-bottom:1px solid var(--border)}
.tl4-stage-num{display:flex;align-items:center;gap:.35rem;font-size:.6rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;opacity:.75}
.tl4-stage-num::before{content:"";display:inline-block;width:.42rem;height:.42rem;border-radius:50%;background:currentColor}
.tl4-stage-name{display:block;font-size:.8rem;font-weight:700;margin-top:.2rem;color:var(--foreground)}
.tl4-stage-1{color:var(--tl4-1)} thead .tl4-stage-1{background:var(--tl4-1-bg)}
.tl4-stage-2{color:var(--tl4-2)} thead .tl4-stage-2{background:var(--tl4-2-bg)}
.tl4-stage-3{color:var(--tl4-3)} thead .tl4-stage-3{background:var(--tl4-3-bg)}
.tl4-stage-4{color:var(--tl4-4)} thead .tl4-stage-4{background:var(--tl4-4-bg)}
.tl4-table tbody .tl4-stage-col{color:var(--foreground)}
.tl4-table tbody tr:nth-child(even){background:rgba(255,255,255,.014)}
.tl4-table tbody tr:hover{background:rgba(255,255,255,.03)}
.tl4-table td.tl4-na{color:var(--foreground-dim);font-family:var(--font-sans);font-size:.78rem;font-weight:400;cursor:help}

/* ── 2026-08-13: MT5 Trade Record, Planned vs. Broker-Executed ── */
.tl4-mt5-section-head{margin-bottom:1.3rem}
.tl4-mt5-pending{display:flex;align-items:flex-start;gap:.75rem;padding:1.1rem 1.2rem;background:var(--background-surface);border:1px dashed var(--border);border-radius:var(--radius-lg)}
.tl4-mt5-pending-icon{font-size:1.1rem;line-height:1.3}
.tl4-mt5-pending-title{font-size:.82rem;font-weight:800;color:var(--tl4-3)}
.tl4-mt5-pending-sub{font-size:.76rem;color:var(--foreground-dim);margin-top:.3rem;line-height:1.55;max-width:44rem}
.tl4-mt5-label{font-size:.68rem;font-weight:700;color:var(--foreground-dim);text-transform:uppercase;letter-spacing:.08em;margin:0 0 .7rem}
.tl4-mt5-table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:.3rem;border:1px solid var(--border);border-radius:var(--radius-lg)}
.tl4-mt5-table{width:100%;min-width:1040px;border-collapse:collapse}
.tl4-mt5-table th{white-space:nowrap;text-align:left;font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--foreground-dim);padding:.7rem .9rem;border-bottom:1px solid var(--border);background:var(--tl4-4-bg)}
.tl4-mt5-table td{white-space:nowrap;padding:.75rem .9rem;font-size:.8rem;color:var(--foreground);font-family:var(--font-mono);border-bottom:1px solid rgba(255,255,255,.04)}
.tl4-mt5-table tbody tr:nth-child(even){background:rgba(255,255,255,.014)}
.tl4-mt5-table tbody tr:hover{background:rgba(255,255,255,.03)}
.tl4-mt5-table tbody tr.tl4-mt5-row-current{background:var(--tl4-1-bg);box-shadow:inset 3px 0 0 var(--tl4-1)}
.tl4-mt5-table tbody tr.tl4-mt5-row-current:hover{background:color-mix(in srgb,var(--tl4-1) 12%,transparent)}
.tl4-mt5-table td.tl4-na{color:var(--foreground-dim);font-family:var(--font-sans);font-size:.78rem;font-weight:400;cursor:help}
.tl4-mt5-meta{font-size:.72rem;color:var(--foreground-dim);margin:.5rem 0 1.5rem}
.tl4-mt5-compare-table{width:100%;min-width:640px;border-collapse:collapse}
.tl4-mt5-compare-table th{text-align:left;font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--foreground-dim);padding:.65rem .9rem;border-bottom:1px solid var(--border)}
.tl4-mt5-compare-table td{padding:.72rem .9rem;font-size:.8rem;color:var(--foreground);border-bottom:1px solid rgba(255,255,255,.04);font-family:var(--font-mono)}
.tl4-mt5-compare-table td.tl4-mt5-compare-label{font-family:var(--font-sans);font-weight:500;color:var(--foreground-dim);font-size:.74rem}
.tl4-mt5-compare-table td.tl4-na{color:var(--foreground-dim);font-family:var(--font-sans);font-size:.78rem;font-weight:400;cursor:help}
.tl4-mt5-compare-table tbody tr:hover{background:rgba(255,255,255,.03)}
@media(max-width:700px){.tl4-mt5-pending{flex-direction:column}}

/* ── Stage 4 module (2026-08-19 redesign): a visually distinct card,
   clearly separated from the lifecycle tables above, so the trader
   reads it as "now I'm looking at broker verification," not more rows
   in the same table. ── */
.tl4-mt5-module{margin-top:2.25rem;padding:1.75rem;background:var(--background-surface);border:1px solid var(--border);border-radius:var(--radius-xl)}
.tl4-mt5-tabs{display:inline-flex;gap:.2rem;margin-bottom:1.3rem;background:var(--background-elevated);border:1px solid var(--border);border-radius:var(--radius-lg);padding:.3rem}
.tl4-mt5-tab{-webkit-appearance:none;appearance:none;padding:.6rem 1.35rem;border-radius:var(--radius-md);font-size:.8rem;font-weight:700;color:var(--foreground-dim);background:transparent;border:none;cursor:pointer;font-family:inherit;transition:all var(--t-fast);display:inline-flex;align-items:center;gap:.45rem;white-space:nowrap}
.tl4-mt5-tab:hover{color:var(--foreground)}
.tl4-mt5-tab.active{background:var(--tl4-4);color:#0A1F17;box-shadow:0 2px 10px rgba(95,174,140,.25)}
.tl4-mt5-panel{margin-bottom:0}
.tl4-mt5-parity-table{min-width:1200px}
.tl4-mt5-empty,.tl4-verdict-empty{display:flex;align-items:flex-start;gap:1rem;padding:1.5rem 1.6rem;border-radius:var(--radius-lg);border:1px solid var(--border)}
.tl4-mt5-empty{background:var(--tl4-4-bg)}
.tl4-verdict-empty{background:var(--tl4-3-bg)}
.tl4-mt5-empty-icon,.tl4-verdict-empty-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:2.4rem;height:2.4rem;border-radius:50%;font-size:1.1rem}
.tl4-mt5-empty-icon{background:color-mix(in srgb,var(--tl4-4) 16%,transparent)}
.tl4-verdict-empty-icon{background:color-mix(in srgb,var(--tl4-3) 16%,transparent)}
.tl4-mt5-empty-title,.tl4-verdict-empty-title{font-size:.86rem;font-weight:700;color:var(--foreground)}
.tl4-mt5-empty-sub,.tl4-verdict-empty-sub{font-size:.78rem;color:var(--foreground-dim);margin-top:.35rem;line-height:1.6;max-width:44rem}
.tl4-mt5-upload-link{display:inline-flex;align-items:center;gap:.4rem;margin-top:.9rem;padding:.55rem 1.05rem;border-radius:var(--radius-md);background:var(--tl4-4);color:#0A1F17;font-size:.78rem;font-weight:700;text-decoration:none;transition:filter var(--t-fast)}
.tl4-mt5-upload-link:hover{filter:brightness(1.1)}
.tl4-verdict{margin-top:1.75rem;padding-top:1.5rem;border-top:1px solid var(--border)}
.tl4-verdict-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.6rem;align-items:start}
.tl4-verdict-list{list-style:none;margin:0;padding:0}
.tl4-verdict-list li{display:flex;align-items:baseline;gap:.5rem;padding:.5rem 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:.78rem}
.tl4-verdict-list li:last-child{border-bottom:none}
.tl4-verdict-icon{font-size:.72rem;font-weight:700;flex-shrink:0}
.tl4-verdict-ok .tl4-verdict-icon{color:var(--positive)}
.tl4-verdict-off .tl4-verdict-icon{color:var(--warning)}
.tl4-verdict-label{font-weight:600;color:var(--foreground-muted);min-width:5.5rem;flex-shrink:0}
.tl4-verdict-detail{color:var(--foreground);font-weight:500;line-height:1.4}
.tl4-verdict-chart{display:flex;flex-direction:column;gap:.8rem;justify-content:center}
.tl4-verdict-bar-row{display:grid;grid-template-columns:6.5rem 1fr auto;gap:.6rem;align-items:center;font-size:.74rem}
.tl4-verdict-bar-label{color:var(--foreground-dim)}
.tl4-verdict-bar-track{height:.5rem;background:var(--background-elevated);border-radius:var(--radius-full);overflow:hidden}
.tl4-verdict-bar-fill{height:100%;border-radius:var(--radius-full)}
.tl4-bar-pos{background:var(--positive)}
.tl4-bar-neg{background:var(--negative)}
.tl4-verdict-bar-value{font-family:var(--font-mono);font-weight:700}
.tl4-verdict-summary{margin-top:1rem;padding-top:.9rem;border-top:1px solid var(--border);font-size:.76rem;color:var(--foreground-dim)}
@media(max-width:900px){.tl4-verdict-grid{grid-template-columns:1fr}}

.tl4-commentary{margin-top:1.4rem;padding-top:1.2rem;border-top:1px solid var(--border)}
.tl4-commentary-title{font-size:.78rem;font-weight:800;color:var(--foreground);margin-bottom:.6rem}
.tl4-commentary-list{list-style:none;margin:0;padding:0;font-size:.8rem;color:var(--foreground-muted)}
.tl4-commentary-list li{position:relative;padding:.4rem 0 .4rem 1.1rem;line-height:1.55;border-bottom:1px solid rgba(255,255,255,.04)}
.tl4-commentary-list li:last-child{border-bottom:none}
.tl4-commentary-list li::before{content:"-";position:absolute;left:0;color:var(--tl4-2)}
@media(max-width:700px){.tl4-strategy-chip{order:2}}

/* ── Milestone dividers between cascading blocks (2026-08-19 redesign):
   a centered label on a quiet rule, replacing the old full-width flat
   color banner — reads as a section boundary, not another alert box. ── */
.tl4-table+.tl4-table-scroll,.tl4-table-scroll+.tl4-table-scroll{margin-top:0}
.tl4-milestone{display:flex;align-items:center;gap:1rem;margin:1.75rem 0 .5rem}
.tl4-milestone-line{flex:1;height:1px;background:var(--border)}
.tl4-milestone-chip{display:inline-flex;align-items:center;gap:.5rem;padding:.4rem .95rem;border-radius:var(--radius-full);font-size:.68rem;font-weight:800;letter-spacing:.04em;white-space:nowrap;border:1px solid var(--border)}
.tl4-milestone-icon{font-size:.72rem}
.tl4-milestone-blue{color:var(--tl4-1);background:var(--tl4-1-bg);border-color:color-mix(in srgb,var(--tl4-1) 30%,var(--border))}
.tl4-milestone-purple{color:var(--tl4-2);background:var(--tl4-2-bg);border-color:color-mix(in srgb,var(--tl4-2) 30%,var(--border))}
.tl4-milestone-gold{color:var(--tl4-3);background:var(--tl4-3-bg);border-color:color-mix(in srgb,var(--tl4-3) 30%,var(--border))}
.tl4-milestone-green{color:var(--tl4-4);background:var(--tl4-4-bg);border-color:color-mix(in srgb,var(--tl4-4) 30%,var(--border))}
.tl4-milestone-pending{color:var(--foreground-dim);background:var(--background-surface);border-color:var(--border)}
.tl4-milestone-sub{font-size:.76rem;color:var(--foreground-dim);text-align:center;margin:0 0 1.5rem;line-height:1.5}

.tl4-expand-btn{background:transparent;border:1px dashed var(--border);color:var(--foreground-dim);font-size:.66rem;font-weight:700;padding:.05rem .4rem;border-radius:var(--radius-sm);cursor:pointer;font-family:var(--font-sans);margin-left:.3rem;white-space:nowrap}
.tl4-expand-btn:hover{color:var(--foreground);border-color:var(--foreground-dim)}
.tl4-expand-rest{color:var(--foreground)}
.tl4-tp-expand{display:block;margin-top:.3rem}
.tl4-tp-expand-row{display:block;font-size:.74rem;color:var(--foreground-dim);padding:.1rem 0}
.tl4-calc-row{display:block;font-size:.72rem;color:var(--foreground-dim);padding:.15rem 0}
.tl4-calc-label{color:var(--foreground-muted);margin-right:.3rem}

.tl4-tone-dot{display:inline-block;width:.5rem;height:.5rem;border-radius:50%;margin-right:.4rem;vertical-align:middle}
.tl4-tone-positive{background:var(--positive)} .tl4-tone-neutral{background:var(--foreground-dim)}
.tl4-tone-caution{background:var(--warning)} .tl4-tone-negative{background:var(--negative)}

.tl4-footer-grid{display:grid;grid-template-columns:1.6fr 1fr;gap:1.4rem;margin-top:1.4rem;padding-top:1.2rem;border-top:1px solid var(--border)}
.tl4-commentary{padding-top:0}
.tl4-commentary-title{font-size:.78rem;font-weight:800;color:var(--foreground);margin-bottom:.6rem}
.tl4-compliance-empty{font-size:.78rem;color:var(--foreground-dim)}
.tl4-compliance-body{display:flex;align-items:center;gap:1rem;flex-wrap:wrap}
.tl4-ring-svg{width:120px;height:120px;flex-shrink:0}
.tl4-ring-value{font-size:8px;font-weight:800;fill:var(--foreground);font-family:var(--font-sans)}
.tl4-ring-label{font-size:2.6px;font-weight:700;fill:var(--foreground-dim);letter-spacing:.04em;text-transform:uppercase;font-family:var(--font-sans)}
.tl4-compliance-legend{list-style:none;margin:0;padding:0;flex:1;min-width:10rem;font-size:.72rem;color:var(--foreground-muted)}
.tl4-compliance-legend li{display:flex;align-items:center;gap:.4rem;padding:.25rem 0}
.tl4-compliance-legend li strong{margin-left:auto;color:var(--foreground)}
.tl4-legend-dot{display:inline-block;width:.55rem;height:.55rem;border-radius:2px;flex-shrink:0}
.tl4-legend-declared_edge{background:#60A5FA} .tl4-legend-calculator_adherence{background:#A78BFA}
.tl4-legend-manual_execution{background:#E09B3A} .tl4-legend-broker_verification{background:#26A69A}
.tl4-compliance-overall{margin-top:.8rem;padding-top:.7rem;border-top:1px solid var(--border);font-size:.78rem;color:var(--foreground-dim)}
.tl4-compliance-overall strong{color:var(--foreground);font-size:.95rem;margin-left:.3rem}
@media(max-width:900px){.tl4-footer-grid{grid-template-columns:1fr}}
</style>
</head>
<body class="dash-layout">
<?php require __DIR__ . '/sidebar.php'; ?>
<main class="main">
<h1 class="page-title">Journal</h1>

<?php
// Journal redesign (2026-07-31) — exactly two workspaces. A single-trade
// drill-in (?trade_id=X) takes over the content area regardless of which
// workspace tab is active, since a trade can be reached from either one
// (Trade Journal rows, or — once built — Trading Archive's evidence
// links); the tab row itself stays visible so it's always obvious how to
// get back.
?>
<div class="zone-tabs" style="margin-bottom:1.5rem;gap:.5rem">
    <?php
    // Item 10 (2026-08-06) — a breached/closed account's trade history
    // reuses Active Journeys' own table component under the hood, but it
    // must never LOOK like the trader is still "inside" that live-account
    // workspace (this account isn't active — it's finished). The
    // "Active Journeys" tab is relabeled, not hidden, so the tab row
    // itself still reads as real navigation, not a break in the app's
    // usual chrome.
    $inBreachView = $workspace === 'active' && $breachAccountId !== null;
    ?>
    <a href="<?= $inBreachView ? htmlspecialchars(archiveUrl(['archive_lens' => 'failure'])) : activeJourneysUrl() ?>" class="zone-tab <?= $workspace === 'active' ? 'active' : '' ?>"><?= $inBreachView ? 'Account Journey Before Breach' : 'Active Journeys' ?></a>
    <!-- 2026-08-18 correction: Trading Archive is real, shared Journal
         functionality — always shown, in both workflows (previously
         hidden while validating; that was the wrong fix for TradingArchiveEngine's
         real workflow-scoping gap, now closed at the data layer instead). -->
    <a href="<?= archiveUrl() ?>" class="zone-tab <?= $workspace === 'archive' ? 'active' : '' ?>">Trading Archive</a>
</div>

<?php if ($jtTradeId): ?>
    <?php if (!$jtTrade): ?>
    <div class="empty-state"><h3>Trade not found</h3><p style="font-size:.85rem">This trade doesn't exist or doesn't belong to your account. <a href="<?= activeJourneysUrl() ?>">Back to Active Journeys</a></p></div>
    <?php else: ?>
    <?php renderJournalTradesSingleView($jtTrade, $db); ?>
    <?php endif; ?>
<?php elseif ($workspace === 'active' && $breachAccountId !== null): ?>
    <?php renderBreachTradesView($userId, $db, $breachAccountId); ?>
<?php elseif ($workspace === 'active'): ?>
    <?php renderRightNow($userId, $db, $pdo); ?>
<?php else: ?>
    <?php renderTradingArchive($userId, $db); ?>
<?php endif; ?>
</main>
<script>
/**
 * Shared "Show More" / "Show Less" toggle for Right Now's trade tables —
 * one function, reused identically by every stage's table (Phase 1,
 * Phase 2, Phase 3, Funded). Rows beyond the initial 7 are rendered
 * server-side with class="rn-row-extra" and display:none; this only
 * flips their visibility and the button's own label/state.
 */
function rnToggleTableRows(btn) {
    var table = document.getElementById(btn.getAttribute('data-table'));
    if (!table) return;
    var expanded = btn.getAttribute('data-expanded') === '1';
    var rows = table.querySelectorAll('.rn-row-extra');
    rows.forEach(function (row) { row.style.display = expanded ? 'none' : 'table-row'; });
    btn.setAttribute('data-expanded', expanded ? '0' : '1');
    btn.textContent = expanded ? btn.getAttribute('data-more-label') : 'Show Less';
}

/**
 * Item 6 — every coaching reference to a specific trade classification
 * count (Disciplined Wins, Tilt Warnings, Revenge Trades, Oversized
 * Losses...) is now a real clickable filter, not just a number the trader
 * has to trust. tableId scopes to one specific rendered table (a single
 * phase/segment's own — used by Goal section chips, which cite counts
 * scoped to exactly that one table); an empty tableId filters every
 * .rn-table on the page at once (used by the Full Coaching Analysis
 * workspace, whose counts span every phase/segment table on the account,
 * not just one).
 */
function rnFilterTrades(classification, label, tableId) {
    // Close any open modal first — a chip clicked from inside Full Coaching
    // Analysis needs the trader to actually see the table it filtered.
    document.querySelectorAll('.rn-modal-backdrop').forEach(function (el) { el.style.display = 'none'; });
    document.body.style.overflow = '';

    var tables = tableId ? [document.getElementById(tableId)].filter(Boolean) : Array.prototype.slice.call(document.querySelectorAll('.rn-table'));
    var matchCount = 0, firstMatch = null;
    tables.forEach(function (table) {
        table.querySelectorAll('tr[data-classification]').forEach(function (row) {
            var isMatch = row.getAttribute('data-classification') === classification;
            row.style.display = isMatch ? 'table-row' : 'none';
            if (isMatch) { matchCount++; if (!firstMatch) firstMatch = row; }
        });
    });

    var banner = document.getElementById('rn-filter-banner');
    if (banner) {
        document.getElementById('rn-filter-banner-text').textContent =
            'Showing only: ' + label + (tableId ? '' : ' (across every account section)') + ', ' + matchCount + ' trade' + (matchCount === 1 ? '' : 's');
        banner.style.display = 'flex';
    }
    if (firstMatch) firstMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

/** Item 6 — restores every table to its normal (unfiltered, collapsed-to-7-rows) state and hides the filter banner. */
function rnClearTradeFilter() {
    document.querySelectorAll('.rn-table tr[data-classification]').forEach(function (row) {
        row.style.display = row.classList.contains('rn-row-extra') ? 'none' : 'table-row';
    });
    document.querySelectorAll('.rn-show-more-btn').forEach(function (btn) {
        btn.setAttribute('data-expanded', '0');
        btn.textContent = btn.getAttribute('data-more-label');
    });
    var banner = document.getElementById('rn-filter-banner');
    if (banner) banner.style.display = 'none';
}

<?php
// Dashboard's clickable behavior-pattern alerts (dashBuildBehaviorAlerts(),
// public/dashboard.php, 2026-08-18) land here with ?auto_filter=<classification>
// once the right account is already selected via rn_account/rn_type — this
// triggers the exact same rnFilterTrades() a trader would get from clicking
// a coaching chip on this same page, so the linked trades are actually
// visible on arrival instead of buried in a collapsed 7-row table. Allow-
// listed to the two classification values that can ever legitimately be
// linked to (see dashBuildBehaviorAlerts()'s own doc comment on why
// Impulsive/FOMO never send this param) — never trusts the raw GET value
// directly into a filter call.
$rnAutoFilterClass = $_GET['auto_filter'] ?? '';
if (in_array($rnAutoFilterClass, ['revenge_trade', 'tilt_warning'], true)):
    $rnAutoFilterLabel = trim((string)($_GET['auto_filter_label'] ?? ''));
    if ($rnAutoFilterLabel === '') $rnAutoFilterLabel = $rnAutoFilterClass === 'revenge_trade' ? 'Revenge Trades' : 'Fear of Trading';
?>
document.addEventListener('DOMContentLoaded', function () {
    rnFilterTrades('<?= addslashes($rnAutoFilterClass) ?>', '<?= addslashes($rnAutoFilterLabel) ?>', '');
});
<?php endif; ?>

/**
 * Trading Archive's own filter driver — every "View Trades" button on the
 * page (findings, patterns, breach causes) calls this, filtering the
 * SAME single #ta-evidence-table by whichever data-{attr} attribute the
 * finding names (classification / account / phase / checklist-skipped /
 * post-loss-spike / post-loss-pause). Deliberately a separate function
 * from Active Journeys' rnFilterTrades() — that one only ever matches
 * data-classification and operates across every .rn-table on the page,
 * neither of which fits Trading Archive's single multi-account table —
 * but it reuses the identical visual/interaction pattern (banner, scroll-
 * to-first-match, Clear Filter).
 */
function taFilterTrades(attr, value, label) {
    var table = document.getElementById('ta-evidence-table');
    if (!table) return;
    var matchCount = 0, firstMatch = null;
    table.querySelectorAll('tr[data-' + attr + ']').forEach(function (row) {
        var isMatch = row.getAttribute('data-' + attr) === value;
        row.style.display = isMatch ? 'table-row' : 'none';
        if (isMatch) { matchCount++; if (!firstMatch) firstMatch = row; }
    });
    var banner = document.getElementById('ta-filter-banner');
    if (banner) {
        document.getElementById('ta-filter-banner-text').textContent = 'Showing only: ' + label + ', ' + matchCount + ' trade' + (matchCount === 1 ? '' : 's');
        banner.style.display = 'flex';
    }
    if (firstMatch) firstMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

/** Restores the Trading Archive evidence table to its normal (unfiltered, collapsed-to-8-rows) state and hides the filter banner. */
function taClearFilter() {
    var table = document.getElementById('ta-evidence-table');
    if (!table) return;
    table.querySelectorAll('tr[data-classification]').forEach(function (row) {
        row.style.display = row.classList.contains('rn-row-extra') ? 'none' : 'table-row';
    });
    var btn = document.querySelector('.rn-show-more-btn[data-table="ta-evidence-table"]');
    if (btn) { btn.setAttribute('data-expanded', '0'); btn.textContent = btn.getAttribute('data-more-label'); }
    var banner = document.getElementById('ta-filter-banner');
    if (banner) banner.style.display = 'none';
}

/* ── Item 3: account selector (identical behavior for Prop/Live) ───── */
function rnToggleAccountPicker(btn) {
    var panel = btn.nextElementSibling;
    if (!panel) return;
    var isOpen = panel.style.display !== 'none';
    document.querySelectorAll('.rn-account-picker-panel').forEach(function (p) { p.style.display = 'none'; });
    if (isOpen) return;
    panel.style.display = 'block';
    // Kept in sync with the identical fix in assets/js/account-picker.js
    // — see that file's own comment for the full root cause. Inline
    // panels (position:static) are never positioned here.
    if (!panel.classList.contains('rn-account-picker-panel-inline')) {
        var rect = btn.getBoundingClientRect();
        var panelRect = panel.getBoundingClientRect();
        var margin = 8;
        var left = rect.right - panelRect.width;
        if (left < margin) left = margin;
        if (left + panelRect.width > window.innerWidth - margin) left = window.innerWidth - margin - panelRect.width;
        var top = rect.bottom + 8;
        if (top + panelRect.height > window.innerHeight - margin) top = rect.top - panelRect.height - 8;
        if (top < margin) top = margin;
        panel.style.left = left + 'px';
        panel.style.top = top + 'px';
    }
    var search = panel.querySelector('.rn-account-search');
    if (search) { search.value = ''; search.focus({ preventScroll: true }); }
}
document.addEventListener('click', function (e) {
    if (!e.target.closest('.rn-account-picker')) {
        document.querySelectorAll('.rn-account-picker-panel:not(.rn-account-picker-panel-inline)').forEach(function (p) { p.style.display = 'none'; });
    }
});
window.addEventListener('scroll', function (e) {
    document.querySelectorAll('.rn-account-picker-panel:not(.rn-account-picker-panel-inline)').forEach(function (p) {
        if (p.style.display !== 'none' && !(e.target && p.contains(e.target))) p.style.display = 'none';
    });
}, true);
function taToggleImpactInfo(btn) {
    var pop = btn.parentElement.querySelector('.ta-impact-popover');
    if (!pop) return;
    var isOpen = pop.style.display !== 'none';
    document.querySelectorAll('.ta-impact-popover').forEach(function (p) { p.style.display = 'none'; });
    if (isOpen) return;
    // position:fixed has no "anchor to my parent" concept — compute
    // top/left from the trigger button's real on-screen position instead,
    // flipping above/left of the button whenever the default below/right
    // placement would run off the viewport (e.g. this column sits near
    // the right edge, or the table row is near the bottom of the page).
    var rect = btn.getBoundingClientRect();
    pop.style.display = 'block';
    var popRect = pop.getBoundingClientRect();
    var margin = 8;
    var left = rect.right - popRect.width;
    if (left < margin) left = margin;
    if (left + popRect.width > window.innerWidth - margin) left = window.innerWidth - margin - popRect.width;
    var top = rect.bottom + 8;
    if (top + popRect.height > window.innerHeight - margin) top = rect.top - popRect.height - 8;
    if (top < margin) top = margin;
    pop.style.left = left + 'px';
    pop.style.top = top + 'px';
}
// position:fixed popovers don't track their trigger as the page scrolls
// — closing on scroll (rather than repositioning on every scroll event)
// is the simpler, still-correct behavior for a short-lived info popover
// like this one. capture:true is required to observe scroll at all
// (scroll doesn't bubble), which also means this fires for any nested
// overflow:auto container's own scrolling (e.g. the trades table's own
// horizontal scrollbar) — ignoring scrolls whose target is inside the
// popover itself keeps this from closing a popover that's simply sitting
// near a scrollable region, matching the identical fix applied to the
// account picker's own scroll-close listener for the same real bug.
window.addEventListener('scroll', function (e) {
    document.querySelectorAll('.ta-impact-popover').forEach(function (p) {
        if (p.style.display !== 'none' && !(e.target && p.contains(e.target))) p.style.display = 'none';
    });
}, true);
document.addEventListener('click', function (e) {
    if (!e.target.closest('.ta-impact-info')) {
        document.querySelectorAll('.ta-impact-popover').forEach(function (p) { p.style.display = 'none'; });
    }
});
function rnFilterAccounts(input) {
    var panel = input.closest('.rn-account-picker-panel');
    var moreBtn = panel.querySelector('.rn-show-more-accts');
    var q = input.value.trim().toLowerCase();
    var items = panel.querySelectorAll('.rn-account-item');
    if (q === '') {
        var expanded = moreBtn ? moreBtn.getAttribute('data-expanded') === '1' : true;
        items.forEach(function (item) {
            var isExtra = item.classList.contains('rn-acct-extra');
            item.style.display = (isExtra && !expanded) ? 'none' : '';
        });
        if (moreBtn) moreBtn.style.display = '';
    } else {
        items.forEach(function (item) {
            var match = (item.getAttribute('data-search') || '').indexOf(q) !== -1;
            item.style.display = match ? '' : 'none';
        });
        if (moreBtn) moreBtn.style.display = 'none';
    }
}
function rnToggleAccountsMore(btn) {
    var panel = btn.closest('.rn-account-picker-panel');
    var expanded = btn.getAttribute('data-expanded') === '1';
    panel.querySelectorAll('.rn-acct-extra').forEach(function (item) { item.style.display = expanded ? 'none' : ''; });
    btn.setAttribute('data-expanded', expanded ? '0' : '1');
    btn.textContent = expanded ? btn.getAttribute('data-more-label') : 'Show Less';
}

/* ── Items 5 & 6: expandable in-page workspaces ─────────────────────
 * "View Full Coaching Analysis" / "View Account Summary" never navigate
 * away from Right Now (item 5/6) — they reveal a server-rendered panel
 * that's already in the page, hidden by default. */
/**
 * Item 6 — Account Summary and Full Coaching Analysis are both real modal
 * overlays now (position:fixed backdrop + centered panel), not inline
 * content appended at the bottom of the page — opening one never scrolls
 * the trader away from where they were. Toggles the panel and its
 * matching "<id>-backdrop" element together; falls back to the old
 * inline-toggle behavior if no backdrop exists for a given id.
 */
function rnToggleWorkspace(id) {
    var el = document.getElementById(id);
    if (!el) return;
    var bd = document.getElementById(id + '-backdrop');
    var isOpen = el.style.display !== 'none';
    el.style.display = isOpen ? 'none' : 'block';
    if (bd) {
        bd.style.display = isOpen ? 'none' : 'flex';
        document.body.style.overflow = isOpen ? '' : 'hidden';
    } else if (!isOpen) {
        el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
}

/* ── Item 12: TP2-10 click-to-expand (click again to collapse) ─────── */
function rnToggleTpExpand(badge) {
    var expandEl = badge.nextElementSibling;
    if (!expandEl) return;
    var expanded = badge.getAttribute('data-expanded') === '1';
    expandEl.style.display = expanded ? 'none' : 'inline-flex';
    badge.setAttribute('data-expanded', expanded ? '0' : '1');
}

/* 2026-08-19 Stage 4 rebuild — plain show/hide between the "Actual
   Outcome Tracking" and "MT5 Trades" panels, scoped by panelId so
   multiple Stage 4 sections on one page (there's only ever one per
   single-trade view today, but this doesn't assume that) never fight
   over each other's tab state. */
function tl4SwitchS4Tab(btn, panelId) {
    var tabs = btn.parentElement.querySelectorAll('.tl4-mt5-tab');
    for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
    btn.classList.add('active');
    var target = btn.getAttribute('data-target');
    var outcomeEl = document.getElementById(panelId + '-outcome');
    var mt5El = document.getElementById(panelId + '-mt5');
    if (outcomeEl) outcomeEl.style.display = (target === panelId + '-outcome') ? '' : 'none';
    if (mt5El) mt5El.style.display = (target === panelId + '-mt5') ? '' : 'none';
}

/* 2026-08-03 cascading-lifecycle redesign — Stage 1's Instrument(s)/
   Session(s)/Timeframe(s) "+N more" expand (tl4ExpandableList()). */
function tl4ToggleExpand(btn) {
    var restEl = btn.nextElementSibling;
    if (!restEl) return;
    var expanded = btn.getAttribute('data-expanded') === '1';
    if (!expanded && !btn.getAttribute('data-more-label')) {
        btn.setAttribute('data-more-label', btn.textContent);
    }
    restEl.style.display = expanded ? 'none' : 'inline';
    btn.setAttribute('data-expanded', expanded ? '0' : '1');
    btn.textContent = expanded ? btn.getAttribute('data-more-label') : 'Show less';
}

/* Generic block-level detail expand — used by Block 2's TP3+ vertical
   expand (TP3 shown, TP4-TP10 collapsed). Position Size (Lots)'s own
   "View Calculation" disclosure was removed 2026-08-19 (exposed internal
   calculation detail, per explicit instruction) — this function is now
   only reached from TP3+, kept generic in case a future block-level
   expand needs it again. Same interaction pattern as tl4ToggleExpand()
   above, kept separate since it toggles a block-level <span>, not inline
   text. (Renamed from tl4ToggleTp() 2026-08-03 since it's now shared by
   more than just TP rows.) */
function tl4ToggleDetail(btn) {
    var expandEl = btn.nextElementSibling;
    if (!expandEl) return;
    var expanded = btn.getAttribute('data-expanded') === '1';
    expandEl.style.display = expanded ? 'none' : 'block';
    btn.setAttribute('data-expanded', expanded ? '0' : '1');
}

/* ── Item 11: editable trade notes ──────────────────────────────────
 * Each .rn-notes-cell holds three mutually-exclusive states: an "Add
 * Note" button (no note yet), a "text + edit pencil" display (a note
 * exists), and a textarea+Save/Cancel editor. Persists via
 * POST /api/journal/update-note — trades.notes (migration_add_trade_notes.sql). */
function rnEditNote(btn) {
    var cell = btn.closest('.rn-notes-cell');
    cell.querySelectorAll('.rn-notes-display').forEach(function (el) { el.style.display = 'none'; });
    var editEl = cell.querySelector('.rn-notes-edit');
    editEl.style.display = 'inline-flex';
    var ta = editEl.querySelector('.rn-notes-input');
    ta.focus();
}
/* Item 8: mirrors rnTruncatedNoteHtml() (PHP, public/journal.php) — used to
   rebuild the notes cell's truncated Show More/Show Less markup client-side
   after a save/cancel, since the initial server-rendered HTML only exists
   at page load. Keep the two in sync if the truncation format ever changes. */
function rnEscapeHtmlText(s) {
    var d = document.createElement('div');
    d.textContent = s;
    return d.innerHTML;
}
function rnBuildNoteHtml(text, limit) {
    limit = limit || 90;
    text = text.trim();
    if (text.length <= limit) return rnEscapeHtmlText(text).replace(/\n/g, '<br>');
    var shortText = text.slice(0, limit) + '…';
    return '<span class="rn-note-wrap" data-expanded="0">'
        + '<span class="rn-note-short">' + rnEscapeHtmlText(shortText).replace(/\n/g, '<br>') + '</span>'
        + '<span class="rn-note-full" style="display:none">' + rnEscapeHtmlText(text).replace(/\n/g, '<br>') + '</span>'
        + ' <button type="button" class="tl4-expand-btn rn-note-more" onclick="rnToggleNoteText(this)">Show More</button>'
        + '<button type="button" class="tl4-expand-btn rn-note-less" style="display:none" onclick="rnToggleNoteText(this)">Show Less</button>'
        + '</span>';
}
/* Shared by both the Trades table's notes cell and the single-trade
   4-Stage Lifecycle's Trader Notes row (tl4NotesCellHtml() markup uses the
   same .rn-note-wrap/.rn-note-short/.rn-note-full/.rn-note-more/
   .rn-note-less structure). */
function rnToggleNoteText(btn) {
    var wrap = btn.closest('.rn-note-wrap');
    if (!wrap) return;
    var expanded = wrap.getAttribute('data-expanded') === '1';
    wrap.querySelector('.rn-note-short').style.display = expanded ? 'inline' : 'none';
    wrap.querySelector('.rn-note-full').style.display = expanded ? 'none' : 'inline';
    wrap.querySelector('.rn-note-more').style.display = expanded ? 'inline-block' : 'none';
    wrap.querySelector('.rn-note-less').style.display = expanded ? 'none' : 'inline-block';
    wrap.setAttribute('data-expanded', expanded ? '0' : '1');
}
function rnShowNoteDisplay(cell, text) {
    var displays = cell.querySelectorAll('.rn-notes-display');
    var hasText = text && text.trim() !== '';
    displays[0].style.display = hasText ? 'none' : 'inline-flex';
    displays[1].style.display = hasText ? 'inline-flex' : 'none';
    if (hasText) {
        var inner = displays[1].querySelector('.rn-notes-text-inner');
        if (inner) { inner.innerHTML = rnBuildNoteHtml(text, 90); inner.title = text.trim(); }
    }
}
function rnCancelNote(btn) {
    var cell = btn.closest('.rn-notes-cell');
    var ta = cell.querySelector('.rn-notes-input');
    ta.value = ta.getAttribute('data-original') || '';
    cell.querySelector('.rn-notes-edit').style.display = 'none';
    rnShowNoteDisplay(cell, ta.value);
}
function rnSaveNote(btn) {
    var cell = btn.closest('.rn-notes-cell');
    var tradeId = cell.getAttribute('data-trade-id');
    var ta = cell.querySelector('.rn-notes-input');
    var notes = ta.value;
    btn.disabled = true;
    var originalLabel = btn.textContent;
    btn.textContent = 'Saving…';
    fetch('/traderalign/api/journal/update-note', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ trade_id: tradeId, notes: notes })
    }).then(function (r) { return r.json(); }).then(function (data) {
        btn.disabled = false;
        btn.textContent = originalLabel;
        if (data && data.success) {
            ta.setAttribute('data-original', notes);
            cell.querySelector('.rn-notes-edit').style.display = 'none';
            rnShowNoteDisplay(cell, notes);
        } else {
            alert('Could not save note. Please try again.');
        }
    }).catch(function () {
        btn.disabled = false;
        btn.textContent = originalLabel;
        alert('Could not save note. Please try again.');
    });
}

/* ── Item 1: Current Goal modal (shared by every "Set Goal"/"Set New Goal" trigger on the page) ─────── */
function rnOpenGoalModal(accountId, currentBalance) {
    document.getElementById('rn-goal-modal-error').style.display = 'none';
    document.getElementById('rn-goal-modal-current').textContent = '$' + Number(currentBalance).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
    document.getElementById('rn-goal-target').value = '';
    document.getElementById('rn-goal-reason').value = '';
    var bd = document.getElementById('rn-goal-modal-backdrop');
    bd.setAttribute('data-account-id', accountId);
    bd.style.display = 'flex';
    document.body.style.overflow = 'hidden';
}
function rnCloseGoalModal() {
    document.getElementById('rn-goal-modal-backdrop').style.display = 'none';
    document.body.style.overflow = '';
}
function rnSaveGoal() {
    var bd = document.getElementById('rn-goal-modal-backdrop');
    var accountId = bd.getAttribute('data-account-id');
    var target = parseFloat(document.getElementById('rn-goal-target').value);
    var reason = document.getElementById('rn-goal-reason').value;
    var errEl = document.getElementById('rn-goal-modal-error');
    errEl.style.display = 'none';
    if (!target || target <= 0) {
        errEl.textContent = 'Enter a target balance greater than 0.';
        errEl.style.display = 'block';
        return;
    }
    var btn = document.getElementById('rn-goal-save-btn');
    btn.disabled = true;
    btn.textContent = 'Saving…';
    fetch('/traderalign/api/journal/save-goal', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ account_id: accountId, target_balance: target, reason: reason })
    }).then(function (r) { return r.json(); }).then(function (data) {
        if (data && data.success) {
            window.location.reload();
        } else {
            btn.disabled = false;
            btn.textContent = 'Save Goal';
            errEl.textContent = (data && data.error) ? data.error : 'Could not save goal. Please try again.';
            errEl.style.display = 'block';
        }
    }).catch(function () {
        btn.disabled = false;
        btn.textContent = 'Save Goal';
        errEl.textContent = 'Could not save goal. Please try again.';
        errEl.style.display = 'block';
    });
}

/* ── Risk Panel (2026-08-17): Loss Streak card's "I've Completed My Review" action — the one manual step Card 2's FULL STOP state needs before trading can resume. Kept deliberately minimal (confirm + one fetch + reload), matching every other simple action button on this page. ─────── */
function rpClearStreakReview(accountId, btn) {
    var notes = window.prompt('Optional: what did you check? (leave blank to skip)') || '';
    if (!window.confirm('Confirm you have completed a manual review: no data or platform issue, your own trading rules were followed, and you are calm and clear-headed.')) return;
    btn.disabled = true;
    btn.textContent = 'Saving…';
    fetch('/traderalign/api/journal/clear-streak-review', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ account_id: accountId, notes: notes })
    }).then(function (r) { return r.json(); }).then(function (data) {
        if (data && data.success) {
            window.location.reload();
        } else {
            btn.disabled = false;
            btn.textContent = "I've Completed My Review";
            alert((data && data.error) ? data.error : 'Could not save review. Please try again.');
        }
    }).catch(function () {
        btn.disabled = false;
        btn.textContent = "I've Completed My Review";
        alert('Could not save review. Please try again.');
    });
}

/* ── Item 8: Deposit/Withdraw modal (Live Accounts), posts to the app's own existing /api/accounts/transaction endpoint ─────── */
function rnOpenTxModal(accountId, type) {
    document.getElementById('rn-tx-modal-error').style.display = 'none';
    document.getElementById('rn-tx-amount').value = '';
    document.getElementById('rn-tx-modal-title').textContent = type === 'deposit' ? 'Record Deposit' : 'Record Withdrawal';
    var bd = document.getElementById('rn-tx-modal-backdrop');
    bd.setAttribute('data-account-id', accountId);
    bd.setAttribute('data-type', type);
    bd.style.display = 'flex';
    document.body.style.overflow = 'hidden';
}
function rnCloseTxModal() {
    document.getElementById('rn-tx-modal-backdrop').style.display = 'none';
    document.body.style.overflow = '';
}
function rnSaveTx() {
    var bd = document.getElementById('rn-tx-modal-backdrop');
    var accountId = bd.getAttribute('data-account-id');
    var type = bd.getAttribute('data-type');
    var amount = parseFloat(document.getElementById('rn-tx-amount').value);
    var errEl = document.getElementById('rn-tx-modal-error');
    errEl.style.display = 'none';
    if (!amount || amount <= 0) {
        errEl.textContent = 'Enter an amount greater than 0.';
        errEl.style.display = 'block';
        return;
    }
    var btn = document.getElementById('rn-tx-save-btn');
    btn.disabled = true;
    btn.textContent = 'Saving…';
    fetch('/traderalign/api/accounts/transaction', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ account_id: accountId, type: type, amount: amount })
    }).then(function (r) { return r.json(); }).then(function (data) {
        if (data && data.success) {
            window.location.reload();
        } else {
            btn.disabled = false;
            btn.textContent = 'Save';
            errEl.textContent = (data && data.error) ? data.error : 'Could not save. Please try again.';
            errEl.style.display = 'block';
        }
    }).catch(function () {
        btn.disabled = false;
        btn.textContent = 'Save';
        errEl.textContent = 'Could not save. Please try again.';
        errEl.style.display = 'block';
    });
}

/* ── Items 9/10: Edit Account Details modal (Platform/Server/Login/Password/Account Number/Tags/Notes) ─────── */
function rnOpenMetaModal() {
    document.getElementById('rn-meta-modal-error').style.display = 'none';
    var pwField = document.getElementById('rn-meta-password');
    if (pwField) { pwField.value = ''; pwField.disabled = false; }
    var clearBox = document.getElementById('rn-meta-password-clear');
    if (clearBox) clearBox.checked = false;
    document.getElementById('rn-meta-modal-backdrop').style.display = 'flex';
    document.body.style.overflow = 'hidden';
}
function rnCloseMetaModal() {
    document.getElementById('rn-meta-modal-backdrop').style.display = 'none';
    document.body.style.overflow = '';
}
function rnSaveMeta(accountId) {
    var errEl = document.getElementById('rn-meta-modal-error');
    errEl.style.display = 'none';
    var payload = {
        account_id: accountId,
        platform: document.getElementById('rn-meta-platform').value,
        broker_server: document.getElementById('rn-meta-server').value,
        broker_login: document.getElementById('rn-meta-login').value,
        account_number: document.getElementById('rn-meta-account-number').value,
        tags: document.getElementById('rn-meta-tags').value,
        account_notes: document.getElementById('rn-meta-notes').value
    };
    // Password is only ever included in the payload when the trader
    // actually typed a new one or explicitly checked "Clear stored
    // password" — otherwise the key is omitted entirely so the endpoint's
    // own array_key_exists() check leaves the existing stored value
    // completely untouched (never silently overwritten with a blank).
    var clearBox = document.getElementById('rn-meta-password-clear');
    var pwField = document.getElementById('rn-meta-password');
    if ((clearBox && clearBox.checked) || (pwField && pwField.value !== '')) {
        payload.broker_password = (clearBox && clearBox.checked) ? '' : pwField.value;
    }
    var btn = document.getElementById('rn-meta-save-btn');
    btn.disabled = true;
    btn.textContent = 'Saving…';
    fetch('/traderalign/api/journal/update-account-meta', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
    }).then(function (r) { return r.json(); }).then(function (data) {
        if (data && data.success) {
            window.location.reload();
        } else {
            btn.disabled = false;
            btn.textContent = 'Save';
            errEl.textContent = (data && data.error) ? data.error : 'Could not save. Please try again.';
            errEl.style.display = 'block';
        }
    }).catch(function () {
        btn.disabled = false;
        btn.textContent = 'Save';
        errEl.textContent = 'Could not save. Please try again.';
        errEl.style.display = 'block';
    });
}
/** Item 9 — explicit, user-initiated reveal only; never fetched or shown by default. Toggles back to masked on a second click without a second request. */
function rnRevealAccountSecret(accountId, btn) {
    var display = document.getElementById('rn-meta-password-display');
    if (btn.getAttribute('data-revealed') === '1') {
        display.textContent = '••••••••';
        btn.setAttribute('data-revealed', '0');
        return;
    }
    btn.disabled = true;
    fetch('/traderalign/api/journal/reveal-account-secret', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ account_id: accountId })
    }).then(function (r) { return r.json(); }).then(function (data) {
        btn.disabled = false;
        if (data && data.success && data.password) {
            display.textContent = data.password;
            btn.setAttribute('data-revealed', '1');
        }
    }).catch(function () { btn.disabled = false; });
}
</script>
</body>
</html>
