// running/src/planRulesRoadmap.jsx — slice 13 of the module split.
//
// The two documentation-ish leaves under Training: PLAN RULES (rendered
// from running/lib/planRulesDoc.js so constants stay in sync with the
// generator) and ROADMAP (phase periodization projection — its three
// derive/project helpers travel with it; Roadmap is their only
// consumer). Extracted verbatim from running/index.html.

const { C, Card } = window.SharedUI;
const { useState, useEffect } = React;

    // ─────────────────────────────────────────────────────────────────────────
    // PLAN RULES VIEW — human-readable documentation of every rule the plan
    // generator enforces. Content is driven by running/lib/planRulesDoc.js so
    // quantitative constants (methodology zone tables, treadmill inclines,
    // indoor-temp default) stay in sync with the backend modules.
    // ─────────────────────────────────────────────────────────────────────────
    function PlanRules() {
      const doc = typeof window !== "undefined" ? window.PlanRulesDoc : null;
      const [openIds, setOpenIds] = useState(() => new Set((doc?.sections || []).slice(0, 2).map(s => s.id)));
      if (!doc) {
        return <div style={{color:C.textMuted,padding:20}}>Rule documentation is still loading…</div>;
      }
      const toggle = (id) => setOpenIds(prev => {
        const next = new Set(prev);
        if (next.has(id)) next.delete(id); else next.add(id);
        return next;
      });
      const expandAll = () => setOpenIds(new Set(doc.sections.map(s => s.id)));
      const collapseAll = () => setOpenIds(new Set());
      return (
        <div style={{display:"flex",flexDirection:"column",gap:12,paddingTop:12}}>
          <div style={{padding:"14px 16px",background:C.bg2,border:`1px solid ${C.border}`,borderRadius:10}}>
            <div style={{fontSize:13,color:C.text,lineHeight:1.6}}>
              These are the rules the plan generator follows every time a weekly training block is produced. They live in <span style={{fontFamily:"'IBM Plex Mono',monospace",color:C.cyan}}>functions/lib/</span> as pure modules covered by Jest tests, so the docs below stay in sync with what the system actually does.
            </div>
            <div style={{marginTop:10,display:"flex",gap:8,flexWrap:"wrap"}}>
              <button onClick={expandAll} style={{padding:"4px 10px",fontSize:11,fontWeight:600,fontFamily:"'IBM Plex Mono',monospace",border:`1px solid ${C.border}`,background:"transparent",color:C.textMuted,borderRadius:6,cursor:"pointer"}}>Expand all</button>
              <button onClick={collapseAll} style={{padding:"4px 10px",fontSize:11,fontWeight:600,fontFamily:"'IBM Plex Mono',monospace",border:`1px solid ${C.border}`,background:"transparent",color:C.textMuted,borderRadius:6,cursor:"pointer"}}>Collapse all</button>
              <span style={{padding:"4px 10px",fontSize:11,color:C.textDim,fontFamily:"'IBM Plex Mono',monospace"}}>v{doc.version} · {doc.sections.length} sections</span>
            </div>
          </div>
          {doc.sections.map((s, si) => {
            const open = openIds.has(s.id);
            return (
              <div key={s.id} style={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:10,overflow:"hidden"}}>
                <button onClick={() => toggle(s.id)}
                  style={{width:"100%",textAlign:"left",padding:"12px 16px",background:"transparent",border:"none",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"space-between",gap:12}}>
                  <div style={{display:"flex",alignItems:"baseline",gap:10,flex:1,minWidth:0}}>
                    <span style={{fontSize:11,fontWeight:700,fontFamily:"'IBM Plex Mono',monospace",color:C.cyan,flexShrink:0}}>§{si + 1}</span>
                    <span style={{fontSize:14,fontWeight:600,color:C.text,fontFamily:"'Space Grotesk',sans-serif"}}>{s.title}</span>
                  </div>
                  <span style={{fontSize:14,color:C.textMuted,transition:"transform 0.15s",transform: open ? "rotate(90deg)" : "none"}}>▶</span>
                </button>
                {open && (
                  <div style={{padding:"0 16px 14px 16px",borderTop:`1px solid ${C.border}40`,display:"flex",flexDirection:"column",gap:10}}>
                    {s.intro && (
                      <div style={{fontSize:12,color:C.textDim,lineHeight:1.6,marginTop:10}}>{s.intro}</div>
                    )}
                    {s.content.map((block, bi) => {
                      if (block.type === "paragraph") {
                        return (
                          <div key={bi} style={{fontSize:12,color:C.textDim,lineHeight:1.6}}>{block.text}</div>
                        );
                      }
                      if (block.type === "bullets") {
                        return (
                          <ul key={bi} style={{margin:0,padding:"0 0 0 4px",listStyle:"none",display:"flex",flexDirection:"column",gap:6}}>
                            {block.items.map((item, ii) => (
                              <li key={ii} style={{display:"flex",alignItems:"flex-start",gap:10,fontSize:12,color:C.textDim,lineHeight:1.55}}>
                                <span style={{color:C.cyan,marginTop:2,flexShrink:0}}>•</span>
                                <div style={{flex:1}}>
                                  {item.label && (
                                    <span style={{fontFamily:"'IBM Plex Mono',monospace",color:C.amber,fontWeight:700,marginRight:6}}>{item.label}</span>
                                  )}
                                  <span style={{color:C.text}}>{item.text}</span>
                                </div>
                              </li>
                            ))}
                          </ul>
                        );
                      }
                      if (block.type === "table") {
                        return (
                          <div key={bi} style={{overflowX:"auto",border:`1px solid ${C.border}`,borderRadius:8}}>
                            <table style={{width:"100%",borderCollapse:"collapse",fontSize:12,color:C.textDim,fontFamily:"'IBM Plex Mono',monospace"}}>
                              <thead>
                                <tr style={{background:C.bg1}}>
                                  {block.headers.map((h, hi) => (
                                    <th key={hi} style={{textAlign:"left",padding:"8px 10px",color:C.cyan,fontSize:11,fontWeight:700,borderBottom:`1px solid ${C.border}`}}>{h}</th>
                                  ))}
                                </tr>
                              </thead>
                              <tbody>
                                {block.rows.map((row, ri) => (
                                  <tr key={ri} style={{borderTop: ri === 0 ? "none" : `1px solid ${C.border}40`}}>
                                    {row.map((cell, ci) => (
                                      <td key={ci} style={{padding:"7px 10px",color: ci === 0 ? C.text : C.textDim}}>{cell}</td>
                                    ))}
                                  </tr>
                                ))}
                              </tbody>
                            </table>
                          </div>
                        );
                      }
                      return null;
                    })}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // ROADMAP VIEW — Plan-wide view: every week from now to race day
    //
    // Shows the full arc of the training cycle at a glance: week number,
    // date range, phase, target miles (actual for generated weeks, projected
    // via the same periodization template the chart uses for ungenerated
    // weeks), focus tag, and a coach's-note context line. Detail for each
    // generated week stays in the Plan tab — this view is the map, not the
    // turn-by-turn.
    //
    // Focus derivation: for generated weeks we scan the days and pick the
    // dominant workout type (hills, MP, threshold, VO2, long). For ungenerated
    // weeks we derive from phase + position within the phase + weeks-to-race.
    // ─────────────────────────────────────────────────────────────────────────

    // Deterministic periodization template for ungenerated weeks — mirrors
    // the peak16 array used by the Plan tab's mileage chart and the backend
    // generator (functions/index.js) so projections stay consistent between
    // views. Returns a number (target miles).
    function projectRoadmapWeekMiles(phaseName, weekInPhase, phaseLen) {
      // Mirror functions/index.js — Conditioning peaks at 42, Build
      // STARTS at 45 (deliberate +3 step at phase boundary), peaks at 50.
      const CONDITIONING_END = 42;
      const BUILD_START = 45;
      const BUILD_END = 50;
      // +2mi/week ramp to the 62mi peak, hold, then 3-week taper.
      const peak16 = [50, 52, 54, 56, 58, 60, 62, 62, 62, 62, 62, 62, 60, 45, 32, 26];
      const phasePct = phaseLen > 1 ? weekInPhase / (phaseLen - 1) : 1;
      if (phaseName === "Peak" || phaseName === "Peak Training") {
        const idx = Math.min(Math.round(weekInPhase * (peak16.length - 1) / Math.max(phaseLen - 1, 1)), peak16.length - 1);
        return peak16[idx];
      }
      if (phaseName === "Build" || phaseName === "Race Build") {
        // At least +2mi/week (athlete preference), capped at BUILD_END.
        const proportional = BUILD_START + (BUILD_END - BUILD_START) * phasePct;
        const stepped = BUILD_START + 2 * weekInPhase;
        return Math.round(Math.min(BUILD_END, Math.max(proportional, stepped)));
      }
      // Conditioning default: ramp from 20 → ~42, at least +2mi/week.
      const proportional = 20 + (CONDITIONING_END - 20) * phasePct;
      const stepped = 20 + 2 * weekInPhase;
      return Math.round(Math.min(CONDITIONING_END, Math.max(proportional, stepped)));
    }

    // Given a generated week's days, categorize the dominant focus. Returns
    // { focus, context } where focus is a short tag and context is a coach's
    // note explaining the week's purpose.
    function deriveFocusFromDays(days, phaseName) {
      if (!Array.isArray(days) || !days.length) return { focus: "Base", context: "Easy-paced aerobic work; volume over intensity." };
      const types = days.map(d => (d.type || "").toLowerCase());
      const longDay = days.find(d => (d.type || "").toLowerCase() === "long");
      const longDesc = (longDay?.description || "").toLowerCase();
      const allDescs = days.map(d => (d.description || "").toLowerCase()).join(" ");
      const hasHills = /hill|incline|climb/.test(allDescs);
      // MP detection is precise: look for affirmative "X mi @ MP", "MP block",
      // "MP finish", "marathon pace block/segment/finish" — and only on the
      // LONG day's own description. Aggregating across the whole week caught
      // false positives like "no MP work this week" and labeled aerobic
      // weeks as "MP + long". Also gated by phase: Base never gets the MP
      // label even if a description touches the term, because the prompt's
      // MP-block-patience rule says no MP work in Base.
      const inBase = phaseName === "Conditioning" || phaseName === "Base Conditioning";
      const longHasRealMP = !inBase && (
        /(\d+\s*mi|\d+\s*min|final\s*\d+).{0,12}(@|at|of)\s*(mp\b|marathon\s*pace|race\s*pace)/i.test(longDesc) ||
        /(mp|marathon\s*pace)\s*(block|segment|finish|push)/i.test(longDesc) ||
        /(progressive|progression).{0,40}(mp|marathon\s*pace)/i.test(longDesc)
      );
      const hasMPDay = types.includes("race_pace");
      const hasMP = longHasRealMP || hasMPDay;
      const hasIntervals = types.includes("intervals");
      const hasTempo = types.includes("tempo") || /\bthreshold\b|\btempo\b/.test(allDescs);
      const hasLong = types.includes("long");

      if (hasMP && hasLong) return { focus: "MP + long", context: "Marathon-pace segments inside the long run — race-specific endurance." };
      if (hasIntervals && hasLong) return { focus: "VO2 + long", context: "Interval work for top-end fitness, long run building aerobic strength." };
      if (hasTempo && hasLong) return { focus: "Tempo + long", context: "Threshold/tempo session plus a progression long run — classic base-plus-quality week." };
      if (hasHills) return { focus: "Hill strength", context: "Hill work builds leg strength and running economy without the breakdown cost of track speed." };
      if (hasMP) return { focus: "Marathon pace", context: "Goal-pace work — dialing in the feel of race effort." };
      if (hasIntervals) return { focus: "VO2 max", context: "Intervals at 3K–5K pace — raising aerobic ceiling." };
      if (hasTempo) return { focus: "Tempo / LT", context: "Sustained tempo effort pushes lactate threshold — the engine that drives marathon pace." };
      if (hasLong) return { focus: "Long run emphasis", context: "Long-run focused week; aerobic capacity and durability." };
      return { focus: "Aerobic base", context: "Easy-paced volume, recovery-forward." };
    }

    // Template-driven focus for ungenerated weeks. Uses phase + position in
    // phase + weeks-remaining-to-race so the last N weeks always land on
    // taper/sharpening/race-week regardless of phase structure.
    function derivePlannedFocus({ phaseName, weekInPhase, phaseLen, weekNum, weeksTotal }) {
      const toRace = weeksTotal - weekNum; // 0 = race week
      if (toRace === 0) return { focus: "Race week", context: "Rest, fuel, short easy shakeouts. Trust the work. Race day." };
      if (toRace === 1) return { focus: "Pre-race taper", context: "Volume down 40-60%. A couple short race-pace touches to stay sharp. Sleep is the workout." };
      if (toRace === 2) return { focus: "Sharpening", context: "Shorter tune-ups at MP and 5K pace; volume easing. Keep the engine hot, shed accumulated fatigue." };

      const pct = phaseLen > 1 ? weekInPhase / (phaseLen - 1) : 0;
      if (phaseName === "Conditioning") {
        if (pct < 0.5) return { focus: "Aerobic base", context: "Easy-paced mileage, strides 1-2x/wk. Building the aerobic foundation that everything else rests on." };
        return { focus: "Volume ramp", context: "Mileage climbing; throw in the first controlled strides and hill strides to prime the system." };
      }
      if (phaseName === "Build" || phaseName === "Race Build") {
        if (pct < 0.33) return { focus: "Tempo intro", context: "First tempo/threshold sessions of the cycle — short-to-moderate sustained efforts around LT." };
        if (pct < 0.66) return { focus: "Threshold work", context: "Longer tempos, cruise intervals. Pushing threshold pace up so marathon pace feels comfortable later." };
        // Alternate MP-long / aerobic-long across consecutive weeks once
        // MP work is in play — matches the LONG-RUN MP CADENCE rule in
        // the system prompt.
        return weekInPhase % 2 === 0
          ? { focus: "Tempo + progression", context: "Progression long run — easy → steady → MP-ish close — paired with tempo midweek. Race specificity begins." }
          : { focus: "Aerobic long + threshold", context: "Aerobic-only long run this week (no MP) to consolidate last week's quality stress; threshold midweek continues." };
      }
      if (phaseName === "Peak" || phaseName === "Peak Training") {
        if (pct < 0.33) {
          return weekInPhase % 2 === 0
            ? { focus: "MP + strength", context: "Marathon-pace blocks in long runs, hill strength midweek. Race-specific endurance ramping." }
            : { focus: "Aerobic long + strength", context: "Aerobic-only long run this week; hill strength midweek. Absorbing last week's MP work." };
        }
        if (pct < 0.75) {
          return weekInPhase % 2 === 0
            ? { focus: "Peak mileage + long MP", context: "Highest weekly volume of the cycle; long run with extended MP segments." }
            : { focus: "Peak mileage + aerobic long", context: "Peak weekly volume; aerobic-only long run to consolidate the alternating MP block." };
        }
        return { focus: "Quality over volume", context: "Mileage easing slightly, intensity holding. Last shot at meaningful race-specific work before the taper." };
      }
      return { focus: "Base", context: "Aerobic work." };
    }

    function Roadmap({ userId, activities }) {
      const [plans, setPlans] = useState([]);
      const [loading, setLoading] = useState(true);

      useEffect(() => {
        if (!userId) return;
        const unsub = db.collection("users").doc(userId).collection("trainingPlans")
          .where("status", "==", "active").limit(1)
          .onSnapshot(snap => {
            setPlans(snap.docs.map(d => ({ id: d.id, ...d.data() })));
            setLoading(false);
          }, () => setLoading(false));
        return unsub;
      }, [userId]);

      const activePlan = plans[0] || null;

      if (loading) return <div style={{ padding: 24, color: C.textMuted }}>Loading roadmap…</div>;
      if (!activePlan) {
        return (
          <Card>
            <div style={{ padding: 12, textAlign: "center", color: C.textMuted }}>
              No active plan yet. Generate one in the <strong style={{ color: C.cyan }}>Plan</strong> tab and the roadmap will populate.
            </div>
          </Card>
        );
      }

      // Compute the current week (1-indexed) and week date ranges from
      // plan.createdAt so the UI shows e.g. "W14 · Aug 3 – Aug 9".
      const created = activePlan.createdAt?.seconds
        ? new Date(activePlan.createdAt.seconds * 1000)
        : new Date(activePlan.createdAt);
      const planStruct = window.PlanMath.planStructure(activePlan);
      const weeksTotal = planStruct.weeksTotal;
      // Week boundaries anchor to createdAt (not Monday) so historical
      // week ranges align with the activities the athlete actually ran
      // in those weeks. See revert of PR #164.
      const currentWeek = Math.max(1, Math.ceil((Date.now() - created) / (7 * 86400000)));

      // Phase boundaries (0-indexed), derived once by PlanMath so the
      // Roadmap, Plan chart, phase pills and Race readiness can't drift
      // apart on total weeks or phase ranges.
      const phaseBounds = planStruct.phases.map(p => ({ name: p.name, label: p.label, start: p.startWeek - 1, end: p.endWeek - 1 }));
      const phases = planStruct.phases; // header phase pills (name · label · numWeeks)
      const phaseColors = { Conditioning: C.green, Build: C.amber, Peak: C.cyan, "Peak Training": C.cyan, "Race Build": C.amber };
      const findPhase = (weekIdx0) => phaseBounds.find(p => weekIdx0 >= p.start && weekIdx0 <= p.end) || null;

      // Build the row models once so the render is clean.
      const rows = [];
      for (let i = 0; i < weeksTotal; i++) {
        const weekNum = i + 1;
        const week = activePlan.weeks?.[i] || null;
        const phase = findPhase(i);
        const pName = phase?.name || "Peak";
        const pLabel = phase?.label || pName;
        const phaseLen = phase ? phase.end - phase.start + 1 : weeksTotal;
        const weekInPhase = phase ? i - phase.start : i;

        const weekStart = new Date(created.getTime() + (weekNum - 1) * 7 * 86400000);
        const weekEnd = new Date(weekStart.getTime() + 6 * 86400000);

        const generated = !!week && Array.isArray(week.days) && week.days.length > 0;
        const targetMiles = generated
          ? (week.targetMiles || 0)
          : projectRoadmapWeekMiles(pName, weekInPhase, phaseLen);
        const { focus, context } = generated
          ? deriveFocusFromDays(week.days, pName)
          : derivePlannedFocus({ phaseName: pName, weekInPhase, phaseLen, weekNum, weeksTotal });
        const theme = week?.theme || null;

        rows.push({
          weekNum, weekStart, weekEnd, phase: pName, phaseLabel: pLabel, phaseLen, weekInPhase,
          targetMiles, focus, context, theme, generated,
          isPast: weekNum < currentWeek,
          isCurrent: weekNum === currentWeek,
          isRaceWeek: weekNum === weeksTotal,
        });
      }

      const fmtRange = (a, b) => {
        const opt = { month: "short", day: "numeric" };
        return `${a.toLocaleDateString(undefined, opt)} – ${b.toLocaleDateString(undefined, opt)}`;
      };

      // Totals: completed miles so far (from generated + completed weeks),
      // remaining projected miles, plan total. Keeps the athlete oriented.
      const totalProjected = rows.reduce((s, r) => s + (r.targetMiles || 0), 0);
      const completedMiles = rows.filter(r => r.isPast).reduce((s, r) => s + (r.targetMiles || 0), 0);
      const remainingMiles = totalProjected - completedMiles;

      return (
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          {/* Header card: goal + totals */}
          <Card>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 16, flexWrap: "wrap" }}>
              <div>
                <div style={{ fontSize: 13, color: C.textMuted, marginBottom: 4 }}>Race goal</div>
                <div style={{ fontSize: 18, fontWeight: 700, color: C.text }}>
                  {activePlan.goalTime || "Goal TBD"} · {activePlan.raceName || "Target race"}
                </div>
                <div style={{ fontSize: 12, color: C.textMuted, marginTop: 4 }}>
                  {activePlan.raceDate ? new Date(activePlan.raceDate + "T12:00:00").toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" }) : "Race date TBD"}
                </div>
              </div>
              <div style={{ display: "flex", gap: 18, fontSize: 12, color: C.textDim }}>
                <div>
                  <div style={{ color: C.textMuted, fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em" }}>Weeks</div>
                  <div style={{ fontSize: 16, fontWeight: 700, color: C.cyan }}>{Math.min(currentWeek, weeksTotal)} <span style={{ color: C.textMuted, fontWeight: 400 }}>/ {weeksTotal}</span></div>
                </div>
                <div>
                  <div style={{ color: C.textMuted, fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em" }}>Planned total</div>
                  <div style={{ fontSize: 16, fontWeight: 700, color: C.text }}>{Math.round(totalProjected)} mi</div>
                </div>
                <div>
                  <div style={{ color: C.textMuted, fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em" }}>Remaining</div>
                  <div style={{ fontSize: 16, fontWeight: 700, color: C.green }}>{Math.round(remainingMiles)} mi</div>
                </div>
              </div>
            </div>
            {phases.length > 0 && (
              <div style={{ display: "flex", gap: 6, marginTop: 12, flexWrap: "wrap" }}>
                {phases.map((p, i) => {
                  const pc = phaseColors[p.name] || C.cyan;
                  return (
                    <span key={i} style={{ fontSize: 11, padding: "2px 9px", borderRadius: 12, background: pc + "18", border: `1px solid ${pc}40`, color: pc, fontWeight: 600 }}>
                      {p.label || p.name} · {p.numWeeks}w
                    </span>
                  );
                })}
              </div>
            )}
          </Card>

          {/* Week roadmap — one card per week */}
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {rows.map(r => {
              const pc = phaseColors[r.phase] || C.cyan;
              const border = r.isCurrent ? C.cyan : r.isRaceWeek ? C.green : r.isPast ? C.border : C.border;
              const bg = r.isCurrent ? C.cyan + "10" : r.isRaceWeek ? C.green + "10" : r.isPast ? C.bg2 : "transparent";
              const opacity = r.isPast ? 0.65 : 1;
              return (
                <div key={r.weekNum} style={{
                  padding: "10px 14px",
                  border: `1px solid ${border}`,
                  borderLeft: `3px solid ${r.isCurrent ? C.cyan : pc}`,
                  borderRadius: 8,
                  background: bg,
                  opacity,
                  display: "grid",
                  gridTemplateColumns: "60px 120px 90px 1fr auto",
                  gap: 12,
                  alignItems: "center",
                }}>
                  {/* Week # */}
                  <div>
                    <div style={{ fontSize: 18, fontWeight: 700, color: r.isCurrent ? C.cyan : r.isRaceWeek ? C.green : C.text }}>
                      W{r.weekNum}
                    </div>
                    {r.isCurrent && <div style={{ fontSize: 9, color: C.cyan, fontWeight: 700, letterSpacing: "0.05em" }}>NOW</div>}
                    {r.isRaceWeek && <div style={{ fontSize: 9, color: C.green, fontWeight: 700, letterSpacing: "0.05em" }}>RACE</div>}
                  </div>

                  {/* Date range */}
                  <div style={{ fontSize: 11, color: C.textMuted, fontFamily: "'IBM Plex Mono',monospace" }}>
                    {fmtRange(r.weekStart, r.weekEnd)}
                  </div>

                  {/* Phase badge */}
                  <span style={{ fontSize: 10, padding: "2px 8px", borderRadius: 10, background: pc + "18", border: `1px solid ${pc}40`, color: pc, fontWeight: 600, textAlign: "center" }}>
                    {r.phaseLabel}
                  </span>

                  {/* Focus + context */}
                  <div>
                    <div style={{ fontSize: 13, fontWeight: 600, color: C.text }}>
                      {r.focus}
                      {r.theme && r.theme !== r.focus ? <span style={{ color: C.textMuted, fontWeight: 400 }}> · {r.theme}</span> : null}
                      {!r.generated && <span style={{ marginLeft: 8, fontSize: 10, color: C.textMuted, fontWeight: 400, fontStyle: "italic" }}>projected</span>}
                    </div>
                    <div style={{ fontSize: 11, color: C.textDim, marginTop: 2, lineHeight: 1.5 }}>
                      {r.context}
                    </div>
                  </div>

                  {/* Target miles */}
                  <div style={{ textAlign: "right", minWidth: 60 }}>
                    <div style={{ fontSize: 16, fontWeight: 700, color: C.text, fontFamily: "'IBM Plex Mono',monospace" }}>
                      {r.targetMiles || 0}
                    </div>
                    <div style={{ fontSize: 9, color: C.textMuted, textTransform: "uppercase", letterSpacing: "0.05em" }}>mi</div>
                  </div>
                </div>
              );
            })}
          </div>

          {/* Footnote */}
          <div style={{ fontSize: 10, color: C.textMuted, padding: "4px 8px", lineHeight: 1.6 }}>
            Generated weeks show their actual prescribed miles and focus derived from the scheduled workouts.
            Ungenerated weeks show projected miles from the periodization template and a phase-appropriate focus.
            Detail for any week lives in the <strong style={{ color: C.cyan }}>Plan</strong> tab.
          </div>
        </div>
      );
    }

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