// running/src/analytics.jsx — slice 11 of the module split.
//
// ANALYTICS view plus its private metric helpers (streaks, PRs,
// 12-week progression, all-time peak week, year-over-year, recovery
// correlations — Analytics is their only consumer, so they travel with
// the slice). Extracted verbatim from running/index.html. Activity
// classification comes from window.ActivityTypes, HR zones from
// window.HRZones, prediction math from window.RaceMath, load math from
// window.TrainingLoad; calcTrainingDistribution (shared with Summary)
// stays in the main block behind the __appHelpers bridge.

const { C, Card, Stat, Btn, TabBar, EmptyState, ChartTooltip, fmt } = window.SharedUI;
const useData = () => (window.__appUseData ? window.__appUseData() : null);
const calcTrainingDistribution = (...a) => window.__appHelpers.calcTrainingDistribution(...a);
const { ACT_DISPLAY, actCat, isRun, isXT, XT_TSS, effectiveType } = window.ActivityTypes;
const { getHRZones, classifyHRZone } = window.HRZones;
const { calcEffectiveVO2max, predictFromRace, vo2ConfidenceBand } = window.RaceMath;
const { useState, useEffect, useMemo } = React;
const {
  Area, Bar, ComposedChart, Line, Scatter,
  XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
  ReferenceLine,
} = Recharts;

    // ── Streak Analysis ─────────────────────────────────────────────────────
    function calcStreaks(activities) {
      if (!activities.length) return { current: 0, longest: 0, thisYear: 0, activeDays: new Set() };
      const activeDays = new Set();
      activities.forEach(a => {
        const d = a.start_date?.toDate ? a.start_date.toDate() : new Date(a.start_date);
        activeDays.add(d.toISOString().split("T")[0]);
      });

      // Current streak (consecutive days ending today or yesterday)
      const today = new Date();
      today.setHours(0,0,0,0);
      let current = 0;
      let checkDate = new Date(today);
      // Start from today, check if today has activity; if not, start from yesterday
      if (!activeDays.has(checkDate.toISOString().split("T")[0])) {
        checkDate.setDate(checkDate.getDate() - 1);
        if (!activeDays.has(checkDate.toISOString().split("T")[0])) {
          current = 0;
        }
      }
      if (activeDays.has(checkDate.toISOString().split("T")[0])) {
        while (activeDays.has(checkDate.toISOString().split("T")[0])) {
          current++;
          checkDate.setDate(checkDate.getDate() - 1);
        }
      }

      // Longest streak
      const sortedDays = [...activeDays].sort();
      let longest = 0, streak = 1;
      for (let i = 1; i < sortedDays.length; i++) {
        const prev = new Date(sortedDays[i-1]);
        const curr = new Date(sortedDays[i]);
        const diff = (curr - prev) / 86400000;
        if (diff === 1) { streak++; longest = Math.max(longest, streak); }
        else streak = 1;
      }
      longest = Math.max(longest, streak);

      // This year active days
      const yearStart = `${today.getFullYear()}-01-01`;
      const thisYear = sortedDays.filter(d => d >= yearStart).length;

      return { current, longest, thisYear, totalDays: activeDays.size };
    }

    // ── PR Tracking ─────────────────────────────────────────────────────────
    function calcPersonalRecords(activities) {
      const prs = {};
      const distances = [
        { name: "1 Mile", min: 1500, max: 1800, meters: 1609.34 },
        { name: "5K", min: 4800, max: 5300, meters: 5000 },
        { name: "10K", min: 9700, max: 10500, meters: 10000 },
        { name: "Half Marathon", min: 20800, max: 21500, meters: 21097 },
        { name: "Marathon", min: 41500, max: 43000, meters: 42195 },
      ];
      const runs = activities.filter(a => isRun(a) && a.distance > 0 && a.moving_time > 0);

      distances.forEach(d => {
        const qualifying = runs.filter(r => r.distance >= d.min && r.distance <= d.max);
        if (!qualifying.length) return;
        const best = qualifying.reduce((best, r) => {
          const pace = r.moving_time / (r.distance / d.meters);
          const bestPace = best.moving_time / (best.distance / d.meters);
          return pace < bestPace ? r : best;
        });
        const normalizedTime = best.moving_time * (d.meters / best.distance);
        prs[d.name] = {
          time: Math.round(normalizedTime),
          pace: normalizedTime / (d.meters / 1609.34),
          date: best.start_date,
          activityName: best.name,
        };
      });

      // Also track: longest run, fastest pace (any run > 1 mi)
      const longestRun = runs.reduce((best, r) => r.distance > (best?.distance || 0) ? r : best, null);
      if (longestRun) {
        prs["Longest Run"] = {
          distance: longestRun.distance,
          time: longestRun.moving_time,
          date: longestRun.start_date,
          activityName: longestRun.name,
        };
      }

      return prs;
    }

    // ── Progression Analysis (weekly mileage trend + slope) ─────────────────
    function calcProgression(activities, weeks = 12, hrCfg) {
      const TL = window.TrainingLoad;
      const hrOpts = {
        maxHR: (hrCfg && hrCfg.maxHR) || 176,
        restHR: (hrCfg && (hrCfg.restHR != null ? hrCfg.restHR : hrCfg.restingHR)) || 49,
      };
      const now = new Date();
      const weeklyData = [];
      for (let i = weeks - 1; i >= 0; i--) {
        const weekStart = new Date(now);
        weekStart.setDate(now.getDate() - (i * 7) - ((now.getDay() + 6) % 7));
        weekStart.setHours(0,0,0,0);
        const weekEnd = new Date(weekStart);
        weekEnd.setDate(weekStart.getDate() + 7);

        let miles = 0, runCount = 0, xtHours = 0, totalTrimp = 0;
        activities.forEach(a => {
          const d = a.start_date?.toDate ? a.start_date.toDate() : new Date(a.start_date);
          if (d >= weekStart && d < weekEnd) {
            if (isRun(a)) {
              miles += (a.distance || 0) / 1609.34;
              runCount++;
            } else {
              xtHours += (a.moving_time || 0) / 3600;
            }
            totalTrimp += TL.sessionTrimp(a, hrOpts);
          }
        });
        weeklyData.push({
          week: `W${weeks - i}`,
          weekLabel: weekStart.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
          miles: parseFloat(miles.toFixed(1)),
          runCount,
          xtHours: parseFloat(xtHours.toFixed(1)),
          trimp: Math.round(totalTrimp),
        });
      }

      // Linear regression on mileage to get trend
      const n = weeklyData.length;
      const xMean = (n - 1) / 2;
      const yMean = weeklyData.reduce((s, w) => s + w.miles, 0) / n;
      let num = 0, den = 0;
      weeklyData.forEach((w, i) => {
        num += (i - xMean) * (w.miles - yMean);
        den += (i - xMean) * (i - xMean);
      });
      const slope = den > 0 ? num / den : 0;
      const trend = slope > 0.5 ? "Building" : slope < -0.5 ? "Tapering" : "Maintaining";

      return { weeklyData, slope: parseFloat(slope.toFixed(2)), trend };
    }

    // ── All-Time Peak Week ──────────────────────────────────────────────────
    function calcAllTimePeakWeek(activities) {
      const runActs = activities.filter(a => isRun(a) && a.distance > 0);
      if (!runActs.length) return null;
      const grouped = {};
      runActs.forEach(a => {
        const d = a.start_date?.toDate ? a.start_date.toDate() : new Date(a.start_date);
        const day = d.getDay();
        const monday = new Date(d);
        monday.setDate(d.getDate() - (day === 0 ? 6 : day - 1));
        monday.setHours(0,0,0,0);
        const key = monday.toISOString().split("T")[0];
        if (!grouped[key]) grouped[key] = { miles: 0, runs: 0, weekStart: new Date(monday) };
        grouped[key].miles += a.distance / 1609.34;
        grouped[key].runs++;
      });
      const weeks = Object.values(grouped);
      const peak = weeks.reduce((best, w) => w.miles > best.miles ? w : best, { miles: 0, runs: 0, weekStart: new Date() });
      if (!peak.miles) return null;
      const sunday = new Date(peak.weekStart);
      sunday.setDate(peak.weekStart.getDate() + 6);
      return {
        miles: parseFloat(peak.miles.toFixed(1)),
        runs: peak.runs,
        dateLabel: peak.weekStart.toLocaleDateString("en-US", { month: "short", day: "numeric" }) +
          " – " + sunday.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }),
      };
    }

    // ── Year-Over-Year Comparison (rolling 4-week) ──────────────────────────
    function calcYearOverYear(activities) {
      const runActs = activities.filter(a => isRun(a) && a.distance > 0);
      const now = new Date();
      const cur4wStart = new Date(now); cur4wStart.setDate(now.getDate() - 28);
      const lyEnd = new Date(now); lyEnd.setFullYear(now.getFullYear() - 1);
      const lyStart = new Date(lyEnd); lyStart.setDate(lyEnd.getDate() - 28);
      let curMiles = 0, curRuns = 0, lyMiles = 0, lyRuns = 0;
      runActs.forEach(a => {
        const d = a.start_date?.toDate ? a.start_date.toDate() : new Date(a.start_date);
        if (d >= cur4wStart && d <= now) { curMiles += a.distance / 1609.34; curRuns++; }
        if (d >= lyStart && d <= lyEnd) { lyMiles += a.distance / 1609.34; lyRuns++; }
      });
      if (!lyMiles && !lyRuns) return null;
      const diffPct = lyMiles > 0 ? Math.round((curMiles - lyMiles) / lyMiles * 100) : null;
      return {
        currentMiles: parseFloat(curMiles.toFixed(1)),
        lastYearMiles: parseFloat(lyMiles.toFixed(1)),
        diffPct,
        currentRuns: curRuns,
        lastYearRuns: lyRuns,
      };
    }

    // calcExecutionScore is provided by running/lib/scoring.js (ScoringLib)

    // ── Sleep & Recovery Correlations ────────────────────────────────────────
    function calcRecoveryCorrelations(healthEntries, activities) {
      if (!healthEntries?.length || !activities?.length) return null;

      // Build a map of date -> health entry
      const healthByDate = {};
      healthEntries.forEach(h => { if (h.date) healthByDate[h.date] = h; });

      // Build a map of date -> best run performance (pace on easy runs, or all runs)
      const runsByDate = {};
      activities.filter(a => (a.type === "Run" || a.type === "VirtualRun") && a.distance > 1500 && a.moving_time > 0).forEach(a => {
        const d = a.start_date?.toDate ? a.start_date.toDate() : (a.start_date?.seconds ? new Date(a.start_date.seconds * 1000) : new Date(a.start_date));
        const key = d.toISOString().split("T")[0];
        const pace = a.moving_time / (a.distance / 1609.34) / 60; // min/mi
        if (!runsByDate[key] || pace < runsByDate[key].pace) {
          runsByDate[key] = { pace, hr: a.average_heartrate, sufferScore: a.suffer_score, distance: a.distance / 1609.34 };
        }
      });

      // Correlate: for each health date that also has a run, pair them
      const pairs = [];
      Object.keys(healthByDate).forEach(date => {
        const h = healthByDate[date];
        const r = runsByDate[date];
        if (r && r.pace > 0) {
          pairs.push({ date, health: h, run: r });
        }
      });

      if (pairs.length < 5) return null; // need minimum data points

      // Compute correlations for each health metric vs pace
      const correlate = (vals) => {
        const n = vals.length;
        if (n < 5) return null;
        const xMean = vals.reduce((s, v) => s + v.x, 0) / n;
        const yMean = vals.reduce((s, v) => s + v.y, 0) / n;
        let num = 0, denX = 0, denY = 0;
        vals.forEach(v => {
          num += (v.x - xMean) * (v.y - yMean);
          denX += (v.x - xMean) ** 2;
          denY += (v.y - yMean) ** 2;
        });
        const den = Math.sqrt(denX * denY);
        return den > 0 ? num / den : 0;
      };

      // Build insights from correlations
      const insights = [];

      // HRV vs pace (higher HRV → faster pace = negative correlation)
      const hrvPairs = pairs.filter(p => p.health.hrv > 0 || p.health.hrvLastNight > 0).map(p => ({
        x: p.health.hrvLastNight || p.health.hrv, y: p.run.pace
      }));
      if (hrvPairs.length >= 8) {
        const r = correlate(hrvPairs);
        if (r !== null && Math.abs(r) > 0.15) {
          const sorted = [...hrvPairs].sort((a, b) => a.x - b.x);
          const lowHRV = sorted.slice(0, Math.floor(sorted.length / 3));
          const highHRV = sorted.slice(-Math.floor(sorted.length / 3));
          const lowPace = lowHRV.reduce((s, v) => s + v.y, 0) / lowHRV.length;
          const highPace = highHRV.reduce((s, v) => s + v.y, 0) / highHRV.length;
          const diffSec = Math.round((lowPace - highPace) * 60);
          if (Math.abs(diffSec) >= 3) {
            insights.push({
              metric: "HRV",
              icon: "💓",
              r: parseFloat(r.toFixed(2)),
              n: hrvPairs.length,
              finding: diffSec > 0
                ? `High HRV days → ${diffSec}s/mi faster`
                : `HRV shows ${Math.abs(diffSec)}s/mi impact`,
              actionable: r < -0.2 ? "Trust high-HRV days for quality sessions" : "Your pace is fairly stable regardless of HRV",
            });
          }
        }
      }

      // Sleep hours vs pace
      const sleepPairs = pairs.filter(p => p.health.sleepHours > 0).map(p => ({
        x: p.health.sleepHours, y: p.run.pace
      }));
      if (sleepPairs.length >= 8) {
        const r = correlate(sleepPairs);
        if (r !== null && Math.abs(r) > 0.1) {
          const sorted = [...sleepPairs].sort((a, b) => a.x - b.x);
          const lowSleep = sorted.slice(0, Math.floor(sorted.length / 3));
          const highSleep = sorted.slice(-Math.floor(sorted.length / 3));
          const lowPace = lowSleep.reduce((s, v) => s + v.y, 0) / lowSleep.length;
          const highPace = highSleep.reduce((s, v) => s + v.y, 0) / highSleep.length;
          const diffSec = Math.round((lowPace - highPace) * 60);
          const thresh = Math.round(sorted[Math.floor(sorted.length * 0.33)].x * 10) / 10;
          if (Math.abs(diffSec) >= 3) {
            insights.push({
              metric: "Sleep",
              icon: "😴",
              r: parseFloat(r.toFixed(2)),
              n: sleepPairs.length,
              finding: diffSec > 0
                ? `<${thresh}h sleep → ${Math.abs(diffSec)}s/mi slower`
                : `Sleep duration has minimal pace impact`,
              actionable: r < -0.15 ? `Prioritize ${thresh}+ hours before quality sessions` : "Your performance is resilient to sleep variation",
            });
          }
        }
      }

      // Body battery vs pace
      const battPairs = pairs.filter(p => (p.health.bodyBattery > 0 || p.health.bodyBatteryStart > 0)).map(p => ({
        x: p.health.bodyBatteryStart || p.health.bodyBattery, y: p.run.pace
      }));
      if (battPairs.length >= 8) {
        const r = correlate(battPairs);
        if (r !== null && Math.abs(r) > 0.15) {
          const sorted = [...battPairs].sort((a, b) => a.x - b.x);
          const lowBatt = sorted.slice(0, Math.floor(sorted.length / 3));
          const highBatt = sorted.slice(-Math.floor(sorted.length / 3));
          const lowPace = lowBatt.reduce((s, v) => s + v.y, 0) / lowBatt.length;
          const highPace = highBatt.reduce((s, v) => s + v.y, 0) / highBatt.length;
          const diffSec = Math.round((lowPace - highPace) * 60);
          if (Math.abs(diffSec) >= 3) {
            insights.push({
              metric: "Body Battery",
              icon: "🔋",
              r: parseFloat(r.toFixed(2)),
              n: battPairs.length,
              finding: diffSec > 0
                ? `High battery → ${diffSec}s/mi faster`
                : `Battery has ${Math.abs(diffSec)}s/mi impact`,
              actionable: r < -0.2 ? "Schedule hard sessions when battery is 60+" : "Your battery doesn't strongly predict performance",
            });
          }
        }
      }

      // Training readiness vs pace
      const readyPairs = pairs.filter(p => p.health.trainingReadiness > 0).map(p => ({
        x: p.health.trainingReadiness, y: p.run.pace
      }));
      if (readyPairs.length >= 8) {
        const r = correlate(readyPairs);
        if (r !== null && Math.abs(r) > 0.15) {
          insights.push({
            metric: "Training Readiness",
            icon: "⚡",
            r: parseFloat(r.toFixed(2)),
            n: readyPairs.length,
            finding: `Readiness correlates ${Math.abs(r) > 0.3 ? "strongly" : "moderately"} with pace (r=${r.toFixed(2)})`,
            actionable: r < -0.2 ? "Garmin readiness is a reliable signal for you — listen to it" : "Readiness score has limited predictive value for your running",
          });
        }
      }

      return { insights, totalPairs: pairs.length };
    }

    function Analytics({ activities, hrZoneConfig, userId, isOwner }) {
      // MP anchor for workout slicing (lap → rep classification). Declared
      // here too — #422 referenced Training's copy from this component,
      // which is out of scope and crashed the tab with a ReferenceError.
      const sliceMp = useMemo(() => {
        try {
          const runs2 = (activities || []).filter(a => isRun(a));
          const vo2 = window.RaceMath.calcEffectiveVO2max(runs2);
          const tp = vo2 ? window.RaceMath.trainingPaceMidpoints(vo2, runs2) : null;
          return tp?.mp || null;
        } catch (_) { return null; }
      }, [activities]);
      const { healthEntries, activePlan, athleteProfile } = useData();
      const [tab, setTab] = useState("Fitness");
      const [wxVar, setWxVar] = useState("dewPointF");
      const [wxBackfill, setWxBackfill] = useState(null); // null | "running" | result string
      const [raceCalcDist, setRaceCalcDist] = useState("");
      const [raceCalcTime, setRaceCalcTime] = useState("");
      const [manualPRs, setManualPRs] = useState({ racePRs: [], bestEfforts: [] });
      const [showAddPR, setShowAddPR] = useState(null); // "race" | "effort" | null
      const [prForm, setPrForm] = useState({ distance: "", time: "", date: "", activityName: "" });
      // Cached workout slices keyed by activity id — feeds the intensity
      // distribution's middle tier so structured sessions without a
      // Garmin/Strava time-in-zone breakdown are read by their reps rather
      // than flattened to the whole-run average HR. Read-only from the
      // persisted detail/laps doc; never calls Strava.
      const [distSlices, setDistSlices] = useState({}); // { actId: { slices, rollup } | null }

      // Load manual PRs from Firestore, seed if empty
      useEffect(() => {
        if (!userId) return;
        const ref = db.collection("users").doc(userId).collection("settings").doc("personalRecords");
        const unsub = ref.onSnapshot(doc => {
          if (doc.exists) {
            setManualPRs(doc.data());
          } else {
            // Seed with Strava PR data on first load
            const seed = {
              racePRs: [
                { distance:"1 mile", time:"5:56" },
                { distance:"2 miles", time:"12:15" },
                { distance:"5k", time:"19:03" },
                { distance:"10k", time:"39:56" },
                { distance:"15k", time:"1:00:37" },
                { distance:"10 miles", time:"1:05:00" },
                { distance:"20k", time:"1:24:05" },
                { distance:"Half-Marathon", time:"1:26:06" },
                { distance:"30k", time:"2:12:00" },
                { distance:"Marathon", time:"3:10:40" },
              ],
              bestEfforts: [
                { distance:"400m", time:"1:09" },
                { distance:"1/2 mile", time:"2:40" },
                { distance:"1K", time:"3:31" },
                { distance:"1 mile", time:"5:55" },
                { distance:"2 miles", time:"12:15" },
                { distance:"5k", time:"19:03" },
                { distance:"10k", time:"39:56" },
                { distance:"15k", time:"1:00:37" },
                { distance:"10 miles", time:"1:05:01" },
                { distance:"20k", time:"1:20:47" },
                { distance:"Half-Marathon", time:"1:25:15" },
                { distance:"30k", time:"2:09:55" },
                { distance:"Marathon", time:"3:04:16" },
              ],
            };
            ref.set(seed);
            setManualPRs(seed);
          }
        });
        return unsub;
      }, [userId]);

      const savePR = async (type) => {
        if (!prForm.distance || !prForm.time) return;
        const newPR = { ...prForm };
        const updated = { ...manualPRs };
        const key = type === "race" ? "racePRs" : "bestEfforts";
        // Replace if same distance exists, otherwise add
        const existing = updated[key].findIndex(p => p.distance === newPR.distance);
        if (existing >= 0) updated[key][existing] = newPR;
        else updated[key].push(newPR);
        // Sort by standard distance order
        const distOrder = ["400m","1/2 mile","1K","1 mile","2 miles","5k","10k","15k","10 miles","20k","Half-Marathon","30k","Marathon"];
        updated[key].sort((a,b) => {
          const ai = distOrder.findIndex(d => d.toLowerCase() === a.distance.toLowerCase());
          const bi = distOrder.findIndex(d => d.toLowerCase() === b.distance.toLowerCase());
          return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
        });
        await db.collection("users").doc(userId).collection("settings").doc("personalRecords").set(updated);
        setPrForm({ distance: "", time: "", date: "", activityName: "" });
        setShowAddPR(null);
      };

      const deletePR = async (type, index) => {
        const updated = { ...manualPRs };
        const key = type === "race" ? "racePRs" : "bestEfforts";
        updated[key].splice(index, 1);
        await db.collection("users").doc(userId).collection("settings").doc("personalRecords").set(updated);
      };

      const allSorted = useMemo(() =>
        activities.sort((a,b) => a.start_date?.seconds - b.start_date?.seconds),
        [activities]);

      const runs = useMemo(() => allSorted.filter(a => isRun(a)), [allSorted]);

      // Plan start date (from active training plan)
      const planStartDate = useMemo(() => {
        if (!activePlan) return null;
        return activePlan.createdAt?.seconds
          ? new Date(activePlan.createdAt.seconds * 1000)
          : activePlan.createdAt ? new Date(activePlan.createdAt) : null;
      }, [activePlan]);

      // New premium computations
      const planDays = planStartDate ? Math.ceil((Date.now() - planStartDate.getTime()) / 86400000) + 1 : 30;
      const trainingDist = useMemo(() => calcTrainingDistribution(allSorted, planDays, hrZoneConfig, distSlices), [allSorted, planDays, hrZoneConfig, distSlices]);
      // Lazily hydrate slices for runs inside the distribution window when
      // the Distribution tab is open. Reads only the persisted detail/laps
      // doc (never Strava). When that doc has no stored `slices` but DOES
      // carry laps/streams, slice on the fly from the current-fitness MP
      // anchor — otherwise an interval session with un-sliced detail would
      // collapse into the whole-run avg-HR tier and lose its Z3/Z4 reps.
      // Only runs with neither laps/streams nor stored slices resolve to
      // null and fall through to avg-HR. Each id is fetched at most once.
      // Skips runs that already carry an hrTimeInZones breakdown (the slicer
      // tier wouldn't be consulted for them anyway).
      useEffect(() => {
        if (tab !== "Distribution" || !userId) return;
        const cutoff = Date.now() - planDays * 86400000;
        const dateMs = (a) => {
          const s = a?.start_date;
          if (!s) return 0;
          if (typeof s.toDate === "function") return s.toDate().getTime();
          if (typeof s.seconds === "number") return s.seconds * 1000;
          const t = Date.parse(s); return Number.isFinite(t) ? t : 0;
        };
        allSorted.filter(a => {
          if (!isRun(a) || !a.moving_time || dateMs(a) < cutoff) return false;
          if (distSlices[String(a.id)] !== undefined) return false; // fetched / in-flight
          const tiz = a.hrTimeInZones || a.dynamics?.hrTimeInZones || a.runalyze?.hrTimeInZones;
          return !(tiz && typeof tiz === "object" && Object.keys(tiz).length);
        }).forEach(a => {
          const id = String(a.id);
          setDistSlices(prev => prev[id] !== undefined ? prev : { ...prev, [id]: null }); // mark in-flight
          db.collection("users").doc(userId).collection("activities").doc(id)
            .collection("detail").doc("laps").get()
            .then(doc => {
              const data = doc.exists ? doc.data() : null;
              let sl = data ? data.slices : null;
              // No persisted slices — carve them now from the cached
              // laps/streams so the rep intensity is read instead of the
              // diluted whole-run average. Mirrors the per-workout view's
              // on-the-fly slicing (treadmill calibration included).
              if ((!sl || !Array.isArray(sl.slices)) && data && sliceMp && window.WorkoutSlicer) {
                const laps = data.laps, streams = data.streams;
                if ((laps && laps.length) || (streams && streams.length)) {
                  try {
                    const isTreadmill = !!(a.trainer || a.type === "VirtualRun" || a.dynamics?.isIndoor);
                    sl = window.WorkoutSlicer.sliceWorkout({
                      laps, streams, mp: sliceMp, classifyPaceZone: window.RaceMath.classifyPaceZone,
                      activityDistanceM: a.distance || null, isTreadmill,
                    });
                  } catch (_) { sl = null; }
                }
              }
              setDistSlices(prev => ({ ...prev, [id]: (sl && Array.isArray(sl.slices)) ? sl : null }));
            })
            .catch(() => setDistSlices(prev => ({ ...prev, [id]: null })));
        });
      }, [tab, userId, allSorted, planDays, sliceMp]);
      const streaks = useMemo(() => calcStreaks(allSorted), [allSorted]);
      const personalRecords = useMemo(() => calcPersonalRecords(allSorted), [allSorted]);
      const progression = useMemo(() => calcProgression(allSorted, 12, athleteProfile), [allSorted, athleteProfile]);
      const allTimePeak = useMemo(() => calcAllTimePeakWeek(allSorted), [allSorted]);
      const yoy = useMemo(() => calcYearOverYear(allSorted), [allSorted]);
      const vo2Confidence = useMemo(() => vo2ConfidenceBand(runs), [runs]);
      // Effective VO2max — Garmin Firstbeat reading is the canonical source
      // (matches the Status panel + Race Time History chart). Calc-from-
      // quality-runs is the fallback only when no Garmin reading exists.
      const effectiveVO2 = useMemo(() => {
        const dayWithVO2 = (healthEntries || [])
          .filter(h => h?.garminVO2max > 30)
          .sort((a, b) => (b.date || "").localeCompare(a.date || ""))[0];
        if (dayWithVO2) return dayWithVO2.garminVO2max;
        return calcEffectiveVO2max(runs);
      }, [healthEntries, runs]);

      const recoveryCorrelations = useMemo(() => calcRecoveryCorrelations(healthEntries, allSorted), [healthEntries, allSorted]);

      // Race calculator predictions
      const raceCalcPredictions = useMemo(() => {
        if (!raceCalcDist || !raceCalcTime) return null;
        const distMap = { "5K": 5000, "10K": 10000, "Half": 21097, "Marathon": 42195 };
        const knownDist = distMap[raceCalcDist];
        if (!knownDist) return null;
        const parts = raceCalcTime.split(":").map(Number);
        let knownSecs = 0;
        if (parts.length === 3) knownSecs = parts[0]*3600 + parts[1]*60 + parts[2];
        else if (parts.length === 2) knownSecs = parts[0]*60 + parts[1];
        if (!knownSecs) return null;
        const targets = [
          { name: "1 Mile", meters: 1609.34 },
          { name: "5K", meters: 5000 },
          { name: "10K", meters: 10000 },
          { name: "Half Marathon", meters: 21097 },
          { name: "Marathon", meters: 42195 },
        ].filter(t => t.meters !== knownDist);
        return targets.map(t => ({
          name: t.name,
          time: predictFromRace(knownDist, knownSecs, t.meters),
          pace: predictFromRace(knownDist, knownSecs, t.meters) / (t.meters / 1609.34),
        }));
      }, [raceCalcDist, raceCalcTime]);


      // CTL/ATL/TSB — the SAME Banister TRIMP model as the Summary
      // "Fitness & Fatigue" chart and the server snapshot
      // (window.TrainingLoad), so this tab can never disagree with them.
      // This tab previously ran its own linear HR/150 "TSS" with a zero
      // seed — two charts both labeled Form that contradicted each other.
      // Output shape unchanged (fitness/fatigue/form + run/xt split for
      // the load bars); 180-day warm-up so the 42-day constant converges,
      // seeded from the first fortnight like the lib, then last 60 shown.
      const fitnessData = useMemo(() => {
        const TL = window.TrainingLoad;
        if (!allSorted.length || !TL) return [];
        const hrOpts = { maxHR: hrZoneConfig?.maxHR || null, restHR: hrZoneConfig?.restingHR || null };
        const grouped = {};
        allSorted.forEach(r => {
          const d = r.start_date?.toDate ? r.start_date.toDate() : new Date(r.start_date);
          if (isNaN(d.getTime())) return;
          const key = d.toISOString().split("T")[0];
          if (!grouped[key]) grouped[key] = { runLoad: 0, xtLoad: 0 };
          const trimp = TL.sessionTrimp(r, hrOpts);
          if (isRun(r)) grouped[key].runLoad += trimp;
          else grouped[key].xtLoad += trimp;
        });
        const today = new Date();
        const series = [];
        for (let i = 180; i >= 0; i--) {
          const d = new Date(today); d.setDate(today.getDate() - i);
          const g = grouped[d.toISOString().split("T")[0]] || { runLoad: 0, xtLoad: 0 };
          series.push({ d, runLoad: g.runLoad, xtLoad: g.xtLoad, load: g.runLoad + g.xtLoad });
        }
        const seedN = Math.min(14, series.length);
        const seed = series.slice(0, seedN).reduce((s, p) => s + p.load, 0) / seedN;
        let ctl = seed, atl = seed;
        const data = series.map(p => {
          ctl = ctl + (p.load - ctl) * (1 - Math.exp(-1/42));
          atl = atl + (p.load - atl) * (1 - Math.exp(-1/7));
          return {
            date: p.d.toLocaleDateString("en-US",{month:"short",day:"numeric"}),
            fitness: Math.round(ctl),
            // Fatigue (ATL) is a positive load value, plotted above the zero
            // line like Fitness — the standard Performance Management Chart
            // convention. Form = Fitness − Fatigue (the amber line below).
            fatigue: Math.round(atl),
            form: Math.round(ctl - atl),
            runLoad: Math.round(p.runLoad),
            xtLoad: Math.round(p.xtLoad),
          };
        });
        return data.slice(-60);
      }, [allSorted, hrZoneConfig]);

      // Pace trend — all runs
      const paceTrend = useMemo(() => {
        return runs.slice(-20).map(r => ({
          date: fmt.dateShort(r.start_date),
          pace: r.distance > 0 ? parseFloat((r.moving_time / (r.distance/1609.34) / 60).toFixed(2)) : null,
          dist: parseFloat((r.distance/1609.34).toFixed(1)),
        })).filter(r => r.pace !== null && r.dist > 1);
      }, [runs]);

      // Easy run aerobic efficiency (Z1-Z2 HR runs, last 30).
      //
      // Metric: Efficiency Factor (EF) = speed (m/s) ÷ avg HR. Standard
      // endurance metric — direction-correct (higher = more efficient =
      // faster pace at same HR = improving aerobic fitness). Scaled ×1000
      // for readable axis values (e.g. 18.5 instead of 0.0185).
      //
      // The previous metric — pace-sec/HR — was direction-flipped AND
      // got inflated when the athlete deliberately ran slower for
      // base-building (slow pace + low HR = high "efficiency" by that
      // formula even when fitness was unchanged). EF gets it right.
      // Weather-adjusted EF view: heat-driven HR drift inflates the
      // denominator, so summer easy runs read as efficiency loss when
      // they're cardiovascular load. Toggle discounts the HR by the same
      // heat allowance grading uses (ScoringLib.heatHRAllowance). Indoor
      // runs without a stamped weather doc assume the constant treadmill
      // environment (64°F / ~50% RH — newly synced ones arrive stamped
      // from settings/indoorEnvironment by the server).
      const [efHeatAdjusted, setEfHeatAdjusted] = useState(false);
      const easyRunEfficiency = useMemo(() => {
        // Anchor the window to plan start so the EF trend shows the
        // arc of THIS training block — pre-plan runs would only dilute
        // the regression with old fitness state. Falls back to last 30
        // runs if no plan is active so the chart still renders for
        // off-plan periods.
        const easyRuns = runs
          .filter(r => {
            if (!r.average_heartrate || !r.distance || !r.moving_time) return false;
            if (classifyHRZone(r.average_heartrate, hrZoneConfig) > 1) return false;
            if (planStartDate) {
              const d = r.start_date?.toDate ? r.start_date.toDate() : new Date(r.start_date);
              if (d < planStartDate) return false;
            }
            return true;
          })
          .slice(planStartDate ? 0 : -30)
          .map(r => {
            const paceMinMi = parseFloat((r.moving_time / (r.distance / 1609.34) / 60).toFixed(2));
            const hr = Math.round(r.average_heartrate);
            const rawSpeedMs = r.distance / r.moving_time;
            // Grade-adjust speed so a treadmill at 1% or a hilly course
            // doesn't read as efficiency drift. Strava's own
            // gradeAdjustedSpeed wins when present (it handles uphill
            // and downhill cost separately); otherwise derive a simple
            // adjustment from total ascent over distance using a 3.3%
            // pace cost per 1% grade (≈ Daniels / Minetti for moderate
            // grades). Treadmill runs are backfilled with
            // total_elevation_gain, so a 1% indoor base picks up its
            // ~3% efficiency credit and doesn't depress the trend
            // when the athlete switches from flat outdoor to incline
            // indoor work.
            const gapMs = (Number.isFinite(r.gradeAdjustedSpeed) && r.gradeAdjustedSpeed > 0)
              ? r.gradeAdjustedSpeed
              : (() => {
                  const gainM = Number(r.total_elevation_gain) || 0;
                  if (gainM <= 0 || !(r.distance > 0)) return rawSpeedMs;
                  const gradePct = (gainM / r.distance) * 100;
                  return rawSpeedMs * (1 + 0.033 * gradePct);
                })();
            // EF × 1000 → numbers like 17.5–22.0 across recreational fitness.
            const wx = r.weather || ((r.trainer || r.indoor || r.type === "VirtualRun") ? { tempF: 64, humidity: 50 } : null);
            // Continuous aerobic-drift curve (starts ~62°F) — NOT the
            // step-wise grading allowance, which begins at 72°F and would
            // leave a spring block entirely unadjusted. Computed for EVERY
            // run regardless of the toggle, so the card can always report
            // how much heat it removed and draw the unadjusted reference —
            // otherwise a cool block's tiny shift just looks like a dead toggle.
            const heatBpm = (wx && window.ScoringLib && window.ScoringLib.aerobicHeatDrift)
              ? (window.ScoringLib.aerobicHeatDrift(Number(wx.tempF), Number(wx.humidity))?.bpm || 0) : 0;
            const efRaw = parseFloat(((gapMs / hr) * 1000).toFixed(2));
            const efAdj = parseFloat(((gapMs / Math.max(80, hr - heatBpm)) * 1000).toFixed(2));
            const ef = efHeatAdjusted ? efAdj : efRaw;
            return { date: fmt.dateShort(r.start_date), pace: paceMinMi, hr, ef, efRaw, efAdj, heatBpm, dist: parseFloat((r.distance/1609.34).toFixed(1)) };
          })
          .filter(r => r.dist > 0.5 && r.ef > 0);
        if (easyRuns.length < 2) return easyRuns;
        // Linear regression of a chosen EF series over run index.
        const fit = (key) => {
          const n = easyRuns.length;
          const xMean = (n - 1) / 2;
          const yMean = easyRuns.reduce((s, d) => s + d[key], 0) / n;
          let num = 0, den = 0;
          easyRuns.forEach((d, i) => { num += (i - xMean) * (d[key] - yMean); den += (i - xMean) ** 2; });
          const slope = den > 0 ? num / den : 0;
          const intercept = yMean - slope * xMean;
          return { at: (i) => parseFloat((intercept + slope * i).toFixed(2)), slope };
        };
        // Slope thresholds matched to typical EF noise (~0.5 over 30 runs
        // = meaningful fitness shift). Tighter than the old AEI bands.
        // `efTrend` follows the active (toggled) series; `efRefTrend` is the
        // OTHER series — drawn faint when heat is on so the gap between the
        // two trend lines literally is the heat correction.
        const active = fit(efHeatAdjusted ? "efAdj" : "efRaw");
        const reference = fit(efHeatAdjusted ? "efRaw" : "efAdj");
        return easyRuns.map((d, i) => ({ ...d, efTrend: active.at(i), efRefTrend: reference.at(i), trendSlope: active.slope }));
      }, [runs, hrZoneConfig, planStartDate, efHeatAdjusted]);

      // Tempo / threshold pace progression — the "faster stuff" sibling of
      // the easy EF card. Sustained quality only: continuous tempo/
      // threshold runs where the whole-activity average pace genuinely
      // reflects the work. Rep-intervals / track / fartlek are excluded
      // because their average pace (recovery jogs folded in) is not a
      // real effort pace and would just add noise. Faster pace at the
      // same kind of session = fitter, so a falling trend = improving.
      const thresholdPaceProgression = useMemo(() => {
        // Keyed off the workout name: a marathon/half RACE at threshold
        // HR isn't a tempo session, so HR-zone alone over-collects.
        // INCLUDE names the session as tempo/threshold; EXCLUDE drops
        // rep-intervals/track/fartlek (average pace meaningless) and
        // easy/long runs. Whole-activity average is the consistent
        // cross-run signal (same limitation the EF card lives with).
        const EXCLUDE_NAME = /\b(easy|recovery|recover|shake.?out|warm.?up|cool.?down|jog|long\s*run|interval|intervals?|repeats?|reps?|fartlek|track|vo2|strides?|hill\s*sprints?)\b/;
        // Rep-interval markers ("rolling 800s", "6x400", "300s"). \b fails
        // on "800s" (digit→letter is no boundary), so match the digits
        // directly rather than relying on word boundaries.
        const REP = /(rolling\s*\d{2,4}|\d{2,4}\s*m?\s*s\b|\d{1,2}\s*x\s*\d{2,4}|\bx\s*\d{3,4}|\b(?:200|300|400|600|800|1000|1200|1600)\s*m\b|1k\s*rep|mile\s*rep)/;
        const INCLUDE = /\b(tempo|threshold|lt2?|cruise|steady\s*state|steady)\b/;
        // Auto-named Strava runs ("Afternoon Run") never match INCLUDE, so a
        // plan follower's tempo work went uncounted. A run linked to a plan
        // tempo/threshold day IS a sustained quality session — trust the
        // plan tag over the name. Intervals stay excluded (the plan tags
        // them "intervals", which is not in this set).
        const SUSTAINED_PLAN_TYPE = /^(tempo|threshold|race_pace)$/;
        const planTempoIds = new Set();
        (activePlan?.weeks || []).forEach(w => (w.days || []).forEach(d => {
          if (d.linkedActivityId && SUSTAINED_PLAN_TYPE.test(String(d.type || "").toLowerCase())) {
            planTempoIds.add(String(d.linkedActivityId));
          }
        }));
        const qualityRuns = runs
          .filter(r => {
            if (!r.distance || !r.moving_time) return false;
            if (r.distance < 2 * 1609.34) return false; // sub-2mi → not a sustained tempo
            // Plan-tagged tempo/threshold/race_pace days are trusted outright.
            // Everything else has to earn its place — and most athletes don't
            // rename their tempo runs, so the name alone misses them. Fall back
            // to the signals the rest of the app uses (Strava workout_type, the
            // max/avg speed surge ratio that separates surgy intervals from a
            // sustained effort) plus the average-HR band.
            if (!planTempoIds.has(String(r.id))) {
              const name = (r.name || "").toLowerCase();
              // Named rep-work / strides / easy / long never qualify — their
              // whole-run average pace folds in recovery and is meaningless.
              if (EXCLUDE_NAME.test(name) || REP.test(name)) return false;
              // A Strava-flagged race (workout_type 1) is a max effort, not a
              // repeatable tempo session — keep it off this progression trend.
              if (r.workout_type === 1) return false;
              const nameTagged = INCLUDE.test(name);
              // Surge guard: a high peak-to-average speed ratio over a short run
              // is interval surginess, not a sustained effort.
              const ratio = (r.average_speed > 0 && r.max_speed > 0) ? r.max_speed / r.average_speed : 0;
              const surgy = ratio >= 1.35 && r.distance < 8 * 1609.34;
              // Effort signal for an un-named run: Strava marked it a Workout, or
              // its average HR sits in the tempo/threshold band (Z3–Z4 → 0-based
              // index 2–3). Z2 and below is easy / marathon-pace (its own card).
              const hrZ = (Number.isFinite(r.average_heartrate) && r.average_heartrate > 0)
                ? classifyHRZone(r.average_heartrate, hrZoneConfig) : null;
              const effortTagged = !surgy && (r.workout_type === 3 || hrZ === 2 || hrZ === 3);
              if (!nameTagged && !effortTagged) return false;
            }
            if (planStartDate) {
              const d = r.start_date?.toDate ? r.start_date.toDate() : new Date(r.start_date);
              if (d < planStartDate) return false;
            }
            return true;
          })
          .slice(planStartDate ? 0 : -30)
          .map(r => {
            const pace = parseFloat((r.moving_time / (r.distance / 1609.34) / 60).toFixed(2));
            return { date: fmt.dateShort(r.start_date), pace, hr: r.average_heartrate ? Math.round(r.average_heartrate) : null, dist: parseFloat((r.distance / 1609.34).toFixed(1)) };
          })
          .filter(r => r.pace >= 4.5 && r.pace <= 9.0); // drop GPS-garbage paces
        if (qualityRuns.length < 2) return qualityRuns;
        const n = qualityRuns.length;
        const xMean = (n - 1) / 2;
        const yMean = qualityRuns.reduce((s, d) => s + d.pace, 0) / n;
        let num = 0, den = 0;
        qualityRuns.forEach((d, i) => { num += (i - xMean) * (d.pace - yMean); den += (i - xMean) ** 2; });
        const regSlope = den > 0 ? num / den : 0;
        const regIntercept = yMean - regSlope * xMean;
        return qualityRuns.map((d, i) => ({ ...d, paceTrend: parseFloat((regIntercept + regSlope * i).toFixed(3)), trendSlope: regSlope }));
      }, [runs, planStartDate, activePlan, hrZoneConfig]);

      // Weather impact — detrended EF residual vs the observed weather.
      // Weather is denormalised onto the activity doc server-side (webhook
      // + the backfillInsightWeather migration), so this is a pure client
      // computation; runs missing weather drop out and the empty state
      // offers the backfill. Effort is controlled the same way Easy Run
      // Efficiency does (Z1 only) so EF is comparable across the sample.
      const weatherSamples = useMemo(() => {
        return runs
          .filter(r => {
            const w = r.weather;
            if (!w || w.tempF == null) return false;
            if (!r.average_heartrate || !r.distance || !r.moving_time) return false;
            if (classifyHRZone(r.average_heartrate, hrZoneConfig) > 1) return false;
            return true;
          })
          .map(r => {
            const speedMs = r.distance / r.moving_time;
            const ef = (speedMs / Math.round(r.average_heartrate)) * 1000;
            const d = r.start_date?.toDate ? r.start_date.toDate() : new Date(r.start_date);
            return {
              ef: parseFloat(ef.toFixed(3)),
              dateMs: d.getTime(),
              paceSec: Math.round(r.moving_time / (r.distance / 1609.34)),
              tempF: r.weather.tempF,
              humidity: r.weather.humidity,
              dewPointF: r.weather.dewPointF,
              windMph: r.weather.windMph,
              label: `${fmt.dateShort(r.start_date)} · ${r.name || "Run"}`,
            };
          })
          .filter(s => Number.isFinite(s.ef) && s.ef > 0);
      }, [runs, hrZoneConfig]);
      const weatherImpact = useMemo(
        () => (window.WeatherStats ? window.WeatherStats.analyzeWeatherImpact(weatherSamples) : null),
        [weatherSamples]
      );
      // Recovery cost of heat — each easy outdoor run's heat stress paired
      // with the overnight HRV logged the morning after it. Run dates are
      // formatted in local time (avoids a UTC off-by-one on the join).
      const weatherRecoverySamples = useMemo(() => {
        if (!Array.isArray(healthEntries) || !healthEntries.length) return [];
        const hrvByDate = {};
        healthEntries.forEach(h => { if (h && h.date && h.hrv > 0) hrvByDate[h.date] = h.hrv; });
        const localISO = (dt) => `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
        return runs
          .filter(r => {
            const w = r.weather;
            if (!w || w.tempF == null || w.dewPointF == null || !r.average_heartrate) return false;
            return classifyHRZone(r.average_heartrate, hrZoneConfig) <= 1; // easy runs only
          })
          .map(r => {
            const d = r.start_date?.toDate ? r.start_date.toDate() : new Date(r.start_date);
            const next = new Date(d); next.setDate(next.getDate() + 1);
            const hrv = hrvByDate[localISO(next)];
            if (!(hrv > 0)) return null;
            return {
              heatStress: r.weather.tempF + r.weather.dewPointF,
              hrv, dateMs: d.getTime(),
              label: `${fmt.dateShort(r.start_date)} · ${r.name || "Run"}`,
            };
          })
          .filter(Boolean);
      }, [runs, healthEntries, hrZoneConfig]);
      const weatherRecovery = useMemo(
        () => (window.WeatherStats ? window.WeatherStats.analyzeWeatherRecovery(weatherRecoverySamples) : null),
        [weatherRecoverySamples]
      );
      const wxStat = weatherImpact ? weatherImpact.vars[wxVar] : null;
      const wxChart = useMemo(() => {
        if (!wxStat) return [];
        return wxStat.points
          .map(p => {
            const fit = wxStat.intercept + wxStat.slope * p.x;
            return {
              x: parseFloat(p.x.toFixed(2)),
              y: parseFloat(p.y.toFixed(3)),
              fit: parseFloat(fit.toFixed(3)),
              upper: parseFloat((fit + wxStat.residStd).toFixed(3)),
              lower: parseFloat((fit - wxStat.residStd).toFixed(3)),
              runLabel: p.runLabel,
            };
          })
          .sort((a, b) => a.x - b.x);
      }, [wxStat]);

      // HR zone distribution helper
      const buildHRZones = (activities, sinceDate) => {
        const cfg = getHRZones(hrZoneConfig);
        const zones = cfg.zones.map(z => ({
          name: `Z${z.zone} ${z.name}`,
          color: z.color,
          minBPM: z.minBPM,
          maxBPM: z.maxBPM,
          seconds: 0,
        }));
        activities.forEach(r => {
          if (!r.average_heartrate) return;
          const rd = r.start_date?.toDate ? r.start_date.toDate() : new Date(r.start_date);
          if (sinceDate && rd < sinceDate) return;
          const zi = classifyHRZone(r.average_heartrate, hrZoneConfig);
          zones[zi].seconds += r.moving_time;
        });
        const total = zones.reduce((s,z) => s+z.seconds, 0) || 1;
        return zones.map(z => ({ ...z, pct: Math.round(z.seconds/total*100), hours: (z.seconds/3600).toFixed(1) }));
      };
      // Plan-period HR zones (primary) and all-time (secondary)
      const hrZones = useMemo(() => buildHRZones(allSorted, planStartDate), [allSorted, hrZoneConfig, planStartDate]);
      const hrZonesAll = useMemo(() => buildHRZones(allSorted, null), [allSorted, hrZoneConfig]);

      // Cross-training breakdown
      const xtBreakdown = useMemo(() => {
        const cats = {};
        allSorted.filter(a => isXT(a)).forEach(a => {
          const cat = actCat(effectiveType(a));
          if (!cats[cat]) cats[cat] = { count: 0, minutes: 0, calories: 0 };
          cats[cat].count++;
          cats[cat].minutes += (a.moving_time || 0) / 60;
          cats[cat].calories += a.calories || 0;
        });
        return Object.entries(cats).map(([cat, d]) => ({
          ...ACT_DISPLAY[cat], cat, ...d,
          hours: (d.minutes / 60).toFixed(1),
        })).sort((a, b) => b.minutes - a.minutes);
      }, [allSorted]);

      return (
        <div style={{ display:"flex", flexDirection:"column", gap: 20 }}>
          <div>
            <div style={{ fontFamily:"'Space Grotesk', sans-serif", fontSize: 26, fontWeight: 800, color: C.text, marginBottom: 4 }}>Analytics</div>
            <div style={{ color: C.textMuted, fontSize: 13 }}>
              Performance insights from {runs.length} runs + {allSorted.length - runs.length} cross-training activities
            </div>
          </div>

          <TabBar tabs={["Fitness","Pace","HR Zones","Distribution","PRs","Progression","Race Calc","Cross-Training","Weather"]} active={tab} onChange={setTab} />

          {tab === "Fitness" && (
            <div style={{ display:"flex", flexDirection:"column", gap: 16 }}>
              <div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fit,minmax(150px,1fr))", gap:16 }}>
                <Card>
                  <Stat label="Fitness (CTL)" value={fitnessData[fitnessData.length-1]?.fitness || "--"} unit="pts" color={C.green}/>
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>Chronic Training Load</div>
                </Card>
                <Card>
                  <Stat label="Fatigue (ATL)" value={fitnessData[fitnessData.length-1]?.fatigue || "--"} unit="pts" color={C.red}/>
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>Acute Training Load</div>
                </Card>
                <Card>
                  <Stat label="Form (TSB)" value={fitnessData[fitnessData.length-1]?.form || "--"} unit="pts"
                    color={(fitnessData[fitnessData.length-1]?.form||0) >= 0 ? C.green : C.amber}/>
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>
                    {(fitnessData[fitnessData.length-1]?.form||0) > 5 ? "Fresh — ready to race" :
                     (fitnessData[fitnessData.length-1]?.form||0) > -10 ? "Neutral — training" : "Fatigued — recover"}
                  </div>
                </Card>
                {fitnessData.length >= 14 && (() => {
                  const cur = fitnessData[fitnessData.length-1] || {};
                  const tsb = cur.form || 0;
                  const ctl = cur.fitness || 0;
                  const atl = cur.fatigue || 0;
                  const recentLoad = fitnessData.slice(-7).reduce((s,d) => s + d.runLoad + d.xtLoad, 0);
                  const priorLoad = fitnessData.slice(-14,-7).reduce((s,d) => s + d.runLoad + d.xtLoad, 0);
                  const loadDrop = priorLoad > 0 ? (priorLoad - recentLoad) / priorLoad * 100 : 0;
                  let score = 50;
                  score += Math.min(25, Math.max(-25, tsb * 1.5));
                  if (atl < ctl) score += 8;
                  if (loadDrop > 15 && loadDrop < 55) score += 12;
                  else if (loadDrop < 0) score -= 12;
                  score = Math.max(0, Math.min(100, Math.round(score)));
                  const label = score >= 80 ? "Race Ready" : score >= 65 ? "Nearly Fresh" : score >= 45 ? "In Training" : "Fatigued";
                  const col = score >= 80 ? C.green : score >= 65 ? C.cyan : score >= 45 ? C.amber : C.red;
                  return (
                    <Card style={{borderColor: col + "40"}}>
                      <Stat label="Race Readiness" value={score} unit="/100" color={col}/>
                      <div style={{fontSize:11,color:col,marginTop:6,fontWeight:600}}>{label}</div>
                      <div style={{marginTop:8,height:4,background:C.bg3,borderRadius:2}}>
                        <div style={{width:`${score}%`,height:"100%",background:col,borderRadius:2,transition:"width 0.5s"}}/>
                      </div>
                    </Card>
                  );
                })()}
              </div>
              <Card>
                <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:4}}>Fitness/Fatigue/Form — Last 60 Days</div>
                <div style={{fontSize:11,color:C.textMuted,marginBottom:16}}>Includes running + cross-training (weighted by sport type). Form = Fitness − Fatigue.</div>
                {fitnessData.length < 5 ? (
                  <EmptyState icon="📊" title="Need more data" sub="Sync more runs to see fitness trends" />
                ) : (
                  <ResponsiveContainer width="100%" height={220}>
                    <ComposedChart data={fitnessData}>
                      <CartesianGrid strokeDasharray="3 3" stroke={C.border} vertical={false} />
                      <XAxis dataKey="date" tick={{fill:C.textMuted,fontSize:10}} axisLine={false} tickLine={false} interval={9} />
                      <YAxis tick={{fill:C.textMuted,fontSize:11}} axisLine={false} tickLine={false} />
                      <Tooltip content={<ChartTooltip />} />
                      <Legend wrapperStyle={{fontSize:12,color:C.textMuted}} />
                      <Area type="monotone" dataKey="fitness" stroke={C.green} fill={C.green+"20"} strokeWidth={2} name="Fitness" />
                      <Area type="monotone" dataKey="fatigue" stroke={C.red} fill={C.red+"15"} strokeWidth={2} name="Fatigue" />
                      <Line type="monotone" dataKey="form" stroke={C.amber} strokeWidth={2} dot={false} name="Form" />
                      <ReferenceLine y={0} stroke={C.border} strokeDasharray="4 4" />
                    </ComposedChart>
                  </ResponsiveContainer>
                )}
              </Card>

              {/* Form guidance — what the current Form value means and what to aim for */}
              {fitnessData.length >= 10 && (() => {
                const cur = fitnessData[fitnessData.length-1] || {};
                const form = cur.form || 0;
                const prev = fitnessData[fitnessData.length-8]?.form ?? form; // ~7 days ago
                const delta = form - prev;
                const trend = delta > 3 ? "rising" : delta < -3 ? "falling" : "steady";
                const trendTxt = trend === "rising" ? "rising — fatigue is clearing"
                  : trend === "falling" ? "falling — you're loading up"
                  : "holding steady";
                const trendCol = trend === "rising" ? C.green : trend === "falling" ? C.amber : C.textDim;
                let zone, col, guidance;
                if (form > 5) {
                  zone = "Fresh / tapered"; col = C.green;
                  guidance = "You're rested and race-ready. Great for a goal race or a hard test. If no race is near, you're not building — add training load to drive Form back down toward −10 to −20.";
                } else if (form > -10) {
                  zone = "Balanced"; col = C.cyan;
                  guidance = "Sustainable maintenance zone. To build fitness, gradually add volume or intensity so Form drifts to roughly −10 to −20. To freshen up for a race, ease off and let it climb above 0.";
                } else if (form > -30) {
                  zone = "Productive build"; col = C.amber;
                  guidance = "This is where fitness actually grows — exactly where you want to be mid-training. Hold the load steady, but don't live here forever: take a recovery week every 3–4 weeks, and let Form rise toward 0 in the 7–14 days before any race.";
                } else {
                  zone = "Heavy fatigue"; col = C.red;
                  guidance = "Overreaching risk — Form is deep in the red. Back off now with a down week so Form recovers toward −10 or higher. Pushing deeper raises injury and burnout risk.";
                }
                return (
                  <Card>
                    <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8,flexWrap:"wrap"}}>
                      <div style={{fontSize:13,fontWeight:600,color:C.textDim}}>What to aim for</div>
                      <div style={{fontSize:11,fontWeight:600,color:col,background:col+"18",padding:"2px 8px",borderRadius:4}}>{zone}</div>
                      <div style={{fontSize:11,color:trendCol,marginLeft:"auto"}}>Form {trendTxt}</div>
                    </div>
                    <div style={{fontSize:13,color:C.text,lineHeight:1.5}}>{guidance}</div>
                    <div style={{fontSize:11,color:C.textMuted,marginTop:10,lineHeight:1.5}}>
                      Negative Form isn't a target to maximize — it's the byproduct of training hard. The goal is to <strong style={{color:C.textDim}}>cycle</strong>: build (Form −10 to −25) → recover toward 0 → race when Form is positive.
                    </div>
                  </Card>
                );
              })()}

              {/* Recovery Insights — correlations between health metrics & performance */}
              {recoveryCorrelations && recoveryCorrelations.insights.length > 0 && (
                <Card>
                  <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:4}}>Recovery Insights — Your Personal Correlations</div>
                  <div style={{fontSize:11,color:C.textMuted,marginBottom:10}}>
                    Based on {recoveryCorrelations.totalPairs} days with both health + run data
                  </div>
                  {/* Pearson r legend — keeps the correlation cards honest without
                      a user having to know stats. Bars match the per-card `barColor`
                      logic (cyan strong, amber moderate, muted weak). */}
                  <div style={{padding:"10px 14px",background:C.bg2+"80",border:`1px solid ${C.border}`,borderRadius:8,marginBottom:16}}>
                    <div style={{fontSize:10,color:C.textMuted,fontWeight:700,marginBottom:6,letterSpacing:"0.06em"}}>READING THE NUMBERS</div>
                    <div style={{fontSize:11,color:C.textDim,lineHeight:1.55}}>
                      <span style={{fontFamily:"'IBM Plex Mono',monospace",color:C.text}}>r</span> is the Pearson correlation between the health metric and your run performance on the same day. Range is <span style={{fontFamily:"'IBM Plex Mono',monospace"}}>−1 … +1</span>. Sign tells you the direction; magnitude tells you strength.
                    </div>
                    <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(160px,1fr))",gap:8,marginTop:8}}>
                      <div style={{padding:"6px 10px",background:C.cyan+"10",border:`1px solid ${C.cyan}30`,borderRadius:6}}>
                        <div style={{fontSize:10,color:C.cyan,fontWeight:700,fontFamily:"'IBM Plex Mono',monospace"}}>|r| ≥ 0.5</div>
                        <div style={{fontSize:10,color:C.textDim,marginTop:2}}>strong — trust it, act on it</div>
                      </div>
                      <div style={{padding:"6px 10px",background:C.amber+"10",border:`1px solid ${C.amber}30`,borderRadius:6}}>
                        <div style={{fontSize:10,color:C.amber,fontWeight:700,fontFamily:"'IBM Plex Mono',monospace"}}>0.3 ≤ |r| &lt; 0.5</div>
                        <div style={{fontSize:10,color:C.textDim,marginTop:2}}>moderate — useful signal</div>
                      </div>
                      <div style={{padding:"6px 10px",background:C.bg3,border:`1px solid ${C.border}`,borderRadius:6}}>
                        <div style={{fontSize:10,color:C.textMuted,fontWeight:700,fontFamily:"'IBM Plex Mono',monospace"}}>|r| &lt; 0.3</div>
                        <div style={{fontSize:10,color:C.textDim,marginTop:2}}>weak — mostly noise</div>
                      </div>
                    </div>
                    <div style={{fontSize:10,color:C.textMuted,marginTop:8,lineHeight:1.55}}>
                      <span style={{fontFamily:"'IBM Plex Mono',monospace",color:"#94a3b8"}}>r &gt; 0</span>: the two move together (e.g. higher sleep → faster pace).
                      <span style={{fontFamily:"'IBM Plex Mono',monospace",color:"#94a3b8",marginLeft:8}}>r &lt; 0</span>: they move opposite (e.g. higher readiness → lower pace number = faster run).
                    </div>
                  </div>
                  <div style={{display:"flex",flexDirection:"column",gap:12}}>
                    {recoveryCorrelations.insights.map((ins, i) => {
                      const strength = Math.abs(ins.r);
                      const barColor = strength > 0.3 ? C.cyan : strength > 0.2 ? C.amber : C.textMuted;
                      return (
                        <div key={i} style={{padding:"12px 16px",background:C.bg2,borderRadius:8,border:`1px solid ${barColor}25`}}>
                          <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:6}}>
                            <div style={{display:"flex",alignItems:"center",gap:8}}>
                              <span style={{fontSize:16}}>{ins.icon}</span>
                              <span style={{fontSize:13,fontWeight:700,color:C.text}}>{ins.metric}</span>
                            </div>
                            <div style={{display:"flex",alignItems:"center",gap:8}}>
                              <span style={{fontSize:10,color:C.textMuted}}>{ins.n} data points</span>
                              <span style={{fontSize:10,padding:"2px 6px",borderRadius:4,fontWeight:700,
                                background:barColor+"20",color:barColor}}>
                                r={ins.r}
                              </span>
                            </div>
                          </div>
                          <div style={{fontSize:12,color:C.text,fontWeight:600,marginBottom:4}}>{ins.finding}</div>
                          <div style={{fontSize:11,color:C.textMuted,fontStyle:"italic"}}>{ins.actionable}</div>
                          <div style={{marginTop:8,height:3,background:C.bg3,borderRadius:2}}>
                            <div style={{width:`${Math.min(100, strength * 200)}%`,height:"100%",background:barColor,borderRadius:2}}/>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                </Card>
              )}
            </div>
          )}

          {tab === "Pace" && (
            <div style={{display:"flex",flexDirection:"column",gap:16}}>
              {/* All runs pace trend */}
              <Card>
                <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:16}}>Pace Trend — Last 20 Runs</div>
                {paceTrend.length < 3 ? (
                  <EmptyState icon="⚡" title="Not enough data" sub="Complete more runs to see pace trends" />
                ) : (
                  <ResponsiveContainer width="100%" height={220}>
                    <ComposedChart data={paceTrend}>
                      <CartesianGrid strokeDasharray="3 3" stroke={C.border} vertical={false} />
                      <XAxis dataKey="date" tick={{fill:C.textMuted,fontSize:10}} axisLine={false} tickLine={false} />
                      <YAxis yAxisId="pace" reversed tick={{fill:C.textMuted,fontSize:11}} axisLine={false} tickLine={false}
                        tickFormatter={v => `${Math.floor(v)}:${Math.round((v%1)*60).toString().padStart(2,"0")}`} domain={["auto","auto"]}/>
                      <YAxis yAxisId="dist" orientation="right" tick={{fill:C.textMuted,fontSize:11}} axisLine={false} tickLine={false} />
                      <Tooltip content={<ChartTooltip formatter={(v,n) => n==="pace" ? `${Math.floor(v)}:${Math.round((v%1)*60).toString().padStart(2,"0")} /mi` : `${v} mi`} />} />
                      <Legend wrapperStyle={{fontSize:12,color:C.textMuted}} />
                      <Line yAxisId="pace" type="monotone" dataKey="pace" stroke={C.amber} strokeWidth={2} dot={{fill:C.amber,r:3}} name="pace" />
                      <Bar yAxisId="dist" dataKey="dist" fill={C.cyan} fillOpacity={0.3} name="dist" barSize={20} />
                    </ComposedChart>
                  </ResponsiveContainer>
                )}
              </Card>

              {/* Easy run aerobic efficiency — Efficiency Factor (speed/HR) */}
              <Card>
                <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:4}}>
                  <div style={{display:"flex",alignItems:"center",gap:10,flexWrap:"wrap"}}>
                    <div style={{fontSize:13,fontWeight:600,color:C.textDim}}>Easy Run Aerobic Efficiency</div>
                    <div style={{display:"flex",gap:4}}>
                      {[["Hills only", false], ["Hills + heat", true]].map(([lab, val]) => (
                        <button key={lab} onClick={() => setEfHeatAdjusted(val)} style={{
                          padding:"3px 8px", fontSize:10, fontWeight:efHeatAdjusted===val?700:400, borderRadius:5, cursor:"pointer",
                          background:efHeatAdjusted===val?C.cyan+"20":"transparent", color:efHeatAdjusted===val?C.cyan:C.textMuted,
                          border:`1px solid ${efHeatAdjusted===val?C.cyan:C.border}`, fontFamily:"'IBM Plex Mono',monospace",
                        }}>{lab}</button>
                      ))}
                    </div>
                    {efHeatAdjusted && easyRunEfficiency.length > 0 && (() => {
                      // Only count runs with a non-trivial correction (≥1 bpm) —
                      // a 0.3-bpm tweak on a 63°F run is invisible and shouldn't
                      // claim the toggle "did something". Report the magnitude so
                      // a cool block reads as "barely any heat" rather than broken.
                      const adj = easyRunEfficiency.filter(d => d.heatBpm >= 1);
                      const n = adj.length;
                      const peak = n ? Math.max(...adj.map(d => d.heatBpm)) : 0;
                      const avg = n ? adj.reduce((s, d) => s + d.heatBpm, 0) / n : 0;
                      return (
                        <span style={{fontSize:10,color:n ? C.amber : C.textMuted,fontStyle:"italic"}}>
                          {n
                            ? `${n} of ${easyRunEfficiency.length} runs heat-adjusted · avg +${avg.toFixed(1)}, up to +${peak.toFixed(1)} bpm (grey = unadjusted)`
                            : `barely any heat in this block — most runs sit under the 62°F drift onset, so the trend is unchanged`}
                        </span>
                      );
                    })()}
                  </div>
                  {easyRunEfficiency.length >= 2 && (() => {
                    const slope = easyRunEfficiency[easyRunEfficiency.length - 1].trendSlope;
                    // EF noise floor: ±0.02/run. Over 30 runs that's ±0.6.
                    // Slopes outside ±0.015/run are meaningful fitness shifts.
                    const improving = slope > 0.015;
                    const declining = slope < -0.015;
                    return (
                      <div style={{fontSize:11,fontWeight:600,color: improving ? C.green : declining ? C.red : C.textMuted,
                        padding:"2px 8px",borderRadius:4,background: improving ? C.green+"15" : declining ? C.red+"15" : C.bg3}}>
                        {improving ? "↑ Improving" : declining ? "↓ Declining" : "→ Stable"}
                      </div>
                    );
                  })()}
                </div>
                <div style={{fontSize:11,color:C.textMuted,marginBottom:4}}>
                  Z1-Z2 runs only · Efficiency Factor (grade-adjusted m/s ÷ HR ×1000) · higher = faster pace at same HR · {easyRunEfficiency.length} run{easyRunEfficiency.length === 1 ? "" : "s"}{planStartDate ? ` since ${planStartDate.toLocaleDateString("en-US",{month:"short",day:"numeric"})}` : ""}
                </div>
                <div style={{fontSize:10,color:C.textMuted,marginBottom:14,fontStyle:"italic",lineHeight:1.5}}>
                  Aerobic fitness shows up as the trend climbing — same HR producing faster pace, or same pace at lower HR. Pace-selection alone (running slower for base) doesn't move EF; only fitness does. Speed is grade-adjusted so treadmill incline and hilly courses don't show as efficiency loss.
                </div>
                {easyRunEfficiency.length < 3 ? (
                  <EmptyState icon="❤️" title="Not enough easy runs" sub="Need at least 3 Z1-Z2 runs with HR data" />
                ) : (
                  <ResponsiveContainer width="100%" height={220}>
                    {(() => {
                      const slope = easyRunEfficiency[easyRunEfficiency.length - 1]?.trendSlope || 0;
                      const trendColor = slope > 0.015 ? C.green : slope < -0.015 ? C.red : C.textMuted;
                      return (
                        <ComposedChart data={easyRunEfficiency}>
                          <CartesianGrid strokeDasharray="3 3" stroke={C.border} vertical={false} />
                          <XAxis dataKey="date" tick={{fill:C.textMuted,fontSize:10}} axisLine={false} tickLine={false} />
                          <YAxis yAxisId="ef" tick={{fill:C.textMuted,fontSize:11}} axisLine={false} tickLine={false}
                            tickFormatter={v => v.toFixed(1)} domain={["auto","auto"]} />
                          <Tooltip content={<ChartTooltip
                            formatter={(v,n) => n==="EF Trend" ? `EF ${v} · trend` : `EF ${v}`}
                          />} />
                          <Legend wrapperStyle={{fontSize:12,color:C.textMuted}} />
                          <Line yAxisId="ef" type="monotone" dataKey="ef" stroke={C.amber} strokeWidth={1.5}
                            dot={{fill:C.amber,r:3}} name="Efficiency" connectNulls />
                          <Line yAxisId="ef" type="linear" dataKey="efTrend" stroke={trendColor} strokeWidth={2}
                            strokeDasharray="6 3" dot={false} name="EF Trend" connectNulls />
                          {/* When heat is on, show the unadjusted trend faintly so
                              the gap between the two lines IS the heat correction —
                              makes the toggle visibly do something even in a cool block. */}
                          {efHeatAdjusted && (
                            <Line yAxisId="ef" type="linear" dataKey="efRefTrend" stroke={C.textMuted} strokeWidth={1.5}
                              strokeDasharray="2 4" dot={false} name="Unadjusted trend" connectNulls />
                          )}
                        </ComposedChart>
                      );
                    })()}
                  </ResponsiveContainer>
                )}
              </Card>

              {/* Tempo / threshold pace progression — the "faster stuff" */}
              <Card>
                <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:4}}>
                  <div style={{fontSize:13,fontWeight:600,color:C.textDim}}>Tempo / Threshold Pace Progression</div>
                  {thresholdPaceProgression.length >= 2 && (() => {
                    const slope = thresholdPaceProgression[thresholdPaceProgression.length - 1].trendSlope;
                    // Pace noise ~0.05/run; ±0.005/run (~6s/mi over 20 runs)
                    // is a meaningful shift. Lower pace = faster = improving.
                    const improving = slope < -0.005;
                    const declining = slope > 0.005;
                    return (
                      <div style={{fontSize:11,fontWeight:600,color: improving ? C.green : declining ? C.red : C.textMuted,
                        padding:"2px 8px",borderRadius:4,background: improving ? C.green+"15" : declining ? C.red+"15" : C.bg3}}>
                        {improving ? "↑ Improving" : declining ? "↓ Declining" : "→ Stable"}
                      </div>
                    );
                  })()}
                </div>
                <div style={{fontSize:11,color:C.textMuted,marginBottom:4}}>
                  Tempo & threshold runs · avg pace · faster at the same kind of session = fitter · {thresholdPaceProgression.length} run{thresholdPaceProgression.length === 1 ? "" : "s"}{planStartDate ? ` since ${planStartDate.toLocaleDateString("en-US",{month:"short",day:"numeric"})}` : ""}
                </div>
                <div style={{fontSize:10,color:C.textMuted,marginBottom:14,fontStyle:"italic",lineHeight:1.5}}>
                  Sustained quality only — rep-intervals, track and fartlek are excluded because their average pace (recovery folded in) isn't a real effort pace. A falling trend means you're holding faster paces at the same threshold effort.
                </div>
                {thresholdPaceProgression.length < 3 ? (
                  <EmptyState icon="🔥" title="Not enough tempo/threshold runs" sub="Need at least 3 sustained tempo or threshold runs" />
                ) : (
                  <ResponsiveContainer width="100%" height={220}>
                    {(() => {
                      const slope = thresholdPaceProgression[thresholdPaceProgression.length - 1]?.trendSlope || 0;
                      const trendColor = slope < -0.005 ? C.green : slope > 0.005 ? C.red : C.textMuted;
                      const paceFmt = v => `${Math.floor(v)}:${Math.round((v%1)*60).toString().padStart(2,"0")}`;
                      return (
                        <ComposedChart data={thresholdPaceProgression}>
                          <CartesianGrid strokeDasharray="3 3" stroke={C.border} vertical={false} />
                          <XAxis dataKey="date" tick={{fill:C.textMuted,fontSize:10}} axisLine={false} tickLine={false} />
                          <YAxis yAxisId="pace" reversed tick={{fill:C.textMuted,fontSize:11}} axisLine={false} tickLine={false}
                            tickFormatter={paceFmt} domain={["auto","auto"]} />
                          <Tooltip content={<ChartTooltip
                            formatter={(v,n) => n==="Pace Trend" ? `${paceFmt(v)} /mi · trend` : `${paceFmt(v)} /mi`}
                          />} />
                          <Legend wrapperStyle={{fontSize:12,color:C.textMuted}} />
                          <Line yAxisId="pace" type="monotone" dataKey="pace" stroke={C.amber} strokeWidth={1.5}
                            dot={{fill:C.amber,r:3}} name="Pace" connectNulls />
                          <Line yAxisId="pace" type="linear" dataKey="paceTrend" stroke={trendColor} strokeWidth={2}
                            strokeDasharray="6 3" dot={false} name="Pace Trend" connectNulls />
                        </ComposedChart>
                      );
                    })()}
                  </ResponsiveContainer>
                )}
              </Card>
            </div>
          )}

          {tab === "HR Zones" && (
            <div style={{display:"flex",flexDirection:"column",gap:16}}>
              {/* Plan-period HR zones */}
              {(() => {
                const sinceLabel = planStartDate
                  ? planStartDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
                  : "all time";
                const renderZones = (zones) => (
                  <div style={{display:"flex",flexDirection:"column",gap:10}}>
                    {zones.map(z => (
                      <div key={z.name} style={{display:"flex",alignItems:"center",gap:10}}>
                        <div style={{width:120,fontSize:12,color:C.textDim,flexShrink:0}}>{z.name}</div>
                        <div style={{width:80,fontSize:11,color:z.color,fontFamily:"'IBM Plex Mono',monospace",flexShrink:0,textAlign:"center"}}>
                          {z.minBPM > 0 ? z.minBPM : "<" + z.maxBPM}–{z.maxBPM}
                        </div>
                        <div style={{flex:1,background:C.bg3,borderRadius:4,height:20,overflow:"hidden"}}>
                          <div style={{width:`${z.pct}%`,height:"100%",background:z.color,borderRadius:4,transition:"width 0.5s"}}/>
                        </div>
                        <div style={{width:50,fontSize:12,color:z.color,textAlign:"right",fontWeight:600}}>{z.pct}%</div>
                        <div style={{width:50,fontSize:11,color:C.textMuted,textAlign:"right"}}>{z.hours}h</div>
                      </div>
                    ))}
                  </div>
                );
                return (
                  <>
                    <Card>
                      <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}}>
                        <div>
                          <div style={{fontSize:13,fontWeight:600,color:C.textDim}}>Heart Rate Zone Distribution</div>
                          <div style={{fontSize:11,color:C.textMuted}}>Since {sinceLabel}</div>
                        </div>
                        <div style={{fontSize:11,color:C.textMuted}}>Max HR: <span style={{color:C.text,fontWeight:600}}>{getHRZones(hrZoneConfig, allSorted).maxHR} bpm</span></div>
                      </div>
                      {renderZones(hrZones)}
                    </Card>
                    <Card>
                      <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:4}}>Historical — All Time</div>
                      <div style={{fontSize:11,color:C.textMuted,marginBottom:16}}>All recorded activities</div>
                      {renderZones(hrZonesAll)}
                    </Card>
                  </>
                );
              })()}
            </div>
          )}

          {tab === "Distribution" && (
            <div style={{ display:"flex", flexDirection:"column", gap: 16 }}>
              {!trainingDist ? (
                <Card><EmptyState icon="📊" title="Not enough data" sub="Need activities with heart rate data since the start of your plan" /></Card>
              ) : (
                <>
                  {/* Training model badge */}
                  <Card>
                    <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:20}}>
                      <div>
                        <div style={{fontSize:13,fontWeight:600,color:C.textDim}}>Training Intensity Distribution</div>
                        <div style={{fontSize:11,color:C.textMuted}}>
                          {planStartDate ? `Since ${planStartDate.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}` : "Last 30 days"} · {trainingDist.totalHours} hours total
                          {(trainingDist.zoneDataCount > 0 || trainingDist.sliceDataCount > 0 || trainingDist.avgFallbackCount > 0) && (
                            <span style={{marginLeft:6,color:C.textMuted+"99"}}>
                              · {trainingDist.zoneDataCount} time-in-zone{trainingDist.sliceDataCount > 0 ? `, ${trainingDist.sliceDataCount} from intervals` : ""}, {trainingDist.avgFallbackCount} avg-HR
                            </span>
                          )}
                        </div>
                      </div>
                      <div style={{display:"flex",gap:8,alignItems:"center"}}>
                      {isOwner && (
                        <button onClick={async (e) => {
                          const btn = e.currentTarget;
                          try {
                            btn.textContent = "Recomputing…";
                            btn.disabled = true;
                            // Cheap path: re-slice cached laps/streams against
                            // the current MP — no Strava calls. Drains the
                            // backlog so older runs' reps stop reading as easy.
                            const fn = functions.httpsCallable("backfillWorkoutSlices");
                            // forceRecompute so treadmill runs whose slices were
                            // cached before belt-calibration get corrected too.
                            const res = await fn({ limit: 200, forceRecompute: true });
                            if (res.data?.error === "no-mp") { btn.textContent = "Snapshot not ready"; }
                            else {
                              btn.textContent = `Re-sliced ${res.data?.reSliced ?? 0}`;
                              setDistSlices({}); // drop cache so the chart re-reads fresh slices
                            }
                          } catch (err) { btn.textContent = "Error"; }
                          setTimeout(() => { btn.textContent = "↺ Recompute slices"; btn.disabled = false; }, 3500);
                        }} title="Re-slice cached workout data so structured sessions are read by their reps, not their whole-run average. No Strava calls."
                          style={{padding:"6px 12px",borderRadius:6,border:`1px solid ${C.border}`,cursor:"pointer",
                            background:"transparent",color:C.textDim,fontSize:10,fontWeight:600,
                            fontFamily:"'IBM Plex Mono',monospace",whiteSpace:"nowrap"}}>
                          ↺ Recompute slices
                        </button>
                      )}
                      <div style={{padding:"8px 16px",borderRadius:8,background:
                        trainingDist.model==="Polarized" ? C.green+"20" :
                        trainingDist.model==="Pyramidal" ? C.cyan+"20" :
                        trainingDist.model==="Threshold" ? C.amber+"20" : C.bg3,
                        border:`1px solid ${
                          trainingDist.model==="Polarized" ? C.green :
                          trainingDist.model==="Pyramidal" ? C.cyan :
                          trainingDist.model==="Threshold" ? C.amber : C.border}`,
                      }}>
                        <div style={{fontSize:14,fontWeight:700,color:
                          trainingDist.model==="Polarized" ? C.green :
                          trainingDist.model==="Pyramidal" ? C.cyan :
                          trainingDist.model==="Threshold" ? C.amber : C.textDim
                        }}>{trainingDist.model}</div>
                        <div style={{fontSize:10,color:C.textMuted}}>training model</div>
                      </div>
                      </div>
                    </div>
                    {/* 3-zone breakdown */}
                    <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:16,marginBottom:20}}>
                      <div style={{textAlign:"center",padding:16,background:C.bg2,borderRadius:10,borderTop:`3px solid ${C.green}`}}>
                        <div style={{fontSize:28,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",color:C.green}}>{trainingDist.lowPctLabel}%</div>
                        <div style={{fontSize:11,color:C.textMuted,marginTop:4}}>Low Intensity</div>
                        <div style={{fontSize:10,color:C.textMuted}}>{trainingDist.lowLabel}</div>
                      </div>
                      <div style={{textAlign:"center",padding:16,background:C.bg2,borderRadius:10,borderTop:`3px solid ${trainingDist.isPolarized ? C.red : C.amber}`}}>
                        <div style={{fontSize:28,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",color: trainingDist.isPolarized ? C.red : C.amber}}>{trainingDist.midPctLabel}%</div>
                        <div style={{fontSize:11,color:C.textMuted,marginTop:4}}>{trainingDist.isPolarized ? "Grey Zone" : "Mid Intensity"}</div>
                        <div style={{fontSize:10,color:C.textMuted}}>{trainingDist.midLabel}</div>
                      </div>
                      <div style={{textAlign:"center",padding:16,background:C.bg2,borderRadius:10,borderTop:`3px solid ${C.red}`}}>
                        <div style={{fontSize:28,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",color:C.red}}>{trainingDist.highPctLabel}%</div>
                        <div style={{fontSize:11,color:C.textMuted,marginTop:4}}>High Intensity</div>
                        <div style={{fontSize:10,color:C.textMuted}}>{trainingDist.highLabel}</div>
                      </div>
                    </div>
                    {/* 5-zone detail — labels come from the active HR zone config,
                        so polarized users see "Easy/Aerobic / Grey Zone / Threshold / VO2max / Neuromuscular"
                        instead of generic Coggan names. */}
                    <div style={{display:"flex",flexDirection:"column",gap:8}}>
                      {[
                        {name:`Z1 ${trainingDist.zoneNames[0] || "Recovery"}`,raw:trainingDist.zoneRaw.z1,label:trainingDist.zoneLabels.z1,color:"#64748b"},
                        {name:`Z2 ${trainingDist.zoneNames[1] || "Aerobic"}`,raw:trainingDist.zoneRaw.z2,label:trainingDist.zoneLabels.z2,color:trainingDist.isPolarized ? C.red : C.green},
                        {name:`Z3 ${trainingDist.zoneNames[2] || "Tempo"}`,raw:trainingDist.zoneRaw.z3,label:trainingDist.zoneLabels.z3,color:C.amber},
                        {name:`Z4 ${trainingDist.zoneNames[3] || "Threshold"}`,raw:trainingDist.zoneRaw.z4,label:trainingDist.zoneLabels.z4,color:"#f97316"},
                        {name:`Z5 ${trainingDist.zoneNames[4] || "VO2max"}`,raw:trainingDist.zoneRaw.z5,label:trainingDist.zoneLabels.z5,color:C.red},
                      ].map(z => (
                        <div key={z.name} style={{display:"flex",alignItems:"center",gap:12}}>
                          <div style={{width:110,fontSize:12,color:C.textDim,flexShrink:0}}>{z.name}</div>
                          <div style={{flex:1,background:C.bg3,borderRadius:4,height:16,overflow:"hidden"}}>
                            {/* A sub-1% zone still gets a 2% sliver so it reads as present, not empty. */}
                            <div style={{width:`${z.raw > 0 ? Math.max(z.raw, 2) : 0}%`,height:"100%",background:z.color,borderRadius:4,transition:"width 0.5s"}}/>
                          </div>
                          <div style={{width:40,fontSize:12,color:z.color,textAlign:"right"}}>{z.label}%</div>
                        </div>
                      ))}
                    </div>
                  </Card>
                  {/* Recommendation */}
                  <Card style={{borderColor:C.cyan+"40"}}>
                    <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:8}}>Training Model Guide</div>
                    <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(200px,1fr))",gap:12}}>
                      {[
                        {model:"Polarized",desc:"80/0/20 split. Most time easy, rest hard. Proven most effective for endurance.",color:C.green,ideal:"80%+ low, 15-20% high"},
                        {model:"Pyramidal",desc:"75/15/10 split. Most time easy, moderate middle work, least hard.",color:C.cyan,ideal:"75% low, 15% mid, 10% high"},
                        {model:"Threshold",desc:"Heavy tempo/threshold focus. Risk of overtraining. Best in short blocks.",color:C.amber,ideal:"50-65% low, 25%+ mid"},
                      ].map(m => (
                        <div key={m.model} style={{padding:"12px 16px",background: trainingDist.model===m.model ? m.color+"15" : C.bg2,
                          borderRadius:8,border:`1px solid ${trainingDist.model===m.model ? m.color : C.border}`}}>
                          <div style={{fontSize:13,fontWeight:700,color:m.color,marginBottom:4}}>{m.model}</div>
                          <div style={{fontSize:11,color:C.textDim,lineHeight:1.5,marginBottom:6}}>{m.desc}</div>
                          <div style={{fontSize:10,color:C.textMuted}}>Target: {m.ideal}</div>
                        </div>
                      ))}
                    </div>
                  </Card>
                </>
              )}
            </div>
          )}

          {tab === "PRs" && (
            <div style={{ display:"flex", flexDirection:"column", gap: 16 }}>
              {/* Streaks */}
              <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(140px,1fr))",gap:16}}>
                <Card>
                  <Stat label="Current Streak" value={streaks.current} unit="days" color={streaks.current >= 7 ? C.green : C.cyan} size={28} />
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>consecutive active days</div>
                </Card>
                <Card>
                  <Stat label="Longest Streak" value={streaks.longest} unit="days" color={C.amber} size={28} />
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>all-time record</div>
                </Card>
                <Card>
                  <Stat label="Active Days" value={streaks.thisYear} unit="" color={C.purple} size={28} />
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>this year ({(streaks.thisYear / ((new Date().getMonth()+1)*30.4)*100).toFixed(0)}%)</div>
                </Card>
                <Card>
                  <Stat label="Total Active" value={streaks.totalDays} unit="days" color={C.textDim} size={28} />
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>all-time</div>
                </Card>
              </div>

              {/* ── Race PRs (All-Time PRs from Strava) ── */}
              {(() => {
                const racePRs = manualPRs.racePRs || [];
                const bestEfforts = manualPRs.bestEfforts || [];

                // Parse time string "H:MM:SS" or "M:SS" or "M:SS" to seconds
                const parseTime = (s) => {
                  if (!s) return 0;
                  const parts = String(s).split(":").map(Number);
                  if (parts.length === 3) return parts[0]*3600 + parts[1]*60 + parts[2];
                  if (parts.length === 2) return parts[0]*60 + parts[1];
                  return Number(s) || 0;
                };

                // Distance name to meters for pace calc
                const distMeters = {
                  "400m":400, "1/2 mile":804.67, "1K":1000, "1 mile":1609.34, "2 miles":3218.69,
                  "5k":5000, "10k":10000, "15k":15000, "10 miles":16093.4, "20k":20000,
                  "Half-Marathon":21097, "30k":30000, "Marathon":42195,
                };

                // Also merge auto-detected best efforts from Strava
                const autoEfforts = {};
                activities.forEach(act => {
                  if (!act.best_efforts) return;
                  act.best_efforts.forEach(e => {
                    if (!e.pr_rank || e.pr_rank > 3) return;
                    const key = e.name;
                    if (!autoEfforts[key] || e.moving_time < autoEfforts[key].moving_time) {
                      autoEfforts[key] = { ...e, activityName: act.name, activityDate: act.start_date, isRace: act.workout_type === 1, source: "strava" };
                    }
                  });
                });

                const runsWithEfforts = activities.filter(a => isRun(a) && a.best_efforts && a.best_efforts.length > 0).length;
                const totalRuns = activities.filter(a => isRun(a)).length;
                const needsBackfill = runsWithEfforts < totalRuns * 0.5;

                // Add PR form
                const PRAddForm = ({ type }) => (
                  showAddPR === type ? (
                    <div style={{marginTop:12,padding:"12px",background:C.bg2,borderRadius:8,border:`1px solid ${C.border}`}}>
                      <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8}}>
                        <div>
                          <div style={{fontSize:10,color:C.textMuted,marginBottom:3}}>Distance</div>
                          <select value={prForm.distance} onChange={e => setPrForm({...prForm, distance:e.target.value})}
                            style={{width:"100%",padding:"6px 8px",fontSize:11}}>
                            <option value="">Select...</option>
                            {Object.keys(distMeters).map(d => <option key={d} value={d}>{d}</option>)}
                          </select>
                        </div>
                        <div>
                          <div style={{fontSize:10,color:C.textMuted,marginBottom:3}}>Time</div>
                          <input type="text" placeholder="e.g. 19:03 or 3:10:40" value={prForm.time}
                            onChange={e => setPrForm({...prForm, time:e.target.value})}
                            style={{width:"100%",padding:"6px 8px",fontSize:11}} />
                        </div>
                        <div>
                          <div style={{fontSize:10,color:C.textMuted,marginBottom:3}}>Date (optional)</div>
                          <input type="date" value={prForm.date} onChange={e => setPrForm({...prForm, date:e.target.value})}
                            style={{width:"100%",padding:"6px 8px",fontSize:11}} />
                        </div>
                        <div>
                          <div style={{fontSize:10,color:C.textMuted,marginBottom:3}}>{type === "race" ? "Race Name" : "Activity"} (optional)</div>
                          <input type="text" placeholder="e.g. Dublin Marathon" value={prForm.activityName}
                            onChange={e => setPrForm({...prForm, activityName:e.target.value})}
                            style={{width:"100%",padding:"6px 8px",fontSize:11}} />
                        </div>
                      </div>
                      <div style={{display:"flex",gap:8,marginTop:10}}>
                        <Btn onClick={() => savePR(type)}>Save PR</Btn>
                        <Btn onClick={() => setShowAddPR(null)} variant="ghost">Cancel</Btn>
                      </div>
                    </div>
                  ) : null
                );

                return (
                  <React.Fragment>
                    {/* Race PRs */}
                    <Card>
                      <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}}>
                        <div style={{fontSize:13,fontWeight:600,color:C.amber}}>All-Time PRs</div>
                        {isOwner && <button onClick={() => { setShowAddPR(showAddPR === "race" ? null : "race"); setPrForm({distance:"",time:"",date:"",activityName:""}); }}
                          style={{padding:"4px 10px",borderRadius:5,border:`1px solid ${C.amber}40`,cursor:"pointer",
                            background:"transparent",color:C.amber,fontSize:10,fontFamily:"'IBM Plex Mono',monospace"}}>
                          {showAddPR === "race" ? "Cancel" : "Add PR"}
                        </button>}
                      </div>
                      {racePRs.length === 0 ? (
                        <div style={{fontSize:12,color:C.textMuted,padding:"8px 0"}}>No race PRs added yet.</div>
                      ) : (
                        <div>
                          <div style={{display:"grid",gridTemplateColumns:"110px 80px 80px 1fr 30px",gap:8,padding:"0 0 8px",
                            fontSize:10,color:C.textMuted,textTransform:"uppercase",borderBottom:`1px solid ${C.border}`}}>
                            <span>Distance</span><span>Time</span><span>Pace</span><span>Race</span><span></span>
                          </div>
                          {racePRs.map((pr, i) => {
                            const secs = parseTime(pr.time);
                            const meters = distMeters[pr.distance] || 0;
                            const paceSec = meters > 0 && secs > 0 ? secs / (meters / 1609.34) : 0;
                            return (
                              <div key={i} style={{display:"grid",gridTemplateColumns:"110px 80px 80px 1fr 30px",gap:8,alignItems:"center",
                                padding:"10px 0",borderBottom:`1px solid ${C.border}`}}>
                                <div style={{fontSize:12,fontWeight:700,color:C.amber}}>{pr.distance}</div>
                                <div style={{fontSize:15,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",color:C.text}}>{pr.time}</div>
                                <div style={{fontSize:11,color:C.cyan}}>{paceSec > 0 ? fmt.pace(paceSec).split(" ")[0] : ""}</div>
                                <div style={{fontSize:11,color:C.textMuted}}>
                                  {pr.activityName || ""}{pr.date ? (pr.activityName ? " · " : "") + pr.date : ""}
                                </div>
                                {isOwner && <button onClick={() => deletePR("race", i)} style={{background:"transparent",border:"none",
                                  color:C.red+"80",cursor:"pointer",fontSize:12,padding:2}}>x</button>}
                              </div>
                            );
                          })}
                        </div>
                      )}
                      <PRAddForm type="race" />
                    </Card>

                    {/* Best Efforts */}
                    <Card>
                      <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}}>
                        <div style={{fontSize:13,fontWeight:600,color:C.cyan}}>Best Efforts</div>
                        {isOwner && <div style={{display:"flex",gap:8,alignItems:"center"}}>
                          {needsBackfill && (
                            <button onClick={async (e) => {
                              try {
                                e.target.textContent = "Pulling...";
                                e.target.disabled = true;
                                const fn = functions.httpsCallable("backfillBestEfforts");
                                const result = await fn({ limit: 50 });
                                e.target.textContent = `Updated ${result.data.updated}`;
                                setTimeout(() => { e.target.textContent = "Sync Strava"; e.target.disabled = false; }, 3000);
                              } catch (err) { e.target.textContent = "Error"; }
                            }} style={{padding:"4px 10px",borderRadius:5,border:`1px solid ${C.cyan}40`,cursor:"pointer",
                              background:"transparent",color:C.cyan,fontSize:10,fontFamily:"'IBM Plex Mono',monospace"}}>
                              Sync Strava
                            </button>
                          )}
                          <button onClick={() => { setShowAddPR(showAddPR === "effort" ? null : "effort"); setPrForm({distance:"",time:"",date:"",activityName:""}); }}
                            style={{padding:"4px 10px",borderRadius:5,border:`1px solid ${C.cyan}40`,cursor:"pointer",
                              background:"transparent",color:C.cyan,fontSize:10,fontFamily:"'IBM Plex Mono',monospace"}}>
                            {showAddPR === "effort" ? "Cancel" : "Add PR"}
                          </button>
                        </div>}
                      </div>
                      {bestEfforts.length === 0 && Object.keys(autoEfforts).length === 0 ? (
                        <div style={{fontSize:12,color:C.textMuted,padding:"8px 0"}}>No best efforts yet. Add manually or sync from Strava.</div>
                      ) : (() => {
                        // Merge manual + auto: manual takes priority for same distance
                        const merged = {};
                        // Auto-detected first
                        Object.entries(autoEfforts).forEach(([name, e]) => {
                          merged[name] = { distance: name, time: fmt.duration(e.moving_time), source: "strava",
                            activityName: e.activityName || "", date: e.activityDate ? fmt.date(e.activityDate) : "" };
                        });
                        // Manual overrides (if faster)
                        bestEfforts.forEach((pr, idx) => {
                          const key = pr.distance;
                          const manualSecs = parseTime(pr.time);
                          const autoEntry = merged[key];
                          const autoSecs = autoEntry ? parseTime(autoEntry.time) : Infinity;
                          if (manualSecs < autoSecs || !autoEntry) {
                            merged[key] = { ...pr, source: "manual", idx };
                          }
                        });
                        // Sort by distance
                        const distOrder = Object.keys(distMeters);
                        const sorted = Object.entries(merged).sort((a,b) => {
                          const ai = distOrder.indexOf(a[0]); const bi = distOrder.indexOf(b[0]);
                          return (ai===-1?99:ai) - (bi===-1?99:bi);
                        });
                        return (
                          <div>
                            <div style={{display:"grid",gridTemplateColumns:"110px 80px 80px 1fr 30px",gap:8,padding:"0 0 8px",
                              fontSize:10,color:C.textMuted,textTransform:"uppercase",borderBottom:`1px solid ${C.border}`}}>
                              <span>Distance</span><span>Time</span><span>Pace</span><span>Activity</span><span></span>
                            </div>
                            {sorted.map(([name, pr]) => {
                              const secs = parseTime(pr.time);
                              const meters = distMeters[name] || 0;
                              const paceSec = meters > 0 && secs > 0 ? secs / (meters / 1609.34) : 0;
                              return (
                                <div key={name} style={{display:"grid",gridTemplateColumns:"110px 80px 80px 1fr 30px",gap:8,alignItems:"center",
                                  padding:"10px 0",borderBottom:`1px solid ${C.border}`}}>
                                  <div style={{fontSize:12,fontWeight:700,color:C.cyan}}>{name}</div>
                                  <div style={{fontSize:15,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",color:C.text}}>{pr.time}</div>
                                  <div style={{fontSize:11,color:C.amber}}>{paceSec > 0 ? fmt.pace(paceSec).split(" ")[0] : ""}</div>
                                  <div style={{fontSize:11,color:C.textMuted}}>
                                    {pr.activityName || ""}{pr.date ? (pr.activityName ? " · " : "") + pr.date : ""}
                                    {pr.source === "strava" && <span style={{marginLeft:6,padding:"1px 5px",borderRadius:4,background:C.cyan+"15",color:C.cyan,fontSize:9}}>STRAVA</span>}
                                  </div>
                                  {pr.source === "manual" ? (
                                    <button onClick={() => deletePR("effort", pr.idx)} style={{background:"transparent",border:"none",
                                      color:C.red+"80",cursor:"pointer",fontSize:12,padding:2}}>x</button>
                                  ) : <span></span>}
                                </div>
                              );
                            })}
                          </div>
                        );
                      })()}
                      <PRAddForm type="effort" />
                    </Card>
                  </React.Fragment>
                );
              })()}

              {/* VO2max Confidence */}
              {vo2Confidence && (
                <Card>
                  <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:16}}>VO2max Confidence Band</div>
                  <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(120px,1fr))",gap:16}}>
                    <div style={{textAlign:"center"}}>
                      <div style={{fontSize:11,color:C.textMuted}}>Lower (95%)</div>
                      <div style={{fontSize:22,fontWeight:800,color:C.textDim,fontFamily:"'Space Grotesk',sans-serif"}}>{vo2Confidence.lower}</div>
                    </div>
                    <div style={{textAlign:"center"}}>
                      <div style={{fontSize:11,color:C.cyan}}>Mean VO2max</div>
                      <div style={{fontSize:28,fontWeight:800,color:C.cyan,fontFamily:"'Space Grotesk',sans-serif"}}>{vo2Confidence.mean}</div>
                    </div>
                    <div style={{textAlign:"center"}}>
                      <div style={{fontSize:11,color:C.textMuted}}>Upper (95%)</div>
                      <div style={{fontSize:22,fontWeight:800,color:C.textDim,fontFamily:"'Space Grotesk',sans-serif"}}>{vo2Confidence.upper}</div>
                    </div>
                  </div>
                  <div style={{marginTop:12,fontSize:11,color:C.textMuted,textAlign:"center"}}>
                    Based on {vo2Confidence.samples} qualifying runs · σ = {vo2Confidence.sd}
                  </div>
                  {/* Visual confidence bar */}
                  <div style={{marginTop:16,position:"relative",height:40,background:C.bg2,borderRadius:8,overflow:"hidden"}}>
                    {(() => {
                      const range = vo2Confidence.upper - vo2Confidence.lower;
                      const effectivePct = effectiveVO2 ? ((effectiveVO2 - vo2Confidence.lower) / range * 100) : 50;
                      return (
                        <>
                          <div style={{position:"absolute",left:"10%",right:"10%",top:0,bottom:0,background:C.cyan+"15",borderRadius:8}} />
                          <div style={{position:"absolute",left:`${Math.max(5,Math.min(95,effectivePct))}%`,top:4,bottom:4,width:3,background:C.cyan,borderRadius:2}} />
                          <div style={{position:"absolute",left:`${Math.max(5,Math.min(95,effectivePct))-2}%`,top:-2,fontSize:10,color:C.cyan,fontWeight:700}}>
                            {effectiveVO2 ? (+effectiveVO2).toFixed(1) : vo2Confidence.mean}
                          </div>
                        </>
                      );
                    })()}
                  </div>
                </Card>
              )}
            </div>
          )}

          {tab === "Progression" && (
            <div style={{ display:"flex", flexDirection:"column", gap: 16 }}>
              {/* Trend badge */}
              <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:16}}>
                <Card>
                  <Stat label="Trend" value={progression.trend} unit="" color={
                    progression.trend==="Building" ? C.green : progression.trend==="Tapering" ? C.amber : C.textDim
                  } size={22} />
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>
                    {progression.slope > 0 ? "+" : ""}{progression.slope} mi/week slope
                  </div>
                </Card>
                <Card>
                  <Stat label="Peak Week" value={allTimePeak ? allTimePeak.miles : "--"} unit="mi" color={C.cyan} size={22} />
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>
                    {allTimePeak ? `${allTimePeak.dateLabel} · ${allTimePeak.runs} runs` : "all time"}
                  </div>
                </Card>
                <Card>
                  <Stat label="Avg Week" value={(progression.weeklyData.reduce((s,w)=>s+w.miles,0)/progression.weeklyData.length).toFixed(1)} unit="mi" color={C.purple} size={22} />
                  <div style={{fontSize:11,color:C.textMuted,marginTop:6}}>last 12 weeks</div>
                </Card>
              </div>

              {/* Weekly mileage progression chart */}
              <Card>
                <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:4}}>Weekly Volume — 12 Week Progression</div>
                <div style={{fontSize:11,color:C.textMuted,marginBottom:16}}>Running miles + cross-training hours</div>
                <ResponsiveContainer width="100%" height={260}>
                  <ComposedChart data={progression.weeklyData}>
                    <CartesianGrid strokeDasharray="3 3" stroke={C.border} vertical={false} />
                    <XAxis dataKey="weekLabel" tick={{fill:C.textMuted,fontSize:10}} axisLine={false} tickLine={false} />
                    <YAxis yAxisId="miles" tick={{fill:C.textMuted,fontSize:11}} axisLine={false} tickLine={false} />
                    <YAxis yAxisId="trimp" orientation="right" tick={{fill:C.textMuted,fontSize:11}} axisLine={false} tickLine={false} />
                    <Tooltip content={<ChartTooltip />} />
                    <Legend wrapperStyle={{fontSize:12,color:C.textMuted}} />
                    <Bar yAxisId="miles" dataKey="miles" fill={C.cyan} fillOpacity={0.7} name="Run Miles" barSize={24} radius={[4,4,0,0]} />
                    <Bar yAxisId="miles" dataKey="xtHours" fill={C.green} fillOpacity={0.5} name="XT Hours" barSize={24} radius={[4,4,0,0]} />
                    <Line yAxisId="trimp" type="monotone" dataKey="trimp" stroke={C.amber} strokeWidth={2} dot={false} name="TRIMP" />
                  </ComposedChart>
                </ResponsiveContainer>
              </Card>

              {/* Weekly detail table */}
              <Card>
                <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:16}}>Weekly Breakdown</div>
                <div style={{overflowX:"auto"}}>
                  <table style={{width:"100%",borderCollapse:"collapse",fontSize:12}}>
                    <thead>
                      <tr style={{borderBottom:`1px solid ${C.border}`}}>
                        {["Week","Miles","Runs","XT Hrs","TRIMP"].map(h => (
                          <th key={h} style={{padding:"8px 12px",textAlign:"right",color:C.textMuted,fontWeight:600,fontSize:11}}>
                            {h}
                          </th>
                        ))}
                      </tr>
                    </thead>
                    <tbody>
                      {progression.weeklyData.map((w, i) => (
                        <tr key={i} style={{borderBottom:`1px solid ${C.border}22`}}>
                          <td style={{padding:"8px 12px",color:C.textDim,textAlign:"right"}}>{w.weekLabel}</td>
                          <td style={{padding:"8px 12px",color:C.cyan,textAlign:"right",fontWeight:600}}>{w.miles}</td>
                          <td style={{padding:"8px 12px",color:C.textMuted,textAlign:"right"}}>{w.runCount}</td>
                          <td style={{padding:"8px 12px",color:C.green,textAlign:"right"}}>{w.xtHours}</td>
                          <td style={{padding:"8px 12px",color:C.amber,textAlign:"right"}}>{w.trimp}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              </Card>

              {/* Year-Over-Year Comparison */}
              {yoy && (
                <Card>
                  <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:4}}>Year-Over-Year — Rolling 4 Weeks</div>
                  <div style={{fontSize:11,color:C.textMuted,marginBottom:16}}>Same 4-week window compared to this time last year</div>
                  <div style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:16}}>
                    <div style={{textAlign:"center"}}>
                      <div style={{fontSize:11,color:C.textMuted,marginBottom:4}}>This Year</div>
                      <div style={{fontSize:26,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",color:C.cyan}}>{yoy.currentMiles}</div>
                      <div style={{fontSize:11,color:C.textMuted}}>mi · {yoy.currentRuns} runs</div>
                    </div>
                    <div style={{textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"}}>
                      {yoy.diffPct !== null && (
                        <>
                          <div style={{fontSize:22,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",
                            color: yoy.diffPct > 0 ? C.green : yoy.diffPct < -10 ? C.red : C.amber}}>
                            {yoy.diffPct > 0 ? "+" : ""}{yoy.diffPct}%
                          </div>
                          <div style={{fontSize:11,color:C.textMuted}}>{yoy.diffPct > 0 ? "ahead of" : "behind"} last year</div>
                        </>
                      )}
                    </div>
                    <div style={{textAlign:"center"}}>
                      <div style={{fontSize:11,color:C.textMuted,marginBottom:4}}>Last Year</div>
                      <div style={{fontSize:26,fontWeight:800,fontFamily:"'Space Grotesk',sans-serif",color:C.textDim}}>{yoy.lastYearMiles}</div>
                      <div style={{fontSize:11,color:C.textMuted}}>mi · {yoy.lastYearRuns} runs</div>
                    </div>
                  </div>
                </Card>
              )}
            </div>
          )}

          {tab === "Race Calc" && (
            <div style={{ display:"flex", flexDirection:"column", gap: 16 }}>
              <Card>
                <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:4}}>Race Prediction Calculator</div>
                <div style={{fontSize:11,color:C.textMuted,marginBottom:16}}>Enter a recent race or time trial result to predict other distances</div>
                <div style={{display:"grid",gridTemplateColumns:"1fr 1fr auto",gap:12,alignItems:"end"}}>
                  <div>
                    <div style={{fontSize:11,color:C.textMuted,marginBottom:6}}>Distance</div>
                    <select value={raceCalcDist} onChange={e => setRaceCalcDist(e.target.value)}
                      style={{width:"100%",background:C.bg2,border:`1px solid ${C.border}`,borderRadius:6,
                        padding:"10px 12px",color:C.text,fontSize:13,fontFamily:"'IBM Plex Mono',monospace"}}>
                      <option value="">Select distance</option>
                      <option value="5K">5K</option>
                      <option value="10K">10K</option>
                      <option value="Half">Half Marathon</option>
                      <option value="Marathon">Marathon</option>
                    </select>
                  </div>
                  <div>
                    <div style={{fontSize:11,color:C.textMuted,marginBottom:6}}>Time (H:MM:SS or MM:SS)</div>
                    <input type="text" value={raceCalcTime} onChange={e => setRaceCalcTime(e.target.value)}
                      placeholder="e.g. 22:30 or 1:45:00"
                      style={{width:"100%",background:C.bg2,border:`1px solid ${C.border}`,borderRadius:6,
                        padding:"10px 12px",color:C.text,fontSize:13,fontFamily:"'IBM Plex Mono',monospace"}} />
                  </div>
                </div>
              </Card>

              {raceCalcPredictions && (
                <Card>
                  <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:14}}>Predicted Race Times</div>
                  <div style={{fontSize:11,color:C.textMuted,marginBottom:16}}>
                    Based on {raceCalcDist} in {raceCalcTime} · Uses Riegel formula with progressive fatigue
                  </div>
                  <div style={{display:"flex",flexDirection:"column",gap:0}}>
                    {raceCalcPredictions.map((rp, i) => (
                      <div key={rp.name} style={{
                        display:"grid",gridTemplateColumns:"130px 1fr 1fr",gap:8,alignItems:"center",
                        padding:"12px 0",borderBottom: i < raceCalcPredictions.length-1 ? `1px solid ${C.border}` : "none",
                      }}>
                        <div style={{fontSize:13,fontWeight:600,color:C.cyan}}>{rp.name}</div>
                        <div style={{fontSize:18,fontWeight:800,color:C.text,fontFamily:"'Space Grotesk',sans-serif"}}>
                          {fmt.duration(rp.time)}
                        </div>
                        <div style={{fontSize:12,color:C.amber}}>{fmt.pace(rp.pace)}</div>
                      </div>
                    ))}
                  </div>
                </Card>
              )}

            </div>
          )}

          {tab === "Cross-Training" && (
            <div style={{ display:"flex", flexDirection:"column", gap: 16 }}>
              {xtBreakdown.length === 0 ? (
                <Card><EmptyState icon="🏋️" title="No cross-training yet" sub="All non-running Strava activities appear here" /></Card>
              ) : (
                <>
                  {/* XT summary cards */}
                  <div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fit, minmax(160px, 1fr))", gap: 16 }}>
                    <Card>
                      <Stat label="Total XT Sessions" value={xtBreakdown.reduce((s,x) => s+x.count, 0)} unit="" color={C.green} />
                    </Card>
                    <Card>
                      <Stat label="Total XT Hours" value={xtBreakdown.reduce((s,x) => s+parseFloat(x.hours), 0).toFixed(1)} unit="hrs" color={C.green} />
                    </Card>
                    <Card>
                      <Stat label="XT Calories" value={xtBreakdown.reduce((s,x) => s+x.calories, 0).toLocaleString()} unit="kcal" color={C.amber} />
                    </Card>
                  </div>

                  {/* Sport breakdown */}
                  <Card>
                    <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:16}}>Cross-Training by Sport</div>
                    <div style={{display:"flex",flexDirection:"column",gap:12}}>
                      {xtBreakdown.map(x => {
                        const maxMins = Math.max(...xtBreakdown.map(b => b.minutes));
                        return (
                          <div key={x.cat} style={{display:"flex",alignItems:"center",gap:12}}>
                            <span style={{fontSize:18,width:28,textAlign:"center"}}>{x.emoji}</span>
                            <div style={{width:80,fontSize:12,color:C.textDim,flexShrink:0}}>{x.label}</div>
                            <div style={{flex:1,background:C.bg3,borderRadius:4,height:20,overflow:"hidden"}}>
                              <div style={{width:`${(x.minutes/maxMins)*100}%`,height:"100%",background:x.color,borderRadius:4,transition:"width 0.5s"}}/>
                            </div>
                            <div style={{width:60,fontSize:12,color:x.color,textAlign:"right"}}>{x.hours}h</div>
                            <div style={{width:40,fontSize:11,color:C.textMuted,textAlign:"right"}}>{x.count}×</div>
                          </div>
                        );
                      })}
                    </div>
                  </Card>

                  {/* Training load contribution */}
                  <Card>
                    <div style={{fontSize:13,fontWeight:600,color:C.textDim,marginBottom:4}}>Training Load Contribution</div>
                    <div style={{fontSize:11,color:C.textMuted,marginBottom:16}}>How cross-training feeds into your overall fitness score</div>
                    <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(200px,1fr))",gap:16}}>
                      {xtBreakdown.map(x => (
                        <div key={x.cat} style={{background:C.bg2,borderRadius:8,padding:"12px 16px",border:`1px solid ${C.border}`}}>
                          <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}>
                            <span style={{fontSize:16}}>{x.emoji}</span>
                            <span style={{fontSize:12,fontWeight:600,color:C.text}}>{x.label}</span>
                          </div>
                          <div style={{fontSize:11,color:C.textMuted}}>
                            TSS weight: <span style={{color:x.color,fontWeight:600}}>{((XT_TSS[x.cat]||0.5)*100).toFixed(0)}%</span> of running
                          </div>
                          <div style={{fontSize:11,color:C.textMuted,marginTop:4}}>
                            {x.count} sessions · {x.hours} hrs total
                          </div>
                        </div>
                      ))}
                    </div>
                  </Card>
                </>
              )}
            </div>
          )}

          {tab === "Weather" && (
            <div style={{ display:"flex", flexDirection:"column", gap:16 }}>
              {(!weatherImpact || weatherImpact.n < (window.WeatherStats ? window.WeatherStats.MIN_SAMPLE : 6)) ? (
                <Card>
                  <EmptyState icon="🌦️" title="Not enough weather-enriched runs yet"
                    sub={`Needs ≥ ${window.WeatherStats ? window.WeatherStats.MIN_SAMPLE : 6} easy (Z1) outdoor runs with HR + recorded weather. So far: ${weatherImpact ? weatherImpact.n : 0}.`} />
                  {isOwner && (
                    <div style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:8, marginTop:4 }}>
                      <button disabled={wxBackfill === "running"}
                        onClick={async () => {
                          setWxBackfill("running");
                          try {
                            const res = await functions.httpsCallable("backfillInsightWeather")({ limit: 2000 });
                            const d = res.data || {};
                            setWxBackfill(`Enriched ${d.updated||0}, skipped ${d.skipped||0} of ${d.total||0}. Reload to view.`);
                          } catch (e) { setWxBackfill(`Backfill failed: ${e.message}`); }
                        }}
                        style={{ padding:"8px 16px", borderRadius:6, border:`1px solid ${C.cyan}55`, cursor:"pointer",
                          background:C.cyan+"15", color:C.cyan, fontSize:12, fontFamily:"'IBM Plex Mono',monospace" }}>
                        {wxBackfill === "running" ? "Fetching weather history…" : "Backfill weather history"}
                      </button>
                      {wxBackfill && wxBackfill !== "running" && (
                        <span style={{ fontSize:11, color:C.textMuted }}>{wxBackfill}</span>
                      )}
                    </div>
                  )}
                </Card>
              ) : (() => {
                const WS = window.WeatherStats;
                const strongest = weatherImpact.strongest ? weatherImpact.vars[weatherImpact.strongest] : null;
                const s = wxStat;
                const dir = s.slope < 0 ? "slower" : "faster";
                const unitSuffix = s.unit === "T+Dp" ? "" : s.unit;
                return (
                  <>
                    <div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fit,minmax(160px,1fr))", gap:16 }}>
                      <Card><Stat label="Most weather-sensitive" value={strongest ? strongest.label : "—"}
                        unit={strongest ? `r ${strongest.r>=0?"+":""}${strongest.r.toFixed(2)} · ${strongest.strength}` : ""} color={C.cyan} /></Card>
                      <Card><Stat label="Weather-enriched easy runs" value={weatherImpact.n} unit="runs" color={C.green} /></Card>
                      <Card><Stat label="Baseline efficiency" value={weatherImpact.baseEF.toFixed(1)} unit="EF (speed/HR ×1000)" color={C.purple} /></Card>
                    </div>

                    {(() => {
                      // Weather coverage across all loaded runs — the analysis
                      // only counts runs that carry denormalised weather, so
                      // surface the gap and let the owner backfill the rest.
                      const withWx = runs.filter(r => r && r.weather && r.weather.tempF != null).length;
                      if (!isOwner || withWx >= runs.length) return null;
                      return (
                        <div style={{ display:"flex", alignItems:"center", gap:10, flexWrap:"wrap", fontSize:11, color:C.textMuted }}>
                          <span>Weather data on {withWx} of {runs.length} loaded runs.</span>
                          <button disabled={wxBackfill === "running"}
                            onClick={async () => {
                              setWxBackfill("running");
                              try {
                                const res = await functions.httpsCallable("backfillInsightWeather")({ limit: 2000 });
                                const d = res.data || {};
                                setWxBackfill(`Enriched ${d.updated||0}, skipped ${d.skipped||0} of ${d.total||0}. Reload to view.`);
                              } catch (e) { setWxBackfill(`Backfill failed: ${e.message}`); }
                            }}
                            style={{ padding:"4px 12px", borderRadius:6, border:`1px solid ${C.cyan}55`, cursor:"pointer",
                              background:C.cyan+"15", color:C.cyan, fontSize:11, fontFamily:"'IBM Plex Mono',monospace" }}>
                            {wxBackfill === "running" ? "Fetching weather…" : "Backfill older runs"}
                          </button>
                          {wxBackfill && wxBackfill !== "running" && <span>{wxBackfill}</span>}
                        </div>
                      );
                    })()}

                    <Card>
                      <div style={{ display:"flex", flexWrap:"wrap", gap:6, marginBottom:12 }}>
                        {WS.WEATHER_VARS.map(v => (
                          <button key={v.key} onClick={() => setWxVar(v.key)}
                            style={{ padding:"5px 12px", borderRadius:6, fontSize:11, cursor:"pointer", fontFamily:"'IBM Plex Mono',monospace",
                              border:`1px solid ${wxVar===v.key?C.cyan:C.border}`,
                              background: wxVar===v.key?C.cyan+"20":"transparent",
                              color: wxVar===v.key?C.cyan:C.textMuted }}>
                            {v.label}{weatherImpact.vars[v.key] ? ` · r ${weatherImpact.vars[v.key].r>=0?"+":""}${weatherImpact.vars[v.key].r.toFixed(2)}` : ""}
                          </button>
                        ))}
                      </div>

                      <div style={{ fontSize:13, fontWeight:600, color:C.textDim, marginBottom:2 }}>
                        {s.label} vs detrended easy-run efficiency
                      </div>
                      <div style={{ fontSize:11, color:C.textMuted, marginBottom:14, lineHeight:1.5 }}>
                        EF residual is aerobic efficiency with the season's fitness trend removed, so what's left is weather-attributable. Below the line = ran worse than your norm for that {s.label.toLowerCase()}; the band is ±1σ of weather-explained variation.
                      </div>

                      <ResponsiveContainer width="100%" height={320}>
                        <ComposedChart data={wxChart} margin={{ top: 8, right: 16, left: 0, bottom: 16 }}>
                          <CartesianGrid strokeDasharray="3 3" stroke={C.border} />
                          <XAxis type="number" dataKey="x" domain={['dataMin','dataMax']} tick={{ fontSize:11, fill:C.textMuted }}
                            label={{ value:`${s.label} (${s.unit})`, position:"insideBottom", offset:-8, fontSize:11, fill:C.textMuted }} />
                          <YAxis tick={{ fontSize:11, fill:C.textMuted }}
                            label={{ value:"EF residual", angle:-90, position:"insideLeft", fontSize:11, fill:C.textMuted }} />
                          <Tooltip content={({ active, payload }) => {
                            if (!active || !payload || !payload.length) return null;
                            const pt = payload[0] && payload[0].payload; if (!pt) return null;
                            const z = WS.zScoreForRun(s, pt.x, pt.y);
                            return (
                              <div style={{ background:C.bg2, border:`1px solid ${C.border}`, borderRadius:6, padding:"8px 10px", fontSize:11 }}>
                                <div style={{ color:C.text, fontWeight:600, marginBottom:4 }}>{pt.runLabel}</div>
                                <div style={{ color:C.textMuted }}>{s.label}: {pt.x}{unitSuffix}</div>
                                <div style={{ color:C.textMuted }}>EF residual: {pt.y>=0?"+":""}{pt.y}</div>
                                {z!=null && <div style={{ color: z>=0?C.green:C.red, marginTop:2 }}>{z>=0?"+":""}{z.toFixed(1)}σ vs your weather norm — {z>=0?"better":"worse"} than this weather predicts</div>}
                              </div>
                            );
                          }} />
                          <Line type="monotone" dataKey="upper" stroke={C.textMuted} strokeDasharray="4 4" dot={false} strokeWidth={1} isAnimationActive={false} />
                          <Line type="monotone" dataKey="lower" stroke={C.textMuted} strokeDasharray="4 4" dot={false} strokeWidth={1} isAnimationActive={false} />
                          <Line type="monotone" dataKey="fit" stroke={C.cyan} dot={false} strokeWidth={2} isAnimationActive={false} />
                          <Scatter dataKey="y" fill={C.purple} isAnimationActive={false} />
                        </ComposedChart>
                      </ResponsiveContainer>

                      <div style={{ fontSize:12, color:C.textDim, marginTop:14, lineHeight:1.6 }}>
                        {s.n < weatherImpact.minSample ? (
                          <>Not enough runs spanning a range of {s.label.toLowerCase()} to call this yet ({s.n} usable).</>
                        ) : Math.abs(s.r) < 0.2 ? (
                          <>No meaningful link between {s.label.toLowerCase()} and your efficiency (r {s.r>=0?"+":""}{s.r.toFixed(2)}, {s.n} runs) — performance holds up across the {s.label.toLowerCase()} you've trained in.</>
                        ) : (
                          <>Your easy-run efficiency runs <strong>{dir}</strong> as {s.label.toLowerCase()} rises — a {s.strength} link (r {s.r>=0?"+":""}{s.r.toFixed(2)}, r² {(s.r2*100).toFixed(0)}%, {s.n} runs), about {Math.abs(s.slope*10).toFixed(2)} EF per +10{unitSuffix||" units"}. Runs outside the ±1σ band beat or missed your own weather-adjusted norm.</>
                        )}
                      </div>
                    </Card>

                    {(() => {
                      // Weather performance estimator — the heat-stress bands
                      // priced against the athlete's own EF-vs-heat fit.
                      const bands = WS.weatherPenaltyBands(weatherImpact);
                      const paces = weatherSamples.map(s => s.paceSec).filter(p => p > 0).sort((a, b) => a - b);
                      const refPace = paces.length ? paces[Math.floor(paces.length / 2)] : null;
                      const fmtP = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
                      const cell = { padding: "6px 8px", borderBottom: `1px solid ${C.border}` };
                      const th = { ...cell, color: C.textMuted, fontWeight: 600 };
                      return (
                        <Card>
                          <div style={{ fontSize: 13, fontWeight: 600, color: C.textDim, marginBottom: 2 }}>Performance in Weather</div>
                          <div style={{ fontSize: 11, color: C.textMuted, marginBottom: 14, lineHeight: 1.5 }}>
                            Predicted pace cost vs ideal conditions, from your own efficiency-vs-heat-stress fit (air temp + dew point). It's a percentage, so it applies to any pace{refPace ? ` — the last column is at your ${fmtP(refPace)}/mi easy pace` : ""}.
                          </div>
                          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12, fontFamily: "'IBM Plex Mono',monospace" }}>
                            <thead><tr>
                              <th style={{ ...th, textAlign: "left" }}>Conditions</th>
                              <th style={{ ...th, textAlign: "right" }}>T+Dp</th>
                              <th style={{ ...th, textAlign: "right" }}>Pace cost</th>
                              {refPace && <th style={{ ...th, textAlign: "right" }}>At easy pace</th>}
                            </tr></thead>
                            <tbody>
                              {bands.map(b => {
                                const pct = b.penaltyPct;
                                const slow = pct != null && pct > 0.003;
                                const col = !slow ? C.green : pct > 0.03 ? C.red : C.amber;
                                return (
                                  <tr key={b.key}>
                                    <td style={{ ...cell, color: C.text, fontWeight: 600 }}>{b.label}</td>
                                    <td style={{ ...cell, textAlign: "right", color: C.textMuted }}>{b.range}</td>
                                    <td style={{ ...cell, textAlign: "right", color: col, fontWeight: 600 }}>
                                      {b.key === "ideal" ? "baseline" : pct == null ? "—" : !slow ? "minimal" : `+${(pct * 100).toFixed(1)}%`}
                                    </td>
                                    {refPace && <td style={{ ...cell, textAlign: "right", color: col }}>
                                      {b.key === "ideal" || !slow || pct == null ? "—" : `+${Math.round(pct * refPace)} s/mi`}
                                    </td>}
                                  </tr>
                                );
                              })}
                            </tbody>
                          </table>
                          <div style={{ fontSize: 11, color: C.textMuted, marginTop: 12, lineHeight: 1.6 }}>
                            Built from {weatherImpact.n} easy runs. Racing in heat usually costs more than this — core temperature climbs faster at hard efforts — so treat these as a floor for race-day planning.
                          </div>
                        </Card>
                      );
                    })()}

                    {(() => {
                      // Recovery cost of heat — does an easy run in the heat
                      // depress next-day HRV enough that the treadmill is the
                      // better trade for the same easy stimulus?
                      const rec = weatherRecovery;
                      if (!rec || !rec.enoughData) {
                        return (
                          <Card>
                            <div style={{ fontSize: 13, fontWeight: 600, color: C.textDim, marginBottom: 6 }}>Recovery After Hot Runs</div>
                            <EmptyState icon="🛌" title="Not enough paired data"
                              sub={`Needs easy runs with weather AND an overnight HRV reading the next day. So far: ${rec ? rec.n : 0}.`} />
                          </Card>
                        );
                      }
                      const dropPct = rec.severeDropPct;
                      const heatHurts = rec.r < -0.2 && dropPct > 0.03;
                      return (
                        <Card>
                          <div style={{ fontSize: 13, fontWeight: 600, color: C.textDim, marginBottom: 2 }}>Recovery After Hot Runs</div>
                          <div style={{ fontSize: 11, color: C.textMuted, marginBottom: 14, lineHeight: 1.5 }}>
                            Overnight HRV the morning after each easy run vs that run's heat stress, fitness trend removed. Lower next-day HRV = the run cost more to recover from.
                          </div>
                          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(150px,1fr))", gap: 12, marginBottom: 14 }}>
                            <Stat label="Heat → recovery link" value={`r ${rec.r >= 0 ? "+" : ""}${rec.r.toFixed(2)}`} unit={rec.strength} color={heatHurts ? C.amber : C.green} />
                            <Stat label="Next-day HRV, severe vs ideal" value={heatHurts ? `−${(dropPct * 100).toFixed(0)}%` : "≈ flat"} unit={`${rec.n} runs`} color={heatHurts ? C.red : C.green} />
                          </div>
                          <div style={{ fontSize: 12, color: C.textDim, lineHeight: 1.6 }}>
                            {heatHurts ? (
                              <>An easy run in severe heat (T+Dp 160+) leaves your next-morning HRV about <strong>{(dropPct * 100).toFixed(0)}% lower</strong> than the same run in cool air. An easy run is the same aerobic stimulus on a treadmill — so on a hot day, treadmilling it <strong>banks that recovery</strong> for your next quality session. Save the outdoor heat exposure for when heat adaptation is the actual goal.</>
                            ) : (
                              <>No meaningful link between heat and your next-day HRV (r {rec.r >= 0 ? "+" : ""}{rec.r.toFixed(2)}, {rec.n} runs) — recovery holds up after your hot easy runs, so the treadmill isn't buying you recovery. Run outside.</>
                            )}
                            <div style={{ fontSize: 10, color: C.textMuted, marginTop: 8 }}>Directional — next-day HRV also moves with sleep and life stress, which this can't separate out.</div>
                          </div>
                        </Card>
                      );
                    })()}
                  </>
                );
              })()}
            </div>
          )}
        </div>
      );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PERFORMANCE VIEW — extracted to src/performanceView.jsx (module-split slice 4)
    // ─────────────────────────────────────────────────────────────────────────

    // ─────────────────────────────────────────────────────────────────────────
    // MARATHON ANALYSIS VIEW — extracted to src/marathonAnalysis.jsx (module-split slice 8)
    // ─────────────────────────────────────────────────────────────────────────


    // ─────────────────────────────────────────────────────────────────────────
    // RACES & GOALS VIEW — extracted to src/races.jsx (module-split slice 9)
    // ─────────────────────────────────────────────────────────────────────────

window.AppViews = Object.assign(window.AppViews || {}, { Analytics });
