/* global React, ForecastEngine */
const { useState, useEffect, useMemo, useRef } = React;

const SAVED_MODELS_STORAGE_KEY = "nudge:savedForecastModels";
const DEALS_FILTER_STORAGE_KEY = "nudge:forecastDealsFilter";

function loadDealsFilter() {
  try {
    const raw = window.localStorage?.getItem(DEALS_FILTER_STORAGE_KEY);
    if (raw) {
      const parsed = JSON.parse(raw);
      return {
        owner: parsed && typeof parsed.owner === "string" ? parsed.owner : null,
        stage: parsed && typeof parsed.stage === "string" ? parsed.stage : null,
      };
    }
  } catch (_) { /* fall through */ }
  return { owner: null, stage: null };
}

function seedSavedModels() {
  const balanced = ForecastEngine.defaultModel();
  const conservative = ForecastEngine.defaultModel();
  conservative.scenario = "conservative";
  conservative.riskBuffers = conservative.riskBuffers.map((b) => {
    if (b.key === "globalHaircut") return { ...b, value: 15 };
    if (b.key === "noNextStepPenalty") return { ...b, value: 22 };
    if (b.key === "missingDecisionMakerPenalty") return { ...b, value: 25 };
    if (b.key === "noBusinessPainPenalty") return { ...b, value: 12, enabled: true };
    return b;
  });
  return [
    { ...balanced,     id: "seed-balanced",     name: "Q1 baseline",                savedAt: "2026-04-15T10:00:00.000Z" },
    { ...conservative, id: "seed-conservative", name: "Conservative — tight buffers", savedAt: "2026-04-22T14:00:00.000Z" },
  ];
}

function loadSavedModels() {
  try {
    const raw = window.localStorage?.getItem(SAVED_MODELS_STORAGE_KEY);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed) && parsed.length) return parsed;
    }
  } catch (_) { /* fall through to seed */ }
  return seedSavedModels();
}

// ---------- Forecast Model screen ----------
// Four sections, one rendered at a time, driven by the manager sidebar:
//   recalc   — Forecast recalculation (hero, cards, pipeline, theatre carousel)
//   deals    — Deals (Kanban + Forecast Room modal)
//   backtest — Backtest (calibration replay + leaderboard + misses)
//   config   — Model configuration (saved models, formula, variables, buffers)
// Reuses manager-shape deals from ManagerView so evidence signals, severity,
// and ownership flow straight into the engine.
function ForecastModelScreen({ deals, openDeal, section = "recalc", setSection }) {
  const [model, setModel] = useState(() => ForecastEngine.defaultModel());
  const [selectedDealId, setSelectedDealId] = useState(null);
  const [savedModels, setSavedModels] = useState(() => loadSavedModels());
  // View toggle for the embedded AE DealsScreen in the manager's Deals tab.
  const [mgrDealsView, setMgrDealsView] = useState("cards");
  // Cross-section filter — set from Recalc (e.g. AE breakdown click) or the
  // Deals kanban (column-header click), applied by Deals + theatre carousel.
  // Persisted to localStorage so reload preserves the manager's context.
  const [dealsFilter, setDealsFilter] = useState(() => loadDealsFilter());

  useEffect(() => {
    try { window.localStorage?.setItem(DEALS_FILTER_STORAGE_KEY, JSON.stringify(dealsFilter)); }
    catch (_) { /* localStorage may be unavailable; ignore */ }
  }, [dealsFilter]);

  const onFilterByOwner = (owner) => {
    setDealsFilter((f) => ({ ...f, owner }));
    setSection?.("deals");
  };
  const onToggleStage = (stage) => {
    setDealsFilter((f) => ({ ...f, stage: f.stage === stage ? null : stage }));
  };
  const onClearOwner = () => setDealsFilter((f) => ({ ...f, owner: null }));
  const onClearStage = () => setDealsFilter((f) => ({ ...f, stage: null }));
  const clearDealsFilter = () => setDealsFilter({ owner: null, stage: null });

  useEffect(() => {
    try { window.localStorage?.setItem(SAVED_MODELS_STORAGE_KEY, JSON.stringify(savedModels)); }
    catch (_) { /* localStorage may be unavailable; ignore */ }
  }, [savedModels]);

  const summary = useMemo(() => ForecastEngine.summarize(deals, model), [deals, model]);
  const selected = selectedDealId ? summary.rows.find((r) => r.dealId === selectedDealId) : null;

  const saveCurrentAs = (name) => {
    const trimmed = (name || "").trim();
    if (!trimmed) return;
    const snapshot = {
      ...model,
      id: `m-${Date.now().toString(36)}-${Math.floor(Math.random() * 1000)}`,
      name: trimmed,
      savedAt: new Date().toISOString(),
    };
    setSavedModels((list) => [...list, snapshot]);
  };
  const applySaved = (id) => {
    const m = savedModels.find((s) => s.id === id);
    if (!m) return;
    const fallback = ForecastEngine.defaultModel();
    setModel({
      ...fallback,
      name: m.name,
      // Saved models pre-cadence kept a "period" enum; map it forward when missing.
      cadence: m.cadence || fallback.cadence,
      scenario: m.scenario,
      variables: m.variables.map((v) => ({ ...v })),
      riskBuffers: m.riskBuffers.map((b) => ({ ...b })),
      updatedAt: new Date().toISOString(),
    });
  };
  const deleteSaved = (id) => {
    setSavedModels((list) => list.filter((s) => s.id !== id));
  };

  const setVariable = (key, patch) => setModel((m) => ({
    ...m,
    variables: m.variables.map((v) => (v.key === key ? { ...v, ...patch } : v)),
    updatedAt: new Date().toISOString(),
  }));
  const setBuffer = (key, patch) => setModel((m) => ({
    ...m,
    riskBuffers: m.riskBuffers.map((b) => (b.key === key ? { ...b, ...patch } : b)),
    updatedAt: new Date().toISOString(),
  }));
  const setScenario = (scenario) => setModel((m) => ({ ...m, scenario, updatedAt: new Date().toISOString() }));
  const setName = (name) => setModel((m) => ({ ...m, name }));
  const setCadence = (patch) => setModel((m) => ({
    ...m,
    cadence: { ...(m.cadence || { type: "quarterly", fyStartMonth: 0, offset: 0 }), ...patch },
    updatedAt: new Date().toISOString(),
  }));
  const resetModel = () => setModel(ForecastEngine.defaultModel());

  // Active period for period-aware labels. Recomputed when cadence changes.
  const periodInfo = useMemo(() => {
    const c = model.cadence || { type: "quarterly", fyStartMonth: 0, offset: 0 };
    return ForecastEngine.computePeriod(c.type, c.fyStartMonth, c.offset, ForecastEngine.DEMO_TODAY);
  }, [model.cadence]);

  return (
    <div className="fcal" data-screen-label="Forecast model" data-section={section}>
      {section === "recalc" && (
        <RecalcTab
          summary={summary}
          model={model}
          periodInfo={periodInfo}
          onOpenDeal={openDeal}
          onSelect={setSelectedDealId}
          onFilterByOwner={onFilterByOwner}
          filter={dealsFilter}
          onClearOwner={onClearOwner}
          onClearStage={onClearStage}
          onClearFilter={clearDealsFilter}
        />
      )}
      {section === "deals" && (() => {
        // Reuse the AE DealsScreen (embedded) so the manager gets the same
        // kanban, list view, and filter chips. Manager-shape deals already
        // carry the AE fields needed (value/status/statusLabel/summary/gap)
        // via baseManagerDealFromSeed + enrichManagerSecondAe.
        const DS = window.DealsScreen;
        if (!DS) return null;
        return (
          <DS
            deals={deals}
            openDeal={(id, tab) => openDeal(id, tab || "overview")}
            view={mgrDealsView}
            setView={setMgrDealsView}
            headerEyebrow="Manager forecast"
            headerTitle="Deal forecast review"
            headerSubtitle="Every team deal shown through the same buyer-proof lens as AE Deal Truth."
            embedded
            showPageHeader={false}
          />
        );
      })()}
      {/* "Model" section reuses the Addy letter funnel briefing (same data
          source as the AE view, so the pipeline shape stays consistent
          across surfaces). "backtest" falls through to the same view so
          older bookmarks don't land on a blank screen. */}
      {(section === "config" || section === "backtest") && (
        (() => {
          const PB = window.PipelineBriefing;
          const seed = (typeof window !== "undefined" && window.SEED_DEALS) || [];
          if (!PB) return null;
          return (
            <div className="fcal-briefing-wrap" data-tour="analytics-page">
              <PB
                deals={seed}
                openDeal={openDeal}
                lens="manager"
                showHeader={false}
              />
            </div>
          );
        })()
      )}

      {/* Forecast Room modal — hoisted here so it can open from any tab
          (recalc theatre carousel, deals kanban). */}
      {selected && (
        <ForecastRoom
          row={selected}
          model={model}
          onClose={() => setSelectedDealId(null)}
          onOpenDeal={openDeal}
        />
      )}
    </div>
  );
}

// ---------- helpers ----------
function fmtMoney(v) {
  const n = Math.abs(v || 0);
  const sign = v < 0 ? "-" : "";
  if (n >= 1000000) return `${sign}$${(n / 1000000).toFixed(n >= 10000000 ? 0 : 1)}M`;
  return `${sign}$${Math.round(n / 1000)}K`;
}
function pctText(v) { return `${Math.round((v || 0) * 100)}%`; }
function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); }
function probTone(adjusted, declared) {
  const delta = declared - adjusted;
  if (delta > 0.25) return "warn";
  if (delta > 0.10) return "amber";
  return "good";
}
const CATEGORY_KEY = (c) => c.replace(/\s+/g, "-").toLowerCase();

// Persistent filter chips row. Mounts at the top of any section that respects
// the cross-section filter (Recalc + Deals). Empty state renders nothing so
// the section starts clean when no filters are set.
function FilterBar({ filter, onClearOwner, onClearStage, onClearAll }) {
  const owner = filter?.owner;
  const stage = filter?.stage;
  if (!owner && !stage) return null;
  return (
    <div className="fcal-filter-bar" role="status" aria-label="Active filters">
      <em className="muted small">Filters</em>
      {owner && (
        <button type="button" className="fcal-filter-chip" onClick={onClearOwner} aria-label={`Clear AE filter: ${owner}`}>
          <em className="muted small">AE</em>
          <b>{owner}</b>
          <Icon name="close" size={11} />
        </button>
      )}
      {stage && (
        <button type="button" className="fcal-filter-chip" onClick={onClearStage} aria-label={`Clear stage filter: ${stage}`}>
          <em className="muted small">Stage</em>
          <b>{stage}</b>
          <Icon name="close" size={11} />
        </button>
      )}
      {owner && stage && (
        <button type="button" className="fcal-filter-clear" onClick={onClearAll}>Clear all</button>
      )}
    </div>
  );
}

// Inline explanation tooltip. CSS-only on hover; opens on focus for keyboard
// users. Place next to a label, header, or column title.
function InfoTip({ text, label = "More info", placement = "top" }) {
  return (
    <span className={`fcal-tip fcal-tip-${placement}`}>
      <button
        type="button"
        className="fcal-tip-icon"
        aria-label={label}
        // tabIndex stays default 0; mousedown.preventDefault stops focus
        // shifting away from interactive parents.
        onMouseDown={(e) => e.preventDefault()}
      >
        i
      </button>
      <span role="tooltip" className="fcal-tip-body">{text}</span>
    </span>
  );
}

function periodSummary(period, cadenceType = "quarterly") {
  if (!period) return "";
  const cadenceLabel = ({
    monthly: "Monthly",
    quarterly: "Quarterly",
    semester: "Semester",
    annual: "Annual",
  })[cadenceType] || "Quarterly";
  const fmt = (d) => d.toLocaleString("en-US", { month: "short", day: "numeric" });
  return `${cadenceLabel} · ${period.name} · ${fmt(period.start)}-${fmt(period.end)} · ${period.daysRemaining}d left`;
}

function ForecastSectionHeader({ eyebrow = "Manager forecast", title, subtitle, tone = "good", meta, children, level = 1 }) {
  const HeadingTag = level === 2 ? "h2" : "h1";
  return (
    <header className="platform-page-header fcal-section-header">
      <div className="platform-page-copy">
        <span className="platform-eyebrow">
          <span className={`cp-eyebrow-dot ${tone}`} aria-hidden />
          {eyebrow}
        </span>
        <HeadingTag className="platform-page-title">{title}</HeadingTag>
        {subtitle && <p className="platform-page-subtitle">{subtitle}</p>}
      </div>
      {(meta || children) && (
        <div className="platform-page-meta">
          {meta}
          {children}
        </div>
      )}
    </header>
  );
}

function ScenarioPills({ value, onChange }) {
  const options = [
    { key: "conservative", label: "Conservative" },
    { key: "balanced",     label: "Balanced" },
    { key: "aggressive",   label: "Aggressive" },
  ];
  return (
    <div className="fcal-scenarios" role="group" aria-label="Scenario">
      {options.map((o) => (
        <button
          key={o.key}
          type="button"
          className={`fcal-scenario ${value === o.key ? "is-active" : ""}`}
          aria-pressed={value === o.key}
          onClick={() => onChange(o.key)}
        >
          {o.label}
        </button>
      ))}
    </div>
  );
}

function CategoryChip({ category }) {
  const tone =
    category === "Commit" ? "good" :
    category === "Most likely" ? "info" :
    category === "Best case" ? "accent" :
    "warn";
  return <span className={`chip ${tone}`} style={{ height: 22, fontSize: 11 }}>{category}</span>;
}

function SummaryCard({ label, value, sub, tone = "muted", highlight = false, tip }) {
  return (
    <div className={`fcal-card tone-${tone} ${highlight ? "is-highlight" : ""}`}>
      <div className="fcal-card-label">
        {label}
        {tip && <InfoTip text={tip} label={`What is ${typeof label === "string" ? label : "this"}?`} />}
      </div>
      <div className="fcal-card-value">{value}</div>
      <div className="fcal-card-sub muted">{sub}</div>
    </div>
  );
}

// Build an SVG path for a donut slice (annular sector). Returns "" if the
// sweep is non-positive, so zero-value slices simply don't render.
function donutSlicePath(cx, cy, rOuter, rInner, startAngle, endAngle) {
  const sweep = endAngle - startAngle;
  if (sweep <= 0) return "";
  const x1 = cx + rOuter * Math.cos(startAngle);
  const y1 = cy + rOuter * Math.sin(startAngle);
  const x2 = cx + rOuter * Math.cos(endAngle);
  const y2 = cy + rOuter * Math.sin(endAngle);
  const x3 = cx + rInner * Math.cos(endAngle);
  const y3 = cy + rInner * Math.sin(endAngle);
  const x4 = cx + rInner * Math.cos(startAngle);
  const y4 = cy + rInner * Math.sin(startAngle);
  const largeArc = sweep > Math.PI ? 1 : 0;
  return `M ${x1} ${y1} A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${x2} ${y2} L ${x3} ${y3} A ${rInner} ${rInner} 0 ${largeArc} 0 ${x4} ${y4} Z`;
}

function PipelineDonut({ pipeline, total }) {
  const order = ["Commit", "Most likely", "Best case", "Omitted"];
  const slices = order.map((k) => ({ k, v: pipeline[k] || 0 }));
  const sum = slices.reduce((s, x) => s + x.v, 0) || 1;
  const cx = 120, cy = 120, rOut = 90, rIn = 60;
  let cur = -Math.PI / 2;
  return (
    <div className="fcal-donut">
      <div className="fcal-donut-wrap">
        <svg viewBox="0 0 240 240" className="fcal-donut-svg" role="img" aria-label="Pipeline by recommended category">
          <circle cx={cx} cy={cy} r={rOut} className="fcal-donut-track" />
          {slices.map((seg) => {
            const angle = (seg.v / sum) * Math.PI * 2;
            const path = donutSlicePath(cx, cy, rOut, rIn, cur, cur + angle);
            cur += angle;
            return (
              <path key={seg.k} d={path} className={`fcal-donut-slice seg-${CATEGORY_KEY(seg.k)}`}>
                <title>{seg.k}: {fmtMoney(seg.v)} ({Math.round((seg.v / sum) * 100)}%)</title>
              </path>
            );
          })}
          <text x={cx} y={cy - 4} textAnchor="middle" className="fcal-donut-total">{fmtMoney(total)}</text>
          <text x={cx} y={cy + 16} textAnchor="middle" className="fcal-donut-sub">evidence-adjusted</text>
        </svg>
      </div>
      <ul className="fcal-donut-legend">
        {slices.map((seg) => {
          const pct = Math.round((seg.v / sum) * 100);
          return (
            <li key={seg.k} className="fcal-donut-leg">
              <em className={`fcal-donut-key seg-${CATEGORY_KEY(seg.k)}`} aria-hidden="true" />
              <span className="fcal-donut-leg-label">{seg.k}</span>
              <span className="fcal-donut-leg-value">{fmtMoney(seg.v)}</span>
              <span className="fcal-donut-leg-pct muted small">{pct}%</span>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

function PipelineBar({ pipeline, total }) {
  const order = ["Commit", "Most likely", "Best case", "Omitted"];
  const segs = order.map((k) => ({ k, v: pipeline[k] || 0 }));
  const totalAdj = segs.reduce((s, x) => s + x.v, 0) || 1;
  return (
    <section className="fcal-pipeline">
      <div className="fcal-pipeline-head">
        <h3>
          Pipeline by recommended category
          <InfoTip text="Each segment is the sum of evidence-adjusted amount for deals the model put in that bucket. Hover or look at the legend for $." />
        </h3>
        <em className="muted small">Weighted by evidence — total {fmtMoney(total)}</em>
      </div>
      <div className="fcal-pipeline-bar" role="img" aria-label="Pipeline by category">
        {segs.map((seg) => {
          const flex = seg.v / totalAdj;
          if (flex < 0.001) return null;
          return (
            <span
              key={seg.k}
              className={`fcal-pipeline-seg seg-${CATEGORY_KEY(seg.k)}`}
              style={{ flex }}
              title={`${seg.k}: ${fmtMoney(seg.v)}`}
            >
              {flex >= 0.12 && (
                <span className="fcal-pipeline-seg-label">
                  <em>{seg.k}</em>
                  <b>{fmtMoney(seg.v)}</b>
                </span>
              )}
            </span>
          );
        })}
      </div>
      <div className="fcal-pipeline-legend">
        {segs.map((seg) => (
          <span key={seg.k} className="fcal-leg">
            <em className={`seg-${CATEGORY_KEY(seg.k)}`} />
            {seg.k} <b>{fmtMoney(seg.v)}</b>
          </span>
        ))}
      </div>
    </section>
  );
}

// ---------- Forecast Room (manager-side deal modal) ----------
// Same UX shape as the AE Deal Detail modal — just forecast-focused content.
// Three tabs:
//   1. Forecast story  — recommendation banner, declared/CRM/evidence comparison, reasons
//   2. Probability math — step-by-step waterfall (base × variable multipliers × buffers)
//   3. Triggers & flags — risk buffers that fired + forecast-theatre contradictions
function ForecastRoom({ row, model, onClose, onOpenDeal }) {
  const [tab, setTab] = useState("story");
  const modalRef = useRef(null);

  useEffect(() => {
    if (modalRef.current) modalRef.current.focus({ preventScroll: true });
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (!row) return null;
  const tone = row.severity === "critical" ? "warn" : row.severity === "warn" ? "amber" : "good";
  const recommendation = recommendForRow(row);
  const canOpenDealDetail = onOpenDeal && row.dealId && !row.dealId.startsWith("mgr-");

  const tabs = [
    { k: "story",    label: "Forecast story" },
    { k: "math",     label: "Probability math" },
    { k: "triggers", label: "Triggers & flags" },
  ];

  return (
    <div className="modal-back" onClick={onClose}>
      <div
        ref={modalRef}
        className="modal forecast-room"
        onClick={(e) => e.stopPropagation()}
        data-dialog-root="true"
        data-screen-label="Forecast Room"
        role="dialog"
        aria-modal="true"
        aria-label={`Forecast room for ${row.company}`}
        tabIndex={-1}
      >
        <div className="modal-head">
          <div className="modal-status">
            <div className="modal-title-row">
              <h2 className="h2">{row.company}</h2>
              <span className={`deal-status-badge tone-${tone}`}>
                <span aria-hidden="true" />
                Forecast · {recommendation.label}
              </span>
            </div>
            <div className="deal-meta-badges" aria-label="Deal metadata">
              <span className="deal-meta-badge">{fmtMoney(row.declaredAmount)}</span>
              <span className="deal-meta-badge">{row.stage}</span>
              <span className="deal-meta-badge">{row.closeDate || "—"}</span>
              <span className="deal-meta-badge">Owner · {row.owner}</span>
            </div>
          </div>
          <div className="forecast-room-head-actions">
            {canOpenDealDetail && (
              <button type="button" className="btn sm" onClick={() => onOpenDeal(row.dealId)}>Open deal</button>
            )}
            <button type="button" className="btn sm ghost" onClick={onClose} aria-label="Close forecast room">
              <Icon name="close" size={14} />
            </button>
          </div>
        </div>

        <div className="modal-body">
          <div className="dd-tabs">
            <div className="dd-tabs-l">
              {tabs.map((t) => (
                <button
                  key={t.k}
                  type="button"
                  onClick={() => setTab(t.k)}
                  className={`dd-tab ${tab === t.k ? "is-active" : ""}`}
                >
                  {t.label}
                </button>
              ))}
            </div>
          </div>

          {tab === "story"    && <FcRoomStoryTab    row={row} recommendation={recommendation} tone={tone} />}
          {tab === "math"     && <FcRoomMathTab     row={row} model={model} />}
          {tab === "triggers" && <FcRoomTriggersTab row={row} />}
        </div>
      </div>
    </div>
  );
}

function FcRoomStoryTab({ row, recommendation, tone }) {
  const declaredFcst = Math.round(row.declaredAmount * row.declaredProbability);
  const crmFcst = row.crmWeightedAmount;
  const adjFcst = row.nudgeAdjustedAmount;
  const max = Math.max(declaredFcst, crmFcst, adjFcst, 1);
  return (
    <div className="fcal-room-tab fcal-room-story">
      <div className={`fcal-risk-reco tone-${recommendation.tone}`} role="status">
        <span className="fcal-risk-reco-label">Recommended · {recommendation.label}</span>
        <p>{recommendation.text}</p>
      </div>

      <div className="fcal-room-section">
        <h3>Forecast comparison</h3>
        <div className="fcal-room-compare">
          <CompareRoomRow
            label="Declared"
            tip="Amount × the rep's stated probability."
            amount={declaredFcst}
            pct={Math.round(row.declaredProbability * 100)}
            max={max}
            tone="muted"
          />
          <CompareRoomRow
            label="CRM weighted"
            tip="Amount × the default probability for this deal's CRM stage."
            amount={crmFcst}
            pct={Math.round(row.crmProbability * 100)}
            max={max}
            tone="info"
          />
          <CompareRoomRow
            label="Evidence-adjusted"
            tip="Amount × probability after model variables and risk buffers are applied."
            amount={adjFcst}
            pct={Math.round(row.adjustedProbability * 100)}
            max={max}
            tone={tone}
            highlight
          />
        </div>
      </div>

      <div className="fcal-room-section">
        <h3 style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
          Why the model lowered this
          <AgentChip
            agentId="forecaster"
            nudge={{ id: `forecast:${row.dealId}`, title: `Forecast adjustment for ${row.company}`, kind: "FORECAST DRIFT" }}
            deal={{ id: row.dealId, company: row.company }}
            compact
          />
        </h3>
        {row.reasons.length > 0 ? (
          <ul className="fcal-room-reasons">
            {row.reasons.map((r, i) => <li key={i}>{r}</li>)}
          </ul>
        ) : (
          <p className="muted small">No major risk reasons identified — this deal holds up against the model.</p>
        )}
      </div>
    </div>
  );
}

function CompareRoomRow({ label, tip, amount, pct, max, tone, highlight = false }) {
  const width = max > 0 ? Math.max(2, (amount / max) * 100) : 0;
  return (
    <div className={`fcal-room-compare-row ${highlight ? "is-highlight" : ""}`}>
      <div className="fcal-room-compare-label">
        {label}
        {tip && <InfoTip text={tip} />}
      </div>
      <div className="fcal-room-compare-track">
        <span className={`fcal-room-compare-fill tone-${tone}`} style={{ width: `${width}%` }} />
      </div>
      <b>{fmtMoney(amount)}</b>
      <em className="muted small">{pct}%</em>
    </div>
  );
}

function FcRoomMathTab({ row, model }) {
  // Reproduce the engine's stepwise probability path so the manager can see
  // exactly which variable / buffer moved the number, and by how much.
  const baseProb = row.crmProbability;
  let running = baseProb;
  const steps = [{
    label: `Stage probability (${row.stage})`,
    sublabel: "base",
    multiplier: null,
    value: baseProb,
    isBase: true,
  }];

  // Variables in model order. Skip disabled and stageProbability (the base).
  // Skip multipliers === 1 (no effect) so the waterfall isn't padded with no-ops.
  model.variables.forEach((v) => {
    if (!v.enabled || v.key === "stageProbability") return;
    const m = row.multipliers[v.key];
    if (m == null || m === 1) return;
    running *= m;
    steps.push({
      label: v.label,
      sublabel: `weight ${v.weight}`,
      multiplier: m,
      value: running,
    });
  });

  // Buffers: parsed from bufferReasons string format `Label (-X%) — reason`.
  (row.bufferReasons || []).forEach((reasonStr) => {
    const match = reasonStr.match(/^(.*?)\s*\((-\d+)%\)\s*—\s*(.*)$/);
    if (!match) return;
    const [, label, pctStr, reason] = match;
    const pct = parseInt(pctStr, 10);
    const m = 1 + pct / 100;
    running *= m;
    steps.push({
      label,
      sublabel: reason,
      multiplier: m,
      value: running,
      isBuffer: true,
    });
  });

  return (
    <div className="fcal-room-tab fcal-room-math">
      <p className="fcal-room-intro">
        How the probability moved from <b>{Math.round(baseProb * 100)}%</b> at stage default to <b>{Math.round(row.adjustedProbability * 100)}%</b> after evidence checks.
      </p>
      <ol className="fcal-room-waterfall">
        {steps.map((step, i) => {
          const pct = Math.round(Math.max(0, Math.min(1, step.value)) * 100);
          const dropPt = i > 0 && step.multiplier != null ? Math.round((1 - step.multiplier) * 100) : 0;
          return (
            <li key={i} className={`fcal-room-step ${step.isBase ? "is-base" : ""} ${step.isBuffer ? "is-buffer" : ""}`}>
              <div className="fcal-room-step-l">
                <b>{step.label}</b>
                {step.sublabel && <em className="muted small">{step.sublabel}</em>}
              </div>
              <div className="fcal-room-step-bar">
                <span className="fcal-room-step-fill" style={{ width: `${pct}%` }} />
              </div>
              <div className="fcal-room-step-r">
                {dropPt > 0 && <em className="muted small">−{dropPt}pt</em>}
                <b>{pct}%</b>
              </div>
            </li>
          );
        })}
      </ol>
      {steps.length === 1 && (
        <p className="muted small">No variables or buffers fired on this deal — the call stands at stage default.</p>
      )}
    </div>
  );
}

function FcRoomTriggersTab({ row }) {
  return (
    <div className="fcal-room-tab fcal-room-triggers">
      <div className="fcal-room-section">
        <h3>Risk buffers fired</h3>
        {row.bufferReasons.length > 0 ? (
          <ul className="fcal-room-list">
            {row.bufferReasons.map((reason, i) => (
              <li key={i} className="fcal-room-trigger">
                <span className="fcal-room-trigger-bullet" aria-hidden="true" />
                <span>{reason}</span>
              </li>
            ))}
          </ul>
        ) : (
          <p className="muted small">No buffers applied to this deal.</p>
        )}
      </div>

      <div className="fcal-room-section">
        <h3>Forecast theatre flags</h3>
        {row.theatreFlags.length > 0 ? (
          <ul className="fcal-room-flags">
            {row.theatreFlags.map((flag, i) => (
              <li key={i} className="fcal-room-flag">
                <p className="fcal-room-flag-text">{flag}</p>
                <div className="fcal-room-flag-ask">
                  <em className="cp-eyebrow" style={{ display: "inline-flex" }}>
                    <span className="cp-eyebrow-dot amber" />
                    Suggested ask
                  </em>
                  <p>"{suggestedChallenge(flag)}"</p>
                </div>
              </li>
            ))}
          </ul>
        ) : (
          <p className="muted small">No flags raised — declared aligns with evidence.</p>
        )}
      </div>
    </div>
  );
}

// ---------- Recalculation tab ----------
// Widget catalog for the recalc page. Each kind maps to a render branch
// below. Mirrors the AE-cockpit SECTION_TYPES catalogue so the picker UX
// reads the same on both surfaces.
const RECALC_WIDGETS = [
  { kind: "hero",    label: "Gap inspection",      desc: "At-risk amount with composition bar",   icon: "trend" },
  { kind: "deals",   label: "Driving the gap",     desc: "Ranked deals causing the most exposure", icon: "target" },
  { kind: "aes",     label: "By AE",               desc: "Per-AE rollup with inline gap bars",     icon: "users" },
  { kind: "theatre", label: "Challenge in review", desc: "Forecast-theatre flags as a card deck",  icon: "note" },
];

// Standard widget shell — eyebrow with grip + title + remove, body slot.
// The eyebrow row is the HTML5 drag source for slot-reorder; the body is
// not draggable so internal interactions (carousel swipe, row clicks) work.
function RecalcWidget({ title, tone, dragHead, onRemove, children, className = "" }) {
  return (
    <section className={`recalc-widget ${className}`}>
      <header className="recalc-widget-head" {...dragHead}>
        <div className="cp-eyebrow">
          <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          {tone && <span className={`cp-eyebrow-dot ${tone}`} />}
          <span>{title}</span>
          {onRemove && (
            <button type="button" className="cp-slot-remove" onClick={onRemove} aria-label="Remove section" title="Remove section">
              <Icon name="close" size={11} />
            </button>
          )}
        </div>
      </header>
      <div className="recalc-widget-body">{children}</div>
    </section>
  );
}

// Modal picker — same recipe as the AE-cockpit SectionPicker, scoped to the
// recalc widget catalog.
function RecalcSectionPicker({ onSelect, onClose }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="picker-back" onClick={onClose}>
      <div className="picker" onClick={(e) => e.stopPropagation()} role="dialog" aria-label="Add a section">
        <header className="picker-h">
          <span className="eyebrow">Add a section</span>
          <button type="button" className="btn sm ghost" onClick={onClose} aria-label="Close">
            <Icon name="close" size={12} />
          </button>
        </header>
        <div className="picker-list">
          {RECALC_WIDGETS.map((s) => (
            <button key={s.kind} type="button" className="picker-item" onClick={() => onSelect(s.kind)}>
              <span className="picker-item-ic"><Icon name={s.icon} size={14} /></span>
              <span className="picker-item-body">
                <b>{s.label}</b>
                <em>{s.desc}</em>
              </span>
              <Icon name="arrow" size={11} className="picker-item-chev" />
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

function RecalcTab({ summary, model, periodInfo, onOpenDeal, onSelect, onFilterByOwner, filter = {}, onClearOwner, onClearStage, onClearFilter }) {
  if (!summary.rows.length) {
    return <div className="fcal-empty"><p className="muted">No open deals available to forecast.</p></div>;
  }
  const gapPct = summary.forecastGapPercentage;
  const headlineTone = gapPct >= 25 ? "warn" : gapPct >= 10 ? "amber" : "good";
  const sortedRows = [...summary.rows].sort((a, b) => Math.abs(b.forecastGap) - Math.abs(a.forecastGap));

  // Single-bar composition: supported portion (evidence-adjusted) plus
  // at-risk portion (the gap), summing to declared.
  const totalDeclared = summary.declaredForecast || 0;
  const supported = Math.min(summary.nudgeAdjustedForecast || 0, totalDeclared);
  const atRisk = Math.max(0, totalDeclared - supported);
  const supportedPct = totalDeclared > 0 ? (supported / totalDeclared) * 100 : 0;
  const atRiskPct = totalDeclared > 0 ? Math.max(0, 100 - supportedPct) : 0;

  // How many deals contribute to 80% of the gap — the inspection count.
  const gapDealsSorted = sortedRows.filter((r) => r.forecastGap > 0);
  const gapTotal = gapDealsSorted.reduce((s, r) => s + r.forecastGap, 0);
  let gapDealsToInspect = 0; let runningGap = 0;
  for (const row of gapDealsSorted) {
    if (runningGap >= gapTotal * 0.8) break;
    runningGap += row.forecastGap;
    gapDealsToInspect += 1;
  }
  const commitAtRiskCount = sortedRows.filter((r) => r.declaredCategory === "Commit" && r.recommendedCategory !== "Commit").length;

  // Scan panel inputs — top gap drivers (rank list) and per-AE rollup.
  const topDeals = gapDealsSorted.slice(0, 6);
  const byOwner = aggregateByOwner(summary.rows);

  // Slot-based layout — same pattern as the AE cockpit. Each slot holds a
  // widget kind (or null for an empty placeholder). Slots reorder via HTML5
  // drag-and-drop on their eyebrow row; the picker fills empty slots.
  const [slots, setSlots] = useState(["hero", "deals", "aes", "theatre"]);
  const [dragSlotIdx, setDragSlotIdx] = useState(null);
  const [dragOverSlot, setDragOverSlot] = useState(null);
  const [pickerSlot, setPickerSlot] = useState(null);

  const dragSourceProps = (slotIdx) => ({
    draggable: true,
    onDragStart: (e) => {
      setDragSlotIdx(slotIdx);
      e.dataTransfer.effectAllowed = "move";
      try { e.dataTransfer.setData("text/plain", String(slotIdx)); } catch (_) {}
    },
    onDragEnd: () => { setDragSlotIdx(null); setDragOverSlot(null); },
  });
  const slotDropProps = (slotIdx) => ({
    onDragOver: (e) => {
      if (dragSlotIdx == null) return;
      e.preventDefault();
      e.dataTransfer.dropEffect = "move";
      if (dragOverSlot !== slotIdx) setDragOverSlot(slotIdx);
    },
    onDragLeave: () => { setDragOverSlot((s) => (s === slotIdx ? null : s)); },
    onDrop: (e) => {
      e.preventDefault();
      if (dragSlotIdx == null || dragSlotIdx === slotIdx) {
        setDragSlotIdx(null); setDragOverSlot(null); return;
      }
      setSlots((curr) => {
        const out = [...curr];
        const tmp = out[dragSlotIdx];
        out[dragSlotIdx] = out[slotIdx];
        out[slotIdx] = tmp;
        return out;
      });
      setDragSlotIdx(null);
      setDragOverSlot(null);
    },
  });
  const fillSlot = (idx, kind) => {
    setSlots((curr) => { const out = [...curr]; out[idx] = kind; return out; });
    setPickerSlot(null);
  };
  const addSlot = () => setSlots((curr) => [...curr, null]);
  const removeSlot = (idx) => setSlots((curr) => curr.filter((_, i) => i !== idx));

  const renderWidget = (kind, idx) => {
    const dragHead = dragSourceProps(idx);
    const onRemove = () => removeSlot(idx);
    switch (kind) {
      case "hero":
        return (
          <RecalcWidget
            title={summary.forecastGap > 0 ? "Gap inspection" : "Forecast holds"}
            tone={headlineTone}
            dragHead={dragHead}
            onRemove={onRemove}
            className="recalc-widget--hero"
          >
            <div className={`fcal-hero-inline tone-${headlineTone}`}>
              <h2 className="fcal-hero-headline">
                {summary.forecastGap > 0 ? (
                  <>
                    <b className={`tone-${headlineTone}`}>{fmtMoney(summary.forecastGap)}</b>
                    <span className="fcal-hero-suffix">at risk</span>
                  </>
                ) : (
                  <b className="tone-good">Forecast holds</b>
                )}
              </h2>
              <p className="fcal-hero-sub">
                <b>{fmtMoney(supported)}</b> of <b>{fmtMoney(totalDeclared)}</b> backed by evidence
              </p>
              <div
                className="fcal-hero-bar"
                role="img"
                aria-label={`${fmtMoney(supported)} supported, ${fmtMoney(atRisk)} at risk of ${fmtMoney(totalDeclared)} declared`}
              >
                <span className="fcal-hero-bar-supported" style={{ width: `${supportedPct}%` }} />
                {atRiskPct > 0 && (
                  <span className={`fcal-hero-bar-atrisk tone-${headlineTone}`} style={{ width: `${atRiskPct}%` }} />
                )}
              </div>
            </div>
          </RecalcWidget>
        );

      case "deals":
        if (!topDeals.length) return (
          <RecalcWidget title="Driving the gap" tone="good" dragHead={dragHead} onRemove={onRemove}>
            <p className="muted small">No deals are driving the gap right now.</p>
          </RecalcWidget>
        );
        return (
          <RecalcWidget
            title="Driving the gap"
            tone={headlineTone}
            dragHead={dragHead}
            onRemove={onRemove}
          >
            {summary.forecastGap > 0 && (
              <p className="recalc-widget-sub">
                <b>{gapDealsToInspect}</b> deal{gapDealsToInspect === 1 ? "" : "s"} drive 80% of the gap
                {commitAtRiskCount > 0 && <> · <b>{commitAtRiskCount}</b> Commit to downgrade</>}
              </p>
            )}
            <ol className="fcal-rank-list">
              {topDeals.map((row, i) => {
                const downgrade = row.declaredCategory && row.recommendedCategory
                  && row.declaredCategory !== row.recommendedCategory;
                const rowTone = (row.declaredCategory === "Commit" && row.recommendedCategory !== "Commit")
                  ? "warn"
                  : downgrade ? "amber" : "muted";
                return (
                  <li key={row.dealId} className={`fcal-rank-row tone-${rowTone}`}>
                    <button
                      type="button"
                      className="fcal-rank-btn"
                      onClick={() => onSelect(row.dealId)}
                      aria-label={`${row.company}: ${fmtMoney(row.forecastGap)} gap. Open forecast room.`}
                    >
                      <span className="fcal-rank-num">{String(i + 1).padStart(2, "0")}</span>
                      <span className="fcal-rank-co">
                        <b>{row.company}</b>
                        <em>
                          {shortenOwner(row.owner)}
                          {downgrade && <> · {row.declaredCategory} → {row.recommendedCategory}</>}
                          {!downgrade && <> · {row.stage}</>}
                        </em>
                      </span>
                      <span className="fcal-rank-gap">
                        <b className={`tone-${rowTone}`}>{fmtMoney(row.forecastGap)}</b>
                        <em>gap</em>
                      </span>
                    </button>
                  </li>
                );
              })}
            </ol>
          </RecalcWidget>
        );

      case "aes":
        if (!byOwner.length) return (
          <RecalcWidget title="By AE" tone="good" dragHead={dragHead} onRemove={onRemove}>
            <p className="muted small">No AE breakdown to show.</p>
          </RecalcWidget>
        );
        return (
          <RecalcWidget
            title="By AE · click to filter"
            tone="amber"
            dragHead={dragHead}
            onRemove={onRemove}
          >
            <ul className="fcal-ae-list">
              {byOwner.map((o) => {
                const total = o.declaredTotal || 1;
                const supPct = Math.max(0, Math.min(100, (o.adjustedTotal / total) * 100));
                const ownerGapPct = Math.max(0, 100 - supPct);
                const aeTone = o.gapPct >= 25 ? "warn" : o.gapPct >= 10 ? "amber" : "good";
                const isActive = filter.owner === o.owner;
                return (
                  <li key={o.owner}>
                    <button
                      type="button"
                      className={`fcal-ae-row tone-${aeTone} ${isActive ? "is-active" : ""}`}
                      onClick={() => onFilterByOwner(isActive ? null : o.owner)}
                      aria-pressed={isActive}
                    >
                      <div className="fcal-ae-top">
                        <span className="fcal-ae-name">
                          <b>{shortenOwner(o.owner)}</b>
                          <em>{o.dealCount} deal{o.dealCount === 1 ? "" : "s"}</em>
                        </span>
                        <span className="fcal-ae-gap">
                          <b className={`tone-${aeTone}`}>{fmtMoney(o.gap)}</b>
                          <em>{o.gapPct}% gap</em>
                        </span>
                      </div>
                      <div className="fcal-ae-bar" aria-hidden="true">
                        <span className="fcal-ae-bar-sup" style={{ width: `${supPct}%` }} />
                        <span className={`fcal-ae-bar-gap tone-${aeTone}`} style={{ width: `${ownerGapPct}%` }} />
                      </div>
                    </button>
                  </li>
                );
              })}
            </ul>
          </RecalcWidget>
        );

      case "theatre":
        return (
          <RecalcWidget
            title="Challenge in your forecast review"
            tone={headlineTone}
            dragHead={dragHead}
            onRemove={onRemove}
          >
            <TheatreList
              rows={summary.flaggedRows}
              onSelect={onSelect}
              onOpenDeal={onOpenDeal}
              filter={filter}
              onClearOwner={onClearOwner}
              onClearStage={onClearStage}
              embedded
            />
          </RecalcWidget>
        );

      default:
        return null;
    }
  };

  return (
    <>
      <ForecastSectionHeader
        eyebrow="Manager forecast"
        title="Forecast recalculation"
        tone={headlineTone}
        subtitle={`${periodSummary(periodInfo, (model.cadence || {}).type)}. Compare declared forecast with buyer-backed evidence before the team commits.`}
        meta={
          <>
            <span><b>{fmtMoney(summary.declaredForecast)}</b> declared</span>
            <span><b>{fmtMoney(summary.nudgeAdjustedForecast)}</b> supported</span>
            <span><b>{fmtMoney(summary.forecastGap)}</b> gap</span>
          </>
        }
      />
      <FilterBar
        filter={filter}
        onClearOwner={onClearOwner}
        onClearStage={onClearStage}
        onClearAll={onClearFilter}
      />
      <div className="cp-stack recalc-stack">
        {slots.map((kind, idx) => (
          <div
            key={idx}
            className={`cp-slot ${kind ? "is-occupied" : "is-empty"} ${dragOverSlot === idx && dragSlotIdx != null ? "is-drop-target" : ""}`}
            {...slotDropProps(idx)}
          >
            {kind ? renderWidget(kind, idx) : (
              <button
                type="button"
                className="cp-slot-empty"
                onClick={() => setPickerSlot(idx)}
                aria-label="Add a section to this slot"
              >
                <Icon name="plus" size={14} />
                <span>{dragSlotIdx != null ? "Drop here" : "Add section"}</span>
              </button>
            )}
          </div>
        ))}
      </div>
      <div className="cp-stack-foot">
        <button type="button" className="cp-add-slot" onClick={addSlot}>
          <Icon name="plus" size={11} /> Add slot
        </button>
      </div>
      {pickerSlot != null && (
        <RecalcSectionPicker
          onSelect={(kind) => fillSlot(pickerSlot, kind)}
          onClose={() => setPickerSlot(null)}
        />
      )}
    </>
  );
}

// Aggregate forecast rows by owner. Used by both the column chart and the
// (now-deprecated) horizontal list view.
function aggregateByOwner(rows) {
  const map = new Map();
  rows.forEach((row) => {
    const owner = row.owner || "—";
    if (!map.has(owner)) {
      map.set(owner, {
        owner,
        dealCount: 0,
        declaredTotal: 0,
        adjustedTotal: 0,
        commitAtRisk: 0,
        theatreFlagCount: 0,
      });
    }
    const entry = map.get(owner);
    entry.dealCount += 1;
    entry.declaredTotal += Math.round((row.declaredAmount || 0) * (row.declaredProbability || 0));
    entry.adjustedTotal += row.nudgeAdjustedAmount || 0;
    if (row.declaredCategory === "Commit" && row.recommendedCategory !== "Commit") entry.commitAtRisk += 1;
    entry.theatreFlagCount += (row.theatreFlags || []).length;
  });
  return Array.from(map.values())
    .map((e) => ({
      ...e,
      gap: e.declaredTotal - e.adjustedTotal,
      gapPct: e.declaredTotal > 0
        ? Math.round(((e.declaredTotal - e.adjustedTotal) / e.declaredTotal) * 100)
        : 0,
    }))
    .sort((a, b) => b.gap - a.gap);
}

function shortenOwner(name) {
  if (!name || name.length <= 12) return name;
  const parts = name.split(/\s+/);
  return parts[0] || name;
}

// Vertical column chart: each AE is a stacked bar with supported (bottom) +
// at-risk (top, striped) segments, gap percentage labeled above. Click a
// column → filter Deals to that owner. Different visual primitive from the
// horizontal bars elsewhere in the tab.
function OwnerColumns({ rows, onSelectOwner, activeOwner }) {
  const byOwner = useMemo(() => aggregateByOwner(rows), [rows]);
  if (!byOwner.length) return null;
  const max = Math.max(...byOwner.map((o) => o.declaredTotal), 1);
  const ticks = [0, max * 0.5, max];

  const W = 460;
  const H = 240;
  const padL = 56;
  const padR = 12;
  const padT = 28;
  const padB = 50;
  const innerW = W - padL - padR;
  const innerH = H - padT - padB;
  const N = byOwner.length;
  const colWidth = innerW / N;
  const barWidth = Math.max(20, Math.min(44, colWidth * 0.55));
  const yToPx = (v) => H - padB - (v / max) * innerH;

  return (
    <svg
      viewBox={`0 0 ${W} ${H}`}
      className="fcal-owner-cols-svg"
      role="img"
      aria-label="Forecast gap by AE"
      preserveAspectRatio="xMidYMid meet"
    >
      {ticks.map((t) => (
        <g key={t} className="fcal-owner-cols-grid">
          <line x1={padL} x2={W - padR} y1={yToPx(t)} y2={yToPx(t)} />
          <text x={padL - 8} y={yToPx(t) + 4} textAnchor="end" className="fcal-owner-cols-axis">
            {t === 0 ? "$0" : fmtMoney(t)}
          </text>
        </g>
      ))}
      <line x1={padL} x2={W - padR} y1={H - padB} y2={H - padB} className="fcal-owner-cols-baseline" />

      {byOwner.map((o, i) => {
        const xCenter = padL + (i + 0.5) * colWidth;
        const xLeft = xCenter - barWidth / 2;
        const supportedTop = yToPx(o.adjustedTotal);
        const supportedHeight = (o.adjustedTotal / max) * innerH;
        const atRiskTop = yToPx(o.declaredTotal);
        const atRiskHeight = ((o.declaredTotal - o.adjustedTotal) / max) * innerH;
        const tone = o.gapPct >= 25 ? "warn" : o.gapPct >= 10 ? "amber" : "good";
        const isActive = activeOwner === o.owner;
        const onClick = onSelectOwner ? () => onSelectOwner(o.owner) : undefined;

        return (
          <g
            key={o.owner}
            className={`fcal-owner-col ${onClick ? "is-clickable" : ""} ${isActive ? "is-active" : ""}`}
            onClick={onClick}
            role={onClick ? "button" : undefined}
            tabIndex={onClick ? 0 : undefined}
            aria-pressed={onClick ? isActive : undefined}
            aria-label={onClick ? `Show ${o.owner}'s deals` : undefined}
            onKeyDown={(e) => {
              if (!onClick) return;
              if (e.key === "Enter" || e.key === " ") {
                e.preventDefault();
                onClick();
              }
            }}
          >
            <rect
              x={padL + i * colWidth}
              y={padT - 12}
              width={colWidth}
              height={innerH + (padB - 12)}
              className="fcal-owner-col-hit"
            />
            {atRiskHeight > 1 && (
              <rect
                x={xLeft}
                y={atRiskTop}
                width={barWidth}
                height={atRiskHeight}
                rx="3"
                className={`fcal-owner-col-atrisk tone-${tone}`}
              />
            )}
            {supportedHeight > 1 && (
              <rect
                x={xLeft}
                y={supportedTop}
                width={barWidth}
                height={supportedHeight}
                rx="3"
                className="fcal-owner-col-supported"
              />
            )}
            {o.declaredTotal > 0 && (
              <text
                x={xCenter}
                y={atRiskTop - 8}
                textAnchor="middle"
                className={`fcal-owner-col-gap tone-${tone}`}
              >
                −{o.gapPct}%
              </text>
            )}
            <text x={xCenter} y={H - padB + 16} textAnchor="middle" className="fcal-owner-col-name">
              {shortenOwner(o.owner)}
            </text>
            <text x={xCenter} y={H - padB + 30} textAnchor="middle" className="fcal-owner-col-meta">
              {o.dealCount} deal{o.dealCount === 1 ? "" : "s"}
            </text>
            <title>
              {o.owner}: {fmtMoney(o.declaredTotal)} declared, {fmtMoney(o.adjustedTotal)} supported,
              {" "}{o.dealCount} deal{o.dealCount === 1 ? "" : "s"}{o.commitAtRisk > 0 ? ` · ${o.commitAtRisk} Commit at risk` : ""}
            </title>
          </g>
        );
      })}
    </svg>
  );
}

// Per-owner gap concentration. Answers "which AE is driving the gap?" — the
// manager's actionable handle for coaching. Sorted by gap descending so the
// top of the list is the highest-leverage conversation.
function OwnerBreakdown({ rows, onSelectOwner, activeOwner }) {
  const byOwner = useMemo(() => aggregateByOwner(rows), [rows]);

  if (!byOwner.length) return null;
  const maxDeclared = Math.max(...byOwner.map((o) => o.declaredTotal), 1);

  return (
    <section className="fcal-block">
      <h3>
        By AE
        <InfoTip text="Forecast gap by deal owner. Sorted by largest gap so the top of the list is the highest-leverage coaching conversation." />
      </h3>
      <p className="muted small">
        {byOwner.length} owner{byOwner.length === 1 ? "" : "s"} · sorted by gap · click a row to inspect their deals
      </p>
      <ul className="fcal-owners">
        {byOwner.map((o) => {
          const supportedPct = (o.adjustedTotal / maxDeclared) * 100;
          const atRiskPct = ((o.declaredTotal - o.adjustedTotal) / maxDeclared) * 100;
          const tone = o.gapPct >= 25 ? "warn" : o.gapPct >= 10 ? "amber" : "good";
          const Tag = onSelectOwner ? "button" : "div";
          const isActive = activeOwner === o.owner;
          return (
            <li key={o.owner}>
              <Tag
                type={onSelectOwner ? "button" : undefined}
                className={`fcal-owner ${onSelectOwner ? "is-clickable" : ""} ${isActive ? "is-active" : ""}`}
                onClick={onSelectOwner ? () => onSelectOwner(o.owner) : undefined}
                aria-pressed={onSelectOwner ? isActive : undefined}
                aria-label={onSelectOwner ? (isActive ? `Showing ${o.owner}'s deals — click to refresh` : `Show ${o.owner}'s deals`) : undefined}
              >
                <div className="fcal-owner-l">
                  <b>{o.owner}</b>
                  <em className="muted small">
                    {o.dealCount} deal{o.dealCount === 1 ? "" : "s"}
                    {o.commitAtRisk > 0 && <> · <span className="tone-warn">{o.commitAtRisk} Commit at risk</span></>}
                    {o.theatreFlagCount > 0 && <> · {o.theatreFlagCount} flag{o.theatreFlagCount === 1 ? "" : "s"}</>}
                  </em>
                </div>
                <div className="fcal-owner-bar" aria-hidden="true">
                  {supportedPct > 0 && (
                    <span className="fcal-owner-bar-supported" style={{ width: `${supportedPct}%` }} />
                  )}
                  {atRiskPct > 0 && (
                    <span className={`fcal-owner-bar-atrisk tone-${tone}`} style={{ width: `${atRiskPct}%` }} />
                  )}
                </div>
                <div className="fcal-owner-r">
                  <b>{fmtMoney(o.declaredTotal)} <span className="muted">→</span> {fmtMoney(o.adjustedTotal)}</b>
                  <em className={`fcal-owner-gap tone-${tone}`}>−{o.gapPct}%</em>
                </div>
                {onSelectOwner && (
                  <span className="fcal-owner-go" aria-hidden="true">
                    <Icon name="arrow" size={12} />
                  </span>
                )}
              </Tag>
            </li>
          );
        })}
      </ul>
    </section>
  );
}

// ---------- Deals tab (Kanban) ----------
// Per-deal recalculation as a Kanban board. Same chrome/UX as the AE deals
// page; clicking a card opens the Forecast Room (managed at the screen level
// so it works across tabs).
function DealsTab({ summary, onSelect, filter = {}, onToggleStage, onClearOwner, onClearStage, onClearFilter }) {
  if (!summary.rows.length) {
    return (
      <div className="fcal-empty">
        <p className="muted">No open deals available to forecast.</p>
      </div>
    );
  }
  const filteredRows = summary.rows.filter((r) => {
    if (filter.owner && r.owner !== filter.owner) return false;
    if (filter.stage && !STAGE_MATCH_FCAL(r, filter.stage)) return false;
    return true;
  });
  const totalDeclared = filteredRows.reduce((s, r) => s + (r.declaredAmount || 0), 0);
  const flagged = filteredRows.filter((r) => (r.theatreFlags || []).length > 0).length;
  const commitAtRiskCount = filteredRows.filter(
    (r) => r.declaredCategory === "Commit" && r.recommendedCategory !== "Commit"
  ).length;
  const isFiltered = !!(filter.owner || filter.stage);
  const subtitle = isFiltered
    ? [filter.owner, filter.stage].filter(Boolean).join(" · ") + " — pipeline"
    : "Every open deal, recalculated";

  return (
    <section className="fcal-deals-tab">
      <FilterBar
        filter={filter}
        onClearOwner={onClearOwner}
        onClearStage={onClearStage}
        onClearAll={onClearFilter}
      />
      <ForecastSectionHeader
        eyebrow="Manager forecast"
        title={subtitle}
        tone={flagged ? "amber" : "good"}
        subtitle={`Pipeline through the model's lens — ${filteredRows.length} ${isFiltered ? "filtered" : "open"} deals · ${fmtMoney(totalDeclared)} declared${commitAtRiskCount > 0 ? ` · ${commitAtRiskCount} Commit deal${commitAtRiskCount === 1 ? "" : "s"} at risk` : ""}${flagged > 0 ? ` · ${flagged} theatre flag${flagged === 1 ? "" : "s"}` : ""}.`}
        meta={<span>Click a card to open the Forecast Room. Click a stage header to filter.</span>}
      />

      {filteredRows.length > 0 ? (
        <RecalcKanban
          rows={filteredRows}
          onSelect={onSelect}
          activeStage={filter.stage}
          onToggleStage={onToggleStage}
        />
      ) : (
        <div className="fcal-empty">
          <p className="muted">No deals match this filter.</p>
          {onClearFilter && (
            <button type="button" className="btn sm" onClick={onClearFilter}>Clear all filters</button>
          )}
        </div>
      )}
    </section>
  );
}

const SCENARIO_LABEL = { conservative: "Conservative", balanced: "Balanced", aggressive: "Aggressive" };

function CompareRow({ label, value, max, tone, highlight = false, tip }) {
  const width = max > 0 ? Math.max(2, (value / max) * 100) : 0;
  return (
    <div className={`fcal-compare ${highlight ? "is-highlight" : ""}`}>
      <div className="fcal-compare-label">
        {label}
        {tip && <InfoTip text={tip} label={`What is ${label}?`} />}
      </div>
      <div className="fcal-compare-track">
        <span className={`fcal-compare-fill tone-${tone}`} style={{ width: `${width}%` }} />
      </div>
      <div className="fcal-compare-value">{fmtMoney(value)}</div>
    </div>
  );
}

// Kanban view of the deal-by-deal recalculation. Reuses the AE deals
// pipeline-* classes so the chrome (column headers, rail step numbers, card
// frame, expand-on-hover body) is visually identical to the rep-side board.
// Click a card → opens the Forecast Room modal.
const KANBAN_STAGES = ["Discovery", "Qualification", "Proposal", "Negotiation", "Closed"];
const STAGE_MATCH_FCAL = (row, stage) =>
  row.stage === stage || (stage === "Closed" && /^closed/i.test(row.stage || ""));

function RecalcKanban({ rows, onSelect, activeStage, onToggleStage }) {
  const byStage = KANBAN_STAGES.map((s) => ({
    name: s,
    deals: rows
      .filter((r) => STAGE_MATCH_FCAL(r, s))
      .sort((a, b) => Math.abs(b.forecastGap) - Math.abs(a.forecastGap)),
  }));
  const canToggleStage = typeof onToggleStage === "function";

  return (
    <div className="pipeline fcal-kanban" role="list">
      <div className="pipeline-stage-header" role="presentation">
        {byStage.map((s, i) => {
          const stageValue = s.deals.reduce((sum, d) => sum + (d.declaredAmount || 0), 0);
          const stepNumber = String(i + 1).padStart(2, "0");
          const isLast = i === byStage.length - 1;
          const isActive = activeStage === s.name;
          const HeadTag = canToggleStage ? "button" : "div";
          return (
            <div
              key={`${s.name}-head`}
              className={`pipeline-stage pipeline-stage-head-cell ${isActive ? "is-selected" : ""}`}
              data-stage-index={i + 1}
            >
              <HeadTag
                type={canToggleStage ? "button" : undefined}
                className="pipeline-head"
                onClick={canToggleStage ? () => onToggleStage(s.name) : undefined}
                aria-pressed={canToggleStage ? isActive : undefined}
                title={canToggleStage ? (isActive ? `Showing ${s.name} — click to clear` : `Filter to ${s.name}`) : undefined}
              >
                <span className="pipeline-head-name">{s.name}</span>
                <span className="pipeline-head-rail" aria-hidden="true">
                  <span className="pipeline-head-rail-line" />
                  <span className="pipeline-head-rail-node">
                    {isLast
                      ? <Icon name="check" size={11} />
                      : <span className="pipeline-head-rail-num">{stepNumber}</span>}
                  </span>
                </span>
                <span className="pipeline-head-meta">
                  <span className="pipeline-head-count">
                    {s.deals.length} {s.deals.length === 1 ? "deal" : "deals"}
                  </span>
                  <span className="pipeline-head-dot" aria-hidden="true">·</span>
                  <span className="pipeline-head-value">${Math.round(stageValue / 1000)}K</span>
                </span>
              </HeadTag>
            </div>
          );
        })}
      </div>

      <div className="pipeline-stage-columns">
        {byStage.map((s, i) => (
          <div
            key={s.name}
            className="pipeline-stage pipeline-stage-column"
            data-stage-index={i + 1}
            role="listitem"
          >
            <div className="pipeline-deals">
              {s.deals.length === 0 && <div className="pipeline-empty">— none</div>}
              {s.deals.map((row) => {
                const declared = Math.round((row.declaredProbability || 0) * 100);
                const adjusted = Math.round((row.adjustedProbability || 0) * 100);
                const gapPt = declared - adjusted;
                const status = gapPt >= 25 ? "at-risk" : gapPt >= 10 ? "needs-attention" : "healthy";
                const probTone = status === "at-risk" ? "warn" : status === "needs-attention" ? "amber" : "good";
                // Surface the most actionable label: theatre flag > model reason. Skip
                // when the deal holds up — nothing to flag.
                const topLabel = row.theatreFlags[0] || row.reasons[0] || null;
                return (
                  <button
                    key={row.dealId}
                    type="button"
                    className="pipeline-card"
                    data-status={status}
                    onClick={() => onSelect(row.dealId)}
                    aria-label={`Open Forecast Room for ${row.company}`}
                  >
                    <div className="pipeline-card-top">
                      <span className="pipeline-card-co">{row.company}</span>
                      <span className="pipeline-card-meta">
                        <span className="meta-chip pipeline-card-v">{fmtMoney(row.declaredAmount)}</span>
                        <span
                          className={`risk-meter tone-${status}`}
                          role="img"
                          aria-label={`Forecast gap: ${gapPt}pt`}
                          title={`Declared ${declared}% → evidence ${adjusted}% (${gapPt >= 0 ? "+" : ""}${gapPt}pt)`}
                        >
                          <span /><span /><span />
                        </span>
                      </span>
                    </div>
                    {topLabel && status !== "healthy" && (
                      <div className="buyer-unverified-label">{topLabel}</div>
                    )}
                    <div className="pipeline-card-body" aria-hidden="true">
                      <div className="pipeline-card-body-inner">
                        <div className="fcal-kanban-prob">
                          <em className="muted small">Probability (declared → evidence)</em>
                          <div className="fcal-kanban-prob-pair">
                            <em>{declared}%</em>
                            <span className="fcal-kanban-prob-arrow" aria-hidden="true">→</span>
                            <b className={`tone-${probTone}`}>{adjusted}%</b>
                            <span className={`fcal-kanban-prob-gap tone-${probTone}`}>
                              {gapPt >= 0 ? "−" : "+"}{Math.abs(gapPt)}pt
                            </span>
                          </div>
                        </div>
                        <div className="fcal-kanban-cat">
                          <em className="muted small">Recommend</em>
                          <CategoryChip category={row.recommendedCategory} />
                          <em className="muted small">·</em>
                          <em className="muted small">{fmtMoney(row.nudgeAdjustedAmount)}</em>
                        </div>
                      </div>
                    </div>
                  </button>
                );
              })}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function ProbabilityCompare({ declared, adjusted, declaredCategory }) {
  const tone = probTone(adjusted, declared);
  const declaredPct = Math.round((declared || 0) * 100);
  const adjustedPct = Math.round((adjusted || 0) * 100);
  return (
    <span className="fcal-prob-compare">
      <span className="fcal-prob-pair">
        <em className="muted">{declaredPct}%</em>
        <span className="fcal-prob-arrow" aria-hidden="true">→</span>
        <b className={`tone-${tone}`}>{adjustedPct}%</b>
      </span>
      <span className="fcal-prob-bar" aria-hidden="true">
        <em className="declared" style={{ width: `${declaredPct}%` }} />
        <em className={`adjusted tone-${tone}`} style={{ width: `${adjustedPct}%` }} />
      </span>
    </span>
  );
}

// Maps a theatre-flag string to a concrete question the manager should ask
// in the forecast review. Pattern-based; falls back to a generic challenge.
function suggestedChallenge(flag) {
  if (/no confirmed next step|no buyer-owned next step|next step/i.test(flag)) {
    return "Show me the buyer-owned next step on the calendar — date, time, attendees.";
  }
  if (/economic buyer|decision-maker not identified|decision maker/i.test(flag)) {
    return "Who's signing the contract? When did they last engage personally?";
  }
  if (/approval path/i.test(flag)) {
    return "Walk me through procurement — every step and name from now to signature.";
  }
  if (/inflated|may be inflated/i.test(flag)) {
    return "What's the proof for that probability? Strongest signal in the last 14 days?";
  }
  if (/stakeholder coverage|weak stakeholder|high-value/i.test(flag)) {
    return "Map the buying committee — who has authority, who could block, who's silent?";
  }
  return "Surface the supporting evidence and verify it against this week's activity.";
}

function TheatreList({ rows, onSelect, onOpenDeal, filter = {}, embedded = false }) {
  const filteredRows = rows.filter((r) => {
    if (filter.owner && r.owner !== filter.owner) return false;
    if (filter.stage && !STAGE_MATCH_FCAL(r, filter.stage)) return false;
    return true;
  });
  // `embedded` = used inside a parent widget shell — suppress the duplicate
  // section header and just render the carousel (or the empty-state text).
  if (!rows.length) {
    return embedded
      ? <p className="muted small">Forecast holds. Nothing to challenge.</p>
      : (
        <section className="fcal-theatre">
          <div className="fcal-theatre-head">
            <h3>Forecast holds</h3>
            <em className="muted small">Declared and evidence-adjusted agree. Nothing to challenge.</em>
          </div>
        </section>
      );
  }
  if (!filteredRows.length) {
    return embedded
      ? <p className="muted small">{rows.length} flag{rows.length === 1 ? "" : "s"} on the team — none match this filter.</p>
      : (
        <section className="fcal-theatre">
          <div className="fcal-theatre-head">
            <h3>Nothing flagged here</h3>
            <em className="muted small">
              {rows.length} flag{rows.length === 1 ? "" : "s"} on the team — none match this filter.
            </em>
          </div>
        </section>
      );
  }
  return <ForecastChallengeCarousel rows={filteredRows} onSelect={onSelect} onOpenDeal={onOpenDeal} embedded={embedded} />;
}

// Severity helper, kept local — gap pt threshold matches the AE-home triad.
function challengeTone(deal) {
  const declared = Math.round((deal.declaredProbability || 0) * 100);
  const adjusted = Math.round((deal.adjustedProbability || 0) * 100);
  const gap = declared - adjusted;
  return gap >= 25 ? "warn" : gap >= 10 ? "amber" : "good";
}

// One challenge — what the model lowered, why, and the exact question to ask.
// Reuses the coach-card visual recipe so theatre cards and coaching cards
// read as the same component family across the manager view.
function ChallengeCard({ item, logged, onToggleLogged, onSelect, onOpenDeal, active = true, onAction }) {
  const { deal, flag, key } = item;
  const declaredPct = Math.round((deal.declaredProbability || 0) * 100);
  const adjustedPct = Math.round((deal.adjustedProbability || 0) * 100);
  const tone = challengeTone(deal);
  const downgrade = deal.declaredCategory !== deal.recommendedCategory;
  const canOpenDealDetail = onOpenDeal && deal.dealId && !String(deal.dealId).startsWith("mgr-");
  const [prepOpen, setPrepOpen] = useState(false);
  const CategoryStrip = window.DealCategoryStrip;

  const reasons = deal.reasons || [];
  const buffers = deal.bufferReasons || [];
  const hasPrep = reasons.length > 0 || buffers.length > 0;
  const ask = suggestedChallenge(flag);
  // Body click opens the deal page (matching AE Priority + Coaching cards).
  // Manager-built deals without a real deal page fall back to the Forecast
  // Room math modal — the equivalent "context" view on the manager side.
  const openCard = () => {
    if (canOpenDealDetail) onOpenDeal(deal.dealId);
    else onSelect(deal.dealId);
  };
  const cardAria = canOpenDealDetail
    ? `Open ${deal.company} deal page`
    : `See forecast math for ${deal.company}`;

  return (
    <article
      className={`coach-card tone-${tone} ${logged ? "is-coached" : ""} ${active ? "is-clickable" : ""}`}
      role={active ? "button" : undefined}
      tabIndex={active ? 0 : -1}
      onClick={active ? openCard : undefined}
      onKeyDown={active ? (e) => {
        if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openCard(); }
      } : undefined}
      aria-label={active ? cardAria : undefined}
    >
      <header className="coach-card-head">
        <span className={`coach-sev tone-${tone}`} aria-hidden="true" />
        <div className="coach-card-id">
          <b>{deal.company}</b>
          <span>{deal.stage} · {shortenOwner(deal.owner)}</span>
        </div>
        <div className="coach-card-meta-r">
          {CategoryStrip && <CategoryStrip deal={deal} compact className="coach-cat-strip" />}
          <b>{fmtMoney(deal.declaredAmount)}</b>
          <span className={`coach-gap-chip tone-${tone}`}>
            {declaredPct}% → {adjustedPct}%
          </span>
        </div>
      </header>

      <button
        type="button"
        className="coach-cta"
        onClick={(e) => {
          e.stopPropagation();
          if (!active) return;
          // CTA opens the action preview ("Add to forecast review"). Body
          // click is "Open deal" — the math lives in the prep collapse.
          if (onAction) onAction();
        }}
        disabled={!active}
      >
        <span className="coach-cta-text">&ldquo;{ask}&rdquo;</span>
        <span className="coach-cta-track" aria-hidden="true">
          <span className="coach-cta-dot" />
          <span className="coach-cta-dot" />
          <span className="coach-cta-dot" />
          <Icon name="arrow" size={14} className="coach-cta-arrow" />
        </span>
      </button>

      <div className={`coach-trigger-line tone-${tone}`}>
        <span className="coach-trigger-dot" aria-hidden="true" />
        <span>
          {flag}
          {downgrade && <> · {deal.declaredCategory} → {deal.recommendedCategory}</>}
        </span>
      </div>

      {hasPrep && (
        <div className={`coach-prep ${prepOpen ? "is-open" : ""}`}>
          <button
            type="button"
            className="coach-prep-toggle"
            onClick={(e) => { e.stopPropagation(); setPrepOpen((v) => !v); }}
            aria-expanded={prepOpen}
          >
            <Icon name="chevronR" size={12} className="coach-prep-caret" />
            <span>Forecast math</span>
            <em>
              {reasons.length} reason{reasons.length === 1 ? "" : "s"}
              {buffers.length > 0 && <> · {buffers.length} buffer{buffers.length === 1 ? "" : "s"}</>}
            </em>
          </button>
          <div className="coach-prep-body">
            <div className="coach-prep-inner">
              {buffers.length > 0 && (
                <div className="coach-prep-block">
                  <span className="coach-mini-label">Buffers fired</span>
                  <p className="coach-evidence-line">{buffers.join("  ·  ")}</p>
                </div>
              )}
              {reasons.length > 0 && (
                <div className="coach-prep-block coach-prep-block--hero">
                  <span className="coach-mini-label warn">Why the model lowered</span>
                  <ul className="coach-q-list">
                    {reasons.map((r, i) => <li key={i}>{r}</li>)}
                  </ul>
                </div>
              )}
            </div>
          </div>
        </div>
      )}

      <footer className="coach-card-foot">
        {canOpenDealDetail && (
          <button
            type="button"
            className="coach-btn ghost coach-foot-lead"
            onClick={(e) => { e.stopPropagation(); onOpenDeal(deal.dealId); }}
          >
            Open deal <Icon name="arrow" size={11} />
          </button>
        )}
        <button
          type="button"
          className={`coach-btn ${logged ? "is-done" : ""}`}
          onClick={(e) => { e.stopPropagation(); onToggleLogged(key); }}
          aria-pressed={logged}
        >
          <Icon name="check" size={11} /> {logged ? "Logged" : "Mark logged"}
        </button>
      </footer>
    </article>
  );
}

// Forecast-theatre carousel — rotative card deck, same chrome as the coaching
// queue. Each slide is one (deal, flag) pair so each ask is a single card.
function ForecastChallengeCarousel({ rows, onSelect, onOpenDeal, embedded = false }) {
  const flat = useMemo(
    () => rows.flatMap((deal) =>
      (deal.theatreFlags || []).map((flag, i) => ({ deal, flag, key: `${deal.dealId}-${i}` }))
    ),
    [rows]
  );
  const total = flat.length;

  const [idx, setIdx] = useState(0);
  const [dragDx, setDragDx] = useState(0);
  const [dragging, setDragging] = useState(false);
  const [stageH, setStageH] = useState(null);
  const startX = useRef(null);
  const moved = useRef(false);
  const activeRef = useRef(null);
  const [logged, setLogged] = useState(() => new Set());

  useEffect(() => { setIdx((i) => (i > total - 1 ? 0 : i)); }, [total]);

  React.useLayoutEffect(() => {
    const el = activeRef.current;
    if (!el) return;
    const measure = () => setStageH(el.offsetHeight);
    measure();
    if (typeof ResizeObserver === "undefined") return;
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, [idx, total]);

  if (!total) return null;

  const go = (delta) => setIdx((i) => (i + delta + total) % total);
  // Signed shortest-path offset — the deck wraps in whichever direction is
  // closer to the target, so navigation always animates the short way.
  const signedOffset = (i) => {
    let d = i - idx;
    if (d > total / 2) d -= total;
    if (d < -total / 2) d += total;
    return d;
  };
  const toggleLogged = (k) => setLogged((curr) => {
    const next = new Set(curr);
    if (next.has(k)) next.delete(k); else next.add(k);
    return next;
  });

  // Primary CTA — opens an action preview with the challenge to log, math
  // evidence, and forecast firmness impact. Confirm fires the action and a
  // delayed outcome toast simulating the deal's post-review category move.
  const fireAction = (item) => {
    const { deal, flag } = item;
    const dPct = Math.round((deal.declaredProbability || 0) * 100);
    const aPct = Math.round((deal.adjustedProbability || 0) * 100);
    const ask = suggestedChallenge(flag);
    const downgrade = deal.declaredCategory !== deal.recommendedCategory;
    const reasons = deal.reasons || [];
    const buffers = deal.bufferReasons || [];

    // Theatre impact — the manager challenge in the review forces a category
    // move, which firms the forecast. Firmness = the at-risk gap that gets
    // pulled out of inflated categories.
    const gapAmount = Math.max(0, (deal.declaredAmount || 0) * (deal.declaredProbability || 0)
                              - (deal.nudgeAdjustedAmount || 0));
    const fmtK = (n) => n >= 1000000 ? `$${(n / 1000000).toFixed(1)}M` : `$${Math.round(n / 1000)}K`;
    const firmness = fmtK(gapAmount);
    const likelihood = downgrade ? 88 : 72;
    const probGap = Math.max(0, dPct - aPct);

    const commit = () => {
      if (typeof window.fireToast === "function") {
        window.fireToast(`Added to forecast review · ${deal.company}`);
      }
      if (!logged.has(item.key)) toggleLogged(item.key);
      setTimeout(() => go(1), 500);
      // Loop close — post-review category move + forecast firmness.
      setTimeout(() => {
        if (typeof window.fireToast === "function") {
          const move = downgrade
            ? `${deal.declaredCategory} → ${deal.recommendedCategory}`
            : "downgraded";
          window.fireToast(`✓ ${deal.company} · ${move} · forecast firmed +${firmness}`);
        }
      }, 5200);
    };

    if (typeof window.previewAction === "function") {
      const evidence = [
        { label: "Flag", value: flag },
        { label: "Probability", value: `Declared ${dPct}% → Evidence ${aPct}%` },
      ];
      if (downgrade) {
        evidence.push({
          label: "Recommendation",
          value: `Move from ${deal.declaredCategory} to ${deal.recommendedCategory}`,
        });
      }
      if (reasons.length) {
        evidence.push({ label: "Why lowered", value: reasons.slice(0, 2).join("; ") });
      }
      if (buffers.length) {
        evidence.push({ label: "Buffers fired", value: buffers.join(", ") });
      }
      window.previewAction({
        kicker: "Preview · Forecast review item",
        title: `Add to Friday's review · ${deal.company}`,
        meta: [
          { label: "DEAL", value: `${deal.company} · ${deal.stage}` },
          { label: "OWNER", value: shortenOwner(deal.owner) },
          { label: "AMOUNT", value: fmtMoney(deal.declaredAmount) },
        ],
        body: {
          label: "Challenge to make",
          text: `“${ask}”`,
        },
        impact: {
          likelihood,
          timeHorizon: "this Friday's review",
          firmness,
          firmnessRaw: gapAmount,
          gapLabel: "Probability gap",
          gapBefore: probGap > 0 ? probGap : null,
          // Post-review: declared probability aligns with evidence-adjusted
          // (the category move closes the inflation gap).
          gapAfter: 0,
        },
        evidence,
        primaryLabel: "Add to review",
        onConfirm: commit,
      });
    } else {
      commit();
    }
  };

  // Pointer tracking WITHOUT setPointerCapture — capture retargets the
  // trailing mouseup/click onto the stage so the card's click handler
  // never fires. See CoachCarousel for the full rationale.
  const onPointerDown = (e) => {
    startX.current = e.clientX;
    moved.current = false;
    setDragging(true);
  };
  const onPointerMove = (e) => {
    if (startX.current == null) return;
    const dx = e.clientX - startX.current;
    if (Math.abs(dx) > 6) moved.current = true;
    setDragDx(dx);
  };
  const endDrag = (e) => {
    if (startX.current == null) return;
    const dx = e.clientX - startX.current;
    startX.current = null;
    setDragging(false);
    setDragDx(0);
    if (dx <= -56) go(1);
    else if (dx >= 56) go(-1);
  };
  const onPointerLeave = () => {
    if (startX.current == null) return;
    startX.current = null;
    setDragging(false);
    setDragDx(0);
  };
  // Swallow the click that follows a real drag so swiping never fires the
  // buttons inside the card it landed on.
  const onClickCapture = (e) => {
    if (moved.current) { e.stopPropagation(); e.preventDefault(); moved.current = false; }
  };
  const onKeyDown = (e) => {
    if (e.key === "ArrowLeft") { e.preventDefault(); go(-1); }
    else if (e.key === "ArrowRight") { e.preventDefault(); go(1); }
  };

  // When embedded inside a parent widget shell, skip the outer section +
  // header (the shell provides them) and render the carousel directly.
  const Wrapper = embedded ? React.Fragment : "section";
  const wrapperProps = embedded ? {} : { className: "fcal-theatre" };

  return (
    <Wrapper {...wrapperProps}>
      {!embedded && (
        <div className="fcal-theatre-head">
          <h3>
            Challenge in your forecast review
            <InfoTip text="One flag at a time. Drag, click the arrows, or hit ← → to move through. Each card carries the suggested ask and the math behind the call." />
          </h3>
          <em className="muted small">{total} flag{total === 1 ? "" : "s"} across {rows.length} deal{rows.length === 1 ? "" : "s"}</em>
        </div>
      )}
      {embedded && (
        <p className="recalc-widget-sub">{total} flag{total === 1 ? "" : "s"} across {rows.length} deal{rows.length === 1 ? "" : "s"}</p>
      )}

      <div className="coach-carousel">
        <div className="coach-stage-wrap">
          <button type="button" className="coach-nav prev" onClick={() => go(-1)} aria-label="Previous flag">
            <Icon name="chevronL" size={16} />
          </button>

          <div
            className={`coach-stage ${dragging ? "is-dragging" : ""}`}
            style={stageH ? { height: stageH } : undefined}
            tabIndex={0}
            role="group"
            aria-roledescription="carousel"
            aria-label={`Forecast challenge ${idx + 1} of ${total}`}
            onPointerDown={onPointerDown}
            onPointerMove={onPointerMove}
            onPointerUp={endDrag}
            onPointerCancel={endDrag}
            onPointerLeave={onPointerLeave}
            onClickCapture={onClickCapture}
            onKeyDown={onKeyDown}
          >
            <div className="coach-track" style={{ transform: `translateX(${dragDx}px)` }}>
              {flat.map((item, i) => {
                const off = signedOffset(i);
                const pos = off === 0 ? "active" : Math.abs(off) === 1 ? "peek" : "hidden";
                return (
                  <div
                    key={item.key}
                    className="coach-slide"
                    data-pos={pos}
                    style={{ "--off": off }}
                    aria-hidden={pos !== "active"}
                  >
                    <div ref={pos === "active" ? activeRef : null}>
                      <ChallengeCard
                        item={item}
                        logged={logged.has(item.key)}
                        onToggleLogged={toggleLogged}
                        onSelect={onSelect}
                        onOpenDeal={onOpenDeal}
                        active={pos === "active"}
                        onAction={pos === "active" ? () => fireAction(item) : undefined}
                      />
                    </div>
                  </div>
                );
              })}
            </div>
          </div>

          <button type="button" className="coach-nav next" onClick={() => go(1)} aria-label="Next flag">
            <Icon name="chevronR" size={16} />
          </button>
        </div>

        <div className="coach-rail">
          <div className="coach-pips" role="tablist" aria-label="Forecast challenges">
            {flat.map((item, i) => (
              <button
                key={item.key}
                type="button"
                className={`coach-pip ${i === idx ? "is-on" : ""} ${logged.has(item.key) ? "is-coached" : ""}`}
                data-tone={challengeTone(item.deal)}
                onClick={() => setIdx(i)}
                aria-label={`Go to flag ${i + 1}: ${item.deal.company}`}
                aria-selected={i === idx}
              />
            ))}
          </div>
          <span className="coach-counter">
            {String(idx + 1).padStart(2, "0")} / {String(total).padStart(2, "0")}
          </span>
        </div>
      </div>
    </Wrapper>
  );
}

// Declared-vs-evidence probability bar — tug-of-war between the rep's
// declared call (left) and the evidence-adjusted call (right). Same
// .cp-bsp classes as BuyerSellerProgress so the visual language is shared.
function ProbabilityProgress({ declared, adjusted, tone }) {
  const declaredAhead = declared > adjusted;
  const adjustedAhead = adjusted > declared;
  const gapPt = Math.abs(declared - adjusted);
  const stateLabel = tone === "warn" ? "Inflated" : tone === "amber" ? "Watch" : "Aligned";
  const direction = declaredAhead
    ? "declared running hot"
    : adjustedAhead
      ? "evidence running hotter"
      : "even";
  const declaredWidth = Math.max(0, Math.min(100, declared)) / 2;
  const adjustedWidth = Math.max(0, Math.min(100, adjusted)) / 2;
  return (
    <div
      className={`cp-bsp tone-${tone}`}
      aria-label={`Declared ${declared}%, evidence ${adjusted}% — ${gapPt} point gap (${stateLabel}, ${direction})`}
    >
      <div className="cp-bsp-bar">
        <span className={`cp-bsp-num cp-bsp-num-buyer ${declaredAhead ? "is-leading" : ""}`}>
          <em>Declared</em>
          {declared}%
        </span>
        <div className="cp-bsp-track" aria-hidden="true">
          <span
            className={`cp-bsp-fill cp-bsp-fill-buyer ${declaredAhead ? "is-leading" : "is-trailing"}`}
            style={{ width: `${declaredWidth}%` }}
          />
          <span
            className={`cp-bsp-fill cp-bsp-fill-seller ${adjustedAhead ? "is-leading" : "is-trailing"}`}
            style={{ width: `${adjustedWidth}%` }}
          />
          <span className="cp-bsp-centre" />
        </div>
        <span className={`cp-bsp-num cp-bsp-num-seller ${adjustedAhead ? "is-leading" : ""}`}>
          {adjusted}%
          <em>Evidence</em>
        </span>
      </div>
    </div>
  );
}

function recommendForRow(row) {
  const sameCategory = row.recommendedCategory === row.declaredCategory;
  if (sameCategory && row.reasons.length === 0) {
    return { tone: "good", label: "Hold the call", text: `${row.declaredCategory} call is supported by evidence.` };
  }
  if (sameCategory) {
    return { tone: "amber", label: "Hold but watch", text: `Keep at ${row.declaredCategory}, but resolve: ${row.reasons[0].toLowerCase()}.` };
  }
  if (row.declaredCategory === "Commit" && row.recommendedCategory !== "Commit") {
    return { tone: "warn", label: "Downgrade", text: `Move from Commit to ${row.recommendedCategory} until ${row.reasons[0]?.toLowerCase() || "evidence improves"}.` };
  }
  if (row.recommendedCategory === "Commit" && row.declaredCategory !== "Commit") {
    return { tone: "good", label: "Promote", text: "Evidence supports Commit — promote in the next call." };
  }
  return { tone: "amber", label: "Adjust", text: `Move from ${row.declaredCategory} to ${row.recommendedCategory}.` };
}

function RiskExplain({ row, onClose, onOpenDeal }) {
  // Manager-built deals have ids that don't appear in SEED_DEALS; only open
  // detail when we have a source deal.
  const canOpenDealDetail = onOpenDeal && row.dealId && !row.dealId.startsWith("mgr-");
  const recommendation = recommendForRow(row);
  return (
    <section className="fcal-risk">
      <div className="fcal-risk-head">
        <div>
          <em className="muted small">Why the forecast changed</em>
          <h3>{row.company} <span className="muted">· {row.dealName}</span></h3>
        </div>
        <div className="fcal-risk-actions">
          {canOpenDealDetail && (
            <button type="button" className="btn sm" onClick={() => onOpenDeal(row.dealId)}>Open deal</button>
          )}
          <button type="button" className="btn sm ghost" onClick={onClose}>Close</button>
        </div>
      </div>
      <div className={`fcal-risk-reco tone-${recommendation.tone}`} role="status">
        <span className="fcal-risk-reco-label">Recommended · {recommendation.label}</span>
        <p>{recommendation.text}</p>
      </div>
      <div className="fcal-risk-grid">
        <div className="fcal-risk-cell">
          <em>Declared</em>
          <b>{fmtMoney(row.declaredAmount * row.declaredProbability)}</b>
          <span className="muted small">{pctText(row.declaredProbability)} · {row.declaredCategory}</span>
        </div>
        <div className="fcal-risk-cell">
          <em>CRM weighted</em>
          <b>{fmtMoney(row.crmWeightedAmount)}</b>
          <span className="muted small">{pctText(row.crmProbability)} · stage default</span>
        </div>
        <div className="fcal-risk-cell is-highlight">
          <em>Evidence-adjusted</em>
          <b>{fmtMoney(row.nudgeAdjustedAmount)}</b>
          <span className="muted small">{pctText(row.adjustedProbability)} · {row.recommendedCategory}</span>
        </div>
        <div className="fcal-risk-cell">
          <em>Forecast gap</em>
          <b>{fmtMoney(row.forecastGap)}</b>
          <span className="muted small">Risk you carry</span>
        </div>
      </div>
      <div className="fcal-risk-cols">
        <div className="fcal-risk-col">
          <em className="muted small">Reasons</em>
          <ul className="fcal-risk-list">
            {row.reasons.length
              ? row.reasons.map((r) => <li key={r}>{r}</li>)
              : <li className="muted">No major risk reasons identified.</li>}
          </ul>
        </div>
        <div className="fcal-risk-col">
          <em className="muted small">Risk buffers applied</em>
          <ul className="fcal-risk-list">
            {row.bufferReasons.length
              ? row.bufferReasons.map((r) => <li key={r}>{r}</li>)
              : <li className="muted">No buffers applied to this deal.</li>}
          </ul>
        </div>
        <div className="fcal-risk-col">
          <em className="muted small">Forecast theatre flags</em>
          <ul className="fcal-risk-list">
            {row.theatreFlags.length
              ? row.theatreFlags.map((r) => <li key={r}>{r}</li>)
              : <li className="muted">No theatre flags on this deal.</li>}
          </ul>
        </div>
      </div>
    </section>
  );
}

// ---------- Configuration tab ----------
function ConfigTab({ model, summary, savedModels, periodInfo, onSetName, onSetCadence, onSetVariable, onSetBuffer, onReset, onSaveCurrentAs, onApplySaved, onDeleteSaved }) {
  // How many deals each buffer is currently applied to + average impact per
  // variable. Lets the manager see the effect of a slider before reading the
  // table.
  const bufferImpact = useMemo(() => {
    const counts = {};
    model.riskBuffers.forEach((b) => { counts[b.key] = 0; });
    (summary.rows || []).forEach((row) => {
      row.bufferReasons.forEach((reason) => {
        const buffer = model.riskBuffers.find((b) => reason.startsWith(b.label));
        if (buffer) counts[buffer.key] = (counts[buffer.key] || 0) + 1;
      });
    });
    return counts;
  }, [model.riskBuffers, summary.rows]);

  const variableImpact = useMemo(() => {
    const totals = {};
    model.variables.forEach((v) => { totals[v.key] = { sum: 0, n: 0 }; });
    (summary.rows || []).forEach((row) => {
      Object.entries(row.multipliers || {}).forEach(([key, mul]) => {
        if (!totals[key]) return;
        totals[key].sum += (1 - mul);
        totals[key].n += 1;
      });
    });
    const out = {};
    Object.entries(totals).forEach(([k, v]) => {
      out[k] = v.n ? Math.round((v.sum / v.n) * 100) : 0;
    });
    return out;
  }, [model.variables, summary.rows]);

  return (
    <section className="fcal-config">
      <ForecastSectionHeader
        eyebrow="Manager forecast"
        title="Model calibration"
        tone="good"
        subtitle="Validate the model against closed deals, then snapshot calibrations and adjust signal weights below."
        meta={<span>{periodSummary(periodInfo, (model.cadence || {}).type)}</span>}
      />

      {/* Backtest results — was its own sidebar item; now lives here as
          the validation context for any tuning decisions below. */}
      <BacktestTab
        model={model}
        savedModels={savedModels}
        onApplySaved={onApplySaved}
        embedded
      />

      <SavedModelsBlock
        savedModels={savedModels}
        currentName={model.name}
        onSaveCurrentAs={onSaveCurrentAs}
        onApplySaved={onApplySaved}
        onDeleteSaved={onDeleteSaved}
      />

      <CadenceBlock cadence={model.cadence} period={periodInfo} onSetCadence={onSetCadence} />

      <div className="fcal-config-grid">
        <div className="fcal-block">
          <h3>Model basics</h3>
          <label className="fcal-field">
            <span>Model name</span>
            <input className="input" value={model.name} onChange={(e) => onSetName(e.target.value)} />
          </label>
          <div className="fcal-field">
            <span>CRM source fields</span>
            <div className="fcal-source-pills">
              {["Amount", "Close date", "Stage", "Forecast category", "Probability", "Owner"].map((s) => (
                <span key={s} className="chip" style={{ height: 24, fontSize: 11 }}>{s}</span>
              ))}
            </div>
          </div>
          <div className="fcal-actions">
            <button type="button" className="btn sm ghost" onClick={onReset}>Reset to defaults</button>
            <span className="muted small">Last edited {new Date(model.updatedAt).toLocaleTimeString()}</span>
          </div>
        </div>

        <div className="fcal-block">
          <h3>
            Formula
            <InfoTip text="Risk-adjusted probability = baseProbability(stage) × Π(variable multipliers) × Π(risk-buffer multipliers). Each variable's multiplier is built from its weight (here) and its evidence score (per deal)." />
          </h3>
          <FormulaEditor model={model} onSetVariable={onSetVariable} />
          <p className="muted small">Edit the formula or the table below — they stay in sync. Variables are limited to the list below; weights match each row.</p>
        </div>
      </div>

      <div className="fcal-block">
        <h3>
          Formula variables
          <InfoTip text="Each variable contributes a multiplier to the probability. multiplier = 1 − weight × (1 − evidenceScore) × 0.6, clamped to [0.4, 1]. Higher weight = stronger penalty when the underlying signal is weak." />
        </h3>
        <p className="muted small">Higher weight = stronger penalty when the underlying signal is weak. Hints flag tuning candidates.</p>
        <div className="fcal-rows">
          {(() => {
            // Compute tuning hints once: "biggest impact" for the most-active
            // variable, "no effect today" for variables that don't fire.
            const enabledImpacts = model.variables
              .filter((v) => v.enabled && v.key !== "stageProbability")
              .map((v) => variableImpact[v.key] || 0);
            const maxImpact = enabledImpacts.length ? Math.max(...enabledImpacts) : 0;
            return model.variables.map((v) => {
              const impact = variableImpact[v.key] || 0;
              const tone = impact >= 25 ? "warn" : impact >= 10 ? "amber" : "muted";
              const isBase = v.key === "stageProbability";
              let hint = null;
              if (!isBase && v.enabled) {
                if (impact === maxImpact && maxImpact >= 10) hint = { tone: "amber", text: "Biggest impact" };
                else if (impact === 0) hint = { tone: "muted", text: "No effect today" };
                else if (v.weight === 100) hint = { tone: "warn", text: "Maxed out" };
                else if (v.weight === 0) hint = { tone: "muted", text: "Weight at 0" };
              }
              return (
                <div key={v.key} className={`fcal-row ${!v.enabled ? "is-disabled" : ""}`}>
                  <div className="fcal-row-l">
                    <b>{v.label}</b>
                    <em className="muted small">{v.source}{isBase ? " · base probability" : ""}</em>
                  </div>
                  <div className="fcal-row-c">
                    <input
                      type="range"
                      min="0"
                      max="100"
                      step="5"
                      value={v.weight}
                      disabled={!v.enabled}
                      onChange={(e) => onSetVariable(v.key, { weight: Number(e.target.value) })}
                    />
                    <span className="mono small">{v.weight}</span>
                  </div>
                  <div className="fcal-row-impact">
                    {!isBase && v.enabled
                      ? <span className={`fcal-impact tone-${tone}`} title="Average probability removed across open deals">−{impact}% avg</span>
                      : <span className="muted small">—</span>}
                    {hint && <span className={`fcal-hint tone-${hint.tone}`}>{hint.text}</span>}
                  </div>
                  <div className="fcal-row-r">
                    <Toggle checked={v.enabled} onChange={(checked) => onSetVariable(v.key, { enabled: checked })} />
                  </div>
                </div>
              );
            });
          })()}
        </div>
      </div>

      <div className="fcal-block">
        <h3>
          Risk buffers
          <InfoTip text="Conditional haircuts. Each buffer multiplies probability by (1 − value%) when its trigger is met for a deal — e.g. weak champion, no next step, late stage. The Global haircut applies to every deal." />
        </h3>
        <p className="muted small">Each buffer trims probability when a specific risk is present. Impact shows how many deals are affected today.</p>
        <div className="fcal-rows">
          {model.riskBuffers.map((b) => {
            const count = bufferImpact[b.key] || 0;
            const tone = count >= 4 ? "warn" : count >= 2 ? "amber" : count >= 1 ? "info" : "muted";
            return (
              <div key={b.key} className={`fcal-row ${!b.enabled ? "is-disabled" : ""}`}>
                <div className="fcal-row-l">
                  <b>{b.label}</b>
                  <em className="muted small">Applies to · {b.appliesTo}</em>
                </div>
                <div className="fcal-row-c">
                  <input
                    type="range"
                    min="0"
                    max="40"
                    step="1"
                    value={b.value}
                    disabled={!b.enabled}
                    onChange={(e) => onSetBuffer(b.key, { value: Number(e.target.value) })}
                  />
                  <span className="mono small">−{b.value}%</span>
                </div>
                <div className="fcal-row-impact">
                  {b.enabled
                    ? <span className={`fcal-impact tone-${tone}`}>{count === 0 ? "no deals" : `${count} deal${count === 1 ? "" : "s"}`}</span>
                    : <span className="muted small">off</span>}
                </div>
                <div className="fcal-row-r">
                  <Toggle checked={b.enabled} onChange={(checked) => onSetBuffer(b.key, { enabled: checked })} />
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

// ---------- Business cadence ----------
// How the team measures the period — drives every period-aware label across
// the screen (the persistent header, backtest cohort framing, etc.). Three
// independent knobs so a manager on a fiscal-April calendar can switch from
// quarterly to monthly without losing FY alignment.
const CADENCE_TYPES = [
  { k: "monthly",   label: "Monthly",   help: "Each calendar month is its own period." },
  { k: "quarterly", label: "Quarterly", help: "Three-month chunks aligned to the FY start." },
  { k: "semester",  label: "Semester",  help: "Two six-month halves per FY." },
  { k: "annual",    label: "Annual",    help: "One period per fiscal year." },
];
const FY_START_OPTIONS = [
  { k: 0, label: "January (calendar)" },
  { k: 3, label: "April" },
  { k: 6, label: "July" },
  { k: 9, label: "October" },
  { k: 1, label: "February" },
  { k: 2, label: "March" },
  { k: 4, label: "May" },
  { k: 5, label: "June" },
  { k: 7, label: "August" },
  { k: 8, label: "September" },
  { k: 10, label: "November" },
  { k: 11, label: "December" },
];
const PERIOD_OFFSETS = [
  { k: -1, label: "Previous" },
  { k: 0,  label: "Current" },
  { k: 1,  label: "Next" },
];

// Build the FY rhythm — chunks of the fiscal year that today's cadence carves
// it into (12 if Monthly, 4 if Quarterly, 2 if Semester, 1 if Annual). Used by
// the year strip so the manager can see the year visually and click any chunk
// to jump to it.
function fyChunks(cadence, today) {
  const cycleMonths = { monthly: 1, quarterly: 3, semester: 6, annual: 12 }[cadence.type] || 3;
  const numChunks = Math.max(1, Math.round(12 / cycleMonths));
  const fyStartMonth = typeof cadence.fyStartMonth === "number" ? cadence.fyStartMonth : 0;
  const tMonth = today.getMonth();
  const tYear = today.getFullYear();
  const fyYear = tMonth >= fyStartMonth ? tYear : tYear - 1;
  const monthsIntoFY = (tYear - fyYear) * 12 + (tMonth - fyStartMonth);
  const todayIndex = Math.floor(monthsIntoFY / cycleMonths);

  const chunks = [];
  for (let i = 0; i < numChunks; i++) {
    const startMonth = fyStartMonth + i * cycleMonths;
    const start = new Date(fyYear, startMonth, 1);
    const end = new Date(fyYear, startMonth + cycleMonths, 0);
    let label;
    let sublabel;
    if (cadence.type === "monthly") {
      label = start.toLocaleString("en-US", { month: "short" });
      sublabel = String(start.getFullYear());
    } else if (cadence.type === "quarterly") {
      label = `Q${i + 1}`;
      sublabel = `${start.toLocaleString("en-US", { month: "short" })}–${end.toLocaleString("en-US", { month: "short" })}`;
    } else if (cadence.type === "semester") {
      label = `H${i + 1}`;
      sublabel = `${start.toLocaleString("en-US", { month: "short" })}–${end.toLocaleString("en-US", { month: "short" })}`;
    } else {
      label = `FY ${fyYear}`;
      sublabel = `${start.toLocaleString("en-US", { month: "short" })} ${start.getFullYear()} – ${end.toLocaleString("en-US", { month: "short" })} ${end.getFullYear()}`;
    }
    chunks.push({ index: i, label, sublabel, start, end });
  }
  return { chunks, todayIndex, fyYear, fyStartMonth, cycleMonths };
}

function CadenceBlock({ cadence = {}, period, onSetCadence }) {
  const type = cadence.type || "quarterly";
  const fyStartMonth = typeof cadence.fyStartMonth === "number" ? cadence.fyStartMonth : 0;
  const offset = typeof cadence.offset === "number" ? cadence.offset : 0;
  const today = ForecastEngine.DEMO_TODAY;
  const { chunks, todayIndex, fyYear } = useMemo(
    () => fyChunks({ type, fyStartMonth }, today),
    [type, fyStartMonth, today]
  );
  const activeIndex = todayIndex + offset;
  const fmt = (d) => d.toLocaleString("en-US", { month: "short", day: "numeric" });

  // Tone the days-remaining card so a manager scanning the strip catches
  // crunch periods at a glance.
  const daysTone =
    !period ? "muted"
      : period.daysRemaining <= 14 ? "warn"
      : period.daysRemaining <= 30 ? "amber"
      : "good";

  // Cap the rhythm-tile-list label width so the strip stays one-line on most
  // widths but wraps gracefully on Monthly (12 cells).
  const tileMode = type === "monthly" ? "tight" : "wide";

  return (
    <div className="fcal-block fcal-cadence">
      <header className="fcal-cadence-hero">
        <div className="fcal-cadence-hero-l">
          <span className="cp-eyebrow">
            <span className={`cp-eyebrow-dot ${daysTone === "warn" ? "warn" : daysTone === "amber" ? "amber" : "good"}`} />
            Business cadence
            <InfoTip text="How your team measures the forecast period. Drives every period-aware label across the manager view — backtest cohort framing, the persistent header readout, deal close-date filtering." />
          </span>
          <h1 className="fcal-cadence-period">{period?.name || "—"}</h1>
          <p className="fcal-cadence-meta">
            {period && <>{fmt(period.start)} – {fmt(period.end)}</>}
            <span className="muted">{period && <> · </>}{({
              monthly: "Monthly",
              quarterly: "Quarterly",
              semester: "Semester",
              annual: "Annual",
            })[type]} cadence</span>
          </p>
        </div>
        {period && (
          <div className={`fcal-cadence-days tone-${daysTone}`}>
            <span className="fcal-cadence-days-num">{period.daysRemaining}</span>
            <span className="fcal-cadence-days-label">day{period.daysRemaining === 1 ? "" : "s"} left</span>
          </div>
        )}
      </header>

      <div className="fcal-cadence-strip" role="group" aria-label={`Fiscal year ${fyYear} rhythm`}>
        <div className={`fcal-cadence-tiles is-${tileMode}`}>
          {chunks.map((c) => {
            const isToday = c.index === todayIndex;
            const isActive = c.index === activeIndex;
            const newOffset = c.index - todayIndex;
            return (
              <button
                key={c.index}
                type="button"
                className={`fcal-cadence-tile ${isActive ? "is-active" : ""} ${isToday ? "is-today" : ""}`}
                onClick={() => onSetCadence({ offset: newOffset })}
                aria-pressed={isActive}
                title={`${c.label} · ${fmt(c.start)} – ${fmt(c.end)}`}
              >
                <em className="fcal-cadence-tile-label">{c.label}</em>
                <span className="fcal-cadence-tile-sub">{c.sublabel}</span>
                {isToday && <span className="fcal-cadence-tile-today" aria-label="Today">Today</span>}
              </button>
            );
          })}
        </div>
        <div className="fcal-cadence-strip-foot">
          <span className="muted small">Click any tile to jump · the highlighted tile is the active period</span>
          <span className="muted small">FY {fyYear}</span>
        </div>
      </div>

      <div className="fcal-cadence-controls">
        <div className="fcal-cadence-control">
          <span className="fcal-cadence-label">How you measure</span>
          <div className="fcal-cadence-pills" role="radiogroup" aria-label="Cadence">
            {CADENCE_TYPES.map((c) => (
              <button
                key={c.k}
                type="button"
                role="radio"
                aria-checked={type === c.k}
                className={`fcal-cadence-pill ${type === c.k ? "is-active" : ""}`}
                onClick={() => onSetCadence({ type: c.k })}
                title={c.help}
              >
                {c.label}
              </button>
            ))}
          </div>
        </div>
        <div className="fcal-cadence-control">
          <span className="fcal-cadence-label">Fiscal year starts</span>
          <select
            className="input fcal-cadence-fy"
            value={fyStartMonth}
            onChange={(e) => onSetCadence({ fyStartMonth: Number(e.target.value) })}
            disabled={type === "monthly"}
            aria-disabled={type === "monthly"}
            title={type === "monthly" ? "Monthly cadence ignores fiscal-year start" : undefined}
          >
            {FY_START_OPTIONS.map((o) => (
              <option key={o.k} value={o.k}>{o.label}</option>
            ))}
          </select>
        </div>
      </div>
    </div>
  );
}

// ---------- Saved models block ----------
// Lets the manager snapshot the current calibration with a name, apply a
// previously-saved one, or delete unused ones. Persisted to localStorage by
// the parent screen so calibrations survive reloads.
function SavedModelsBlock({ savedModels, currentName, onSaveCurrentAs, onApplySaved, onDeleteSaved }) {
  const [drafting, setDrafting] = useState(false);
  const [draftName, setDraftName] = useState("");

  const beginDraft = () => {
    setDraftName(currentName ? `${currentName} (copy)` : "Untitled model");
    setDrafting(true);
  };
  const commit = () => {
    onSaveCurrentAs(draftName);
    setDrafting(false);
    setDraftName("");
  };
  const cancel = () => { setDrafting(false); setDraftName(""); };

  return (
    <div className="fcal-block fcal-saved">
      <div className="fcal-saved-head">
        <div>
          <h3>
            Saved models
            <InfoTip text="Snapshot the current calibration so you don't lose your tuning. Saved models persist across reloads and can be replayed against the cohort in the Backtest tab." />
          </h3>
          <p className="muted small" style={{ margin: 0 }}>
            Snapshot calibrations so you can A/B them in the Backtest tab. Apply one to load it as the working model.
          </p>
        </div>
        {drafting ? (
          <div className="fcal-saved-draft">
            <input
              className="input"
              autoFocus
              value={draftName}
              onChange={(e) => setDraftName(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") cancel(); }}
              placeholder="e.g. Q2 with stricter close-date buffer"
            />
            <button type="button" className="btn sm" onClick={commit} disabled={!draftName.trim()}>Save</button>
            <button type="button" className="btn sm ghost" onClick={cancel}>Cancel</button>
          </div>
        ) : (
          <button type="button" className="btn sm" onClick={beginDraft}>Save current as…</button>
        )}
      </div>
      {savedModels.length === 0 ? (
        <p className="muted small" style={{ margin: 0 }}>No saved models yet — save the current calibration to start comparing.</p>
      ) : (
        <ul className="fcal-saved-list">
          {savedModels.map((m) => {
            const enabledVars = m.variables.filter((v) => v.enabled).length;
            const enabledBufs = m.riskBuffers.filter((b) => b.enabled).length;
            const isCurrent = m.name === currentName;
            return (
              <li key={m.id} className={`fcal-saved-item ${isCurrent ? "is-current" : ""}`}>
                <div className="fcal-saved-meta">
                  <b>{m.name}</b>
                  <em className="muted small">
                    {SCENARIO_LABEL[m.scenario] || "Balanced"} · {enabledVars} vars · {enabledBufs} buffers · saved {new Date(m.savedAt).toLocaleDateString()}
                  </em>
                </div>
                <div className="fcal-saved-actions">
                  {isCurrent && <span className="chip info" style={{ height: 22, fontSize: 11 }}>Loaded</span>}
                  <button type="button" className="btn sm ghost" onClick={() => onApplySaved(m.id)}>Apply</button>
                  <button type="button" className="btn sm ghost fcal-saved-del" onClick={() => onDeleteSaved(m.id)} aria-label={`Delete ${m.name}`}>Delete</button>
                </div>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}

// ---------- Backtest tab ----------
// Replays the active model + each saved model against a cohort of historical
// closed deals so the manager can see "would this calibration have called the
// quarter accurately?" before trusting it on live pipeline.
function BacktestTab({ model, savedModels, onApplySaved, embedded = false }) {
  const [period, setPeriod] = useState("all");

  const result = useMemo(
    () => ForecastEngine.backtest(model, ForecastEngine.BACKTEST_COHORT, period),
    [model, period]
  );
  const compareRows = useMemo(() => savedModels.map((m) => ({
    id: m.id,
    name: m.name,
    scenario: m.scenario,
    result: ForecastEngine.backtest(m, ForecastEngine.BACKTEST_COHORT, period),
  })), [savedModels, period]);

  if (!result.total) {
    return <div className="fcal-empty"><p className="muted">No closed deals in this period.</p></div>;
  }

  const revenueDelta = result.predictedRevenue - result.actualRevenue;
  const revenuePct = result.actualRevenue > 0 ? Math.round((revenueDelta / result.actualRevenue) * 100) : 0;
  const scoreTone = toneForScore(result.calibrationScore);
  const accuracyTone = result.accuracy >= 75 ? "good" : result.accuracy >= 60 ? "amber" : "warn";
  const revenueTone = Math.abs(revenuePct) <= 10 ? "good" : Math.abs(revenuePct) <= 20 ? "amber" : "warn";
  const winRatePct = Math.round((result.wins / result.total) * 100);

  // Rank current + saved models by score for the leaderboard.
  const leaderboard = [
    { id: "__current", name: model.name, scenario: model.scenario, isActive: true, result },
    ...compareRows.map((r) => ({ ...r, isActive: false })),
  ].sort((a, b) => b.result.calibrationScore - a.result.calibrationScore);

  return (
    <>
      <section className="fcal-bt-hero">
        <ForecastSectionHeader
          eyebrow={embedded ? "Validation" : "Backtest"}
          title={embedded ? "Past-quarter accuracy" : "Backtest calibration"}
          tone={scoreTone}
          subtitle={`Same evidence the team had on forecast day, replayed through today's variables and buffers · ${result.total} deals · ${result.wins} won · Brier ${result.brier.toFixed(3)}.`}
          level={embedded ? 2 : 1}
        >
          <div className="fcal-backtest-period" role="group" aria-label="Backtest period">
            {["all", "Q1 2026", "Q4 2025"].map((p) => (
              <button
                key={p}
                type="button"
                className={`fcal-scenario ${period === p ? "is-active" : ""}`}
                aria-pressed={period === p}
                onClick={() => setPeriod(p)}
              >
                {p === "all" ? "All periods" : p}
              </button>
            ))}
          </div>
        </ForecastSectionHeader>

        <div className="fcal-bt-hero-body">
          <div className="fcal-bt-score">
            <div className={`fcal-bt-score-num tone-${scoreTone}`}>
              <b>{result.calibrationScore}</b>
              <em>/100</em>
            </div>
            <div className="fcal-bt-score-track" aria-hidden="true">
              <span className={`fcal-bt-score-fill tone-${scoreTone}`} style={{ width: `${result.calibrationScore}%` }} />
            </div>
            <div className="fcal-bt-score-label">
              Calibration score
              <InfoTip text="100 = perfect probability calls; 0 = no skill (Brier 0.25). Score = (1 − Brier / 0.25) × 100. Aim for 75+." />
            </div>
            <p className={`fcal-bt-score-diag tone-${scoreTone}`}>{scoreDiagnosis(result.calibrationScore)}</p>

            <dl className="fcal-bt-stats">
              <div className="fcal-bt-stat">
                <dt>
                  Category accuracy
                  <InfoTip text="% of deals where the model's category called the right side: Commit/Most likely → Won, Best case/Omitted → Lost." />
                </dt>
                <dd className={`tone-${accuracyTone}`}>{result.accuracy}%</dd>
              </div>
              <div className="fcal-bt-stat">
                <dt>
                  Predicted revenue
                  <InfoTip text="Sum of (amount × predicted probability) across the cohort." />
                </dt>
                <dd>{fmtMoney(result.predictedRevenue)}</dd>
              </div>
              <div className="fcal-bt-stat">
                <dt>Actual closed-won</dt>
                <dd>{fmtMoney(result.actualRevenue)}</dd>
              </div>
              <div className="fcal-bt-stat">
                <dt>
                  $ drift
                  <InfoTip text="Predicted minus actual. Positive = the model would have over-forecast the period." />
                </dt>
                <dd className={`tone-${revenueTone}`}>
                  {revenueDelta >= 0 ? "+" : ""}{fmtMoney(revenueDelta)}
                  <em className="muted small fcal-bt-stat-sub">{revenuePct >= 0 ? "+" : ""}{revenuePct}%</em>
                </dd>
              </div>
              <div className="fcal-bt-stat">
                <dt>
                  Cohort win rate
                  <InfoTip text="Share of cohort deals that actually won. The benchmark — calibrated predictions should average near this." />
                </dt>
                <dd>{winRatePct}%</dd>
              </div>
            </dl>
          </div>

          <div className="fcal-bt-chart">
            <div className="fcal-bt-chart-head">
              <div>
                <em className="cp-eyebrow" style={{ display: "inline-flex" }}>
                  <span className="cp-eyebrow-dot info" />
                  Calibration curve
                </em>
                <p className="muted small" style={{ margin: "6px 0 0", maxWidth: 360 }}>
                  Where the dots sit vs the dashed diagonal tells you whether the model is over- or under-confident at each band.
                </p>
              </div>
              <div className="fcal-bt-chart-legend" aria-hidden="true">
                <span className="fcal-bt-legend-item"><em className="line dashed" />Perfect</span>
                <span className="fcal-bt-legend-item"><em className={`line solid tone-${scoreTone}`} />This model</span>
              </div>
            </div>
            <ReliabilityChart buckets={result.buckets} scoreTone={scoreTone} />
            <BucketLegend buckets={result.buckets} />
          </div>
        </div>
      </section>

      {leaderboard.length > 1 && (
        <section className="fcal-block">
          <h3>
            Compare with saved models
            <InfoTip text="Same cohort, different calibrations. Sorted by score so you can see which one would have called the period most accurately." />
          </h3>
          <p className="muted small">Apply one to load it as the working model.</p>
          <ul className="fcal-bt-leaderboard">
            {leaderboard.map((row, idx) => {
              const tone = toneForScore(row.result.calibrationScore);
              return (
                <li key={row.id} className={`fcal-bt-rank ${row.isActive ? "is-active" : ""}`}>
                  <span className="fcal-bt-rank-pos">#{idx + 1}</span>
                  <div className="fcal-bt-rank-name">
                    <b>{row.name}</b>
                    <em className="muted small">{row.isActive ? "Working model" : "Saved"} · {SCENARIO_LABEL[row.scenario] || "Balanced"}</em>
                  </div>
                  <div className="fcal-bt-rank-score">
                    <div className="fcal-bt-rank-bar" aria-hidden="true">
                      <span className={`fcal-bt-rank-fill tone-${tone}`} style={{ width: `${row.result.calibrationScore}%` }} />
                    </div>
                    <b className={`fcal-bt-rank-num tone-${tone}`}>{row.result.calibrationScore}</b>
                  </div>
                  <div className="fcal-bt-rank-stats">
                    <span><em className="muted small">Accuracy</em><b>{row.result.accuracy}%</b></span>
                    <span><em className="muted small">Predicted</em><b>{fmtMoney(row.result.predictedRevenue)}</b></span>
                  </div>
                  <div className="fcal-bt-rank-action">
                    {row.isActive
                      ? <span className="chip info" style={{ height: 22, fontSize: 11 }}>Active</span>
                      : <button type="button" className="btn sm ghost" onClick={() => onApplySaved(row.id)}>Apply</button>}
                  </div>
                </li>
              );
            })}
          </ul>
        </section>
      )}

      <section className="fcal-block">
        <h3>
          Top misses
          <InfoTip text="The 5 deals where (predicted probability − actual outcome) was largest in either direction." />
        </h3>
        <p className="muted small">Where the model was furthest off — read these to learn what your weights miss.</p>
        {(() => {
          const diag = diagnoseMisses(result.misses);
          if (!diag) return null;
          return (
            <div className="fcal-bt-diagnosis" role="note">
              <em className="cp-eyebrow" style={{ display: "inline-flex" }}>
                <span className="cp-eyebrow-dot amber" />
                Pattern detected
              </em>
              <p>
                <b>{diag.pattern}</b> {diag.action}
              </p>
            </div>
          );
        })()}
        <div className="fcal-bt-misses">
          {result.misses.map((row) => {
            const predicted = Math.round(row.predicted * 100);
            const wasWon = row.outcome === 1;
            const overconfident = row.error > 0;
            const driftMag = Math.abs(row.error);
            const driftTone = driftMag > 0.5 ? "warn" : "amber";
            const outcomePct = wasWon ? 100 : 0;
            const driftLeft = Math.min(predicted, outcomePct);
            const driftWidth = Math.abs(predicted - outcomePct);
            return (
              <div key={row.dealId} className="fcal-bt-miss">
                <div className="fcal-bt-miss-meta">
                  <b>{row.company}</b>
                  <em className="muted small">{row.period} · {row.stage} · {fmtMoney(row.declaredAmount)}</em>
                </div>
                <div className="fcal-bt-miss-axis">
                  <div className="fcal-bt-miss-track" aria-label={`Predicted ${predicted}%, actual ${wasWon ? "won" : "lost"}`}>
                    <span className="fcal-bt-miss-baseline" />
                    {[0, 25, 50, 75, 100].map((tick) => (
                      <span key={tick} className="fcal-bt-miss-tick" style={{ left: `${tick}%` }} />
                    ))}
                    <span className={`fcal-bt-miss-drift tone-${driftTone}`} style={{ left: `${driftLeft}%`, width: `${driftWidth}%` }} />
                    <span className={`fcal-bt-miss-marker pred tone-${driftTone}`} style={{ left: `${predicted}%` }} title={`Predicted ${predicted}%`}>
                      <em>{predicted}%</em>
                    </span>
                    <span className={`fcal-bt-miss-marker outcome ${wasWon ? "won" : "lost"}`} style={{ left: `${outcomePct}%` }} title={wasWon ? "Won" : "Lost"}>
                      <em>{wasWon ? "Won" : "Lost"}</em>
                    </span>
                  </div>
                  <div className="fcal-bt-miss-axis-labels" aria-hidden="true">
                    <em>0%</em>
                    <em>50%</em>
                    <em>100%</em>
                  </div>
                </div>
                <div className={`fcal-bt-miss-tag tone-${driftTone}`}>
                  {overconfident
                    ? <>Over-confident by <b>{Math.round(driftMag * 100)}pt</b></>
                    : <>Under-confident by <b>{Math.round(driftMag * 100)}pt</b></>}
                </div>
              </div>
            );
          })}
        </div>
      </section>
    </>
  );
}

function toneForScore(score) {
  return score >= 80 ? "good" : score >= 65 ? "amber" : "warn";
}

function scoreDiagnosis(score) {
  if (score >= 80) return "Strong calibration — trust this model on live pipeline.";
  if (score >= 65) return "Decent but improvable — review Top misses below for tuning ideas.";
  return "Weak calibration — try increasing the global haircut or strengthening evidence-based buffers.";
}

// Look for a shared failure pattern across the top misses and surface a
// concrete tuning suggestion. Falls back to over/under-confidence if no
// dominant feature signal is found.
function diagnoseMisses(misses) {
  if (!misses?.length) return null;
  const total = misses.length;
  const minThreshold = Math.max(2, Math.ceil(total * 0.6));

  const matches = (pattern) => misses.filter((m) => (m.reasons || []).some((r) => pattern.test(r))).length;
  const noNextStep = matches(/next step/i);
  const noEB = matches(/economic buyer|decision owner|decision-maker|decision maker/i);
  const noApproval = matches(/approval path/i);
  const weakChampion = matches(/champion/i);
  const overConf = misses.filter((m) => m.error > 0).length;
  const underConf = misses.filter((m) => m.error < 0).length;

  const candidates = [
    noNextStep >= minThreshold && {
      pattern: `${noNextStep} of ${total} misses had no buyer-owned next step.`,
      action: "Try increasing the No-next-step buffer or raising the Next-step variable weight.",
    },
    noEB >= minThreshold && {
      pattern: `${noEB} of ${total} misses lacked an identified economic buyer.`,
      action: "Try increasing the Missing-decision-maker buffer or raising the Stakeholder coverage weight.",
    },
    weakChampion >= minThreshold && {
      pattern: `${weakChampion} of ${total} misses had a weak or silent champion.`,
      action: "Try increasing the Weak-champion buffer or raising the Champion variable weight.",
    },
    noApproval >= minThreshold && {
      pattern: `${noApproval} of ${total} misses had no documented approval path.`,
      action: "Try raising the Decision-process variable weight or strengthening the Close-date slippage buffer.",
    },
  ].filter(Boolean);

  if (candidates.length) return candidates[0];

  if (overConf >= Math.ceil(total * 0.7)) {
    return {
      pattern: `${overConf} of ${total} misses are over-confident.`,
      action: "Try increasing the Global haircut by ~5pt or strengthening evidence-based buffers.",
    };
  }
  if (underConf >= Math.ceil(total * 0.7)) {
    return {
      pattern: `${underConf} of ${total} misses are under-confident.`,
      action: "Try lowering the Global haircut, or reducing weights on variables that fire too often.",
    };
  }
  return null;
}

// SVG reliability diagram. Plots predicted-probability buckets (x) against the
// actual win rate inside each bucket (y). The dashed diagonal is perfect
// calibration; the colored line is what the current model actually achieves on
// the cohort. Filled area between the two visualizes drift.
function ReliabilityChart({ buckets, scoreTone }) {
  const W = 480, H = 280;
  const padL = 44, padR = 20, padT = 18, padB = 38;
  const innerW = W - padL - padR;
  const innerH = H - padT - padB;
  const xToPx = (x) => padL + (x / 100) * innerW;
  const yToPx = (y) => H - padB - (y / 100) * innerH;
  const ticks = [0, 25, 50, 75, 100];

  const populated = buckets
    .map((b) => ({
      ...b,
      expectedPct: Math.round(b.expected * 100),
      actualPct: b.actual === null ? null : Math.round(b.actual * 100),
    }))
    .filter((b) => b.actualPct !== null && b.count > 0);

  const actualPath = populated.length
    ? populated.map((b, i) => `${i === 0 ? "M" : "L"} ${xToPx(b.expectedPct)} ${yToPx(b.actualPct)}`).join(" ")
    : null;

  // Drift envelope: actual line on top, then back along the diagonal at the
  // same x's to close the shape.
  const driftPath = populated.length
    ? [
        ...populated.map((b, i) => `${i === 0 ? "M" : "L"} ${xToPx(b.expectedPct)} ${yToPx(b.actualPct)}`),
        ...populated.slice().reverse().map((b) => `L ${xToPx(b.expectedPct)} ${yToPx(b.expectedPct)}`),
        "Z",
      ].join(" ")
    : null;

  return (
    <svg
      viewBox={`0 0 ${W} ${H}`}
      className="fcal-bt-rel-svg"
      preserveAspectRatio="xMidYMid meet"
      role="img"
      aria-label="Calibration reliability diagram"
    >
      {ticks.map((t) => (
        <g key={t} className="fcal-bt-rel-grid">
          <line x1={xToPx(t)} x2={xToPx(t)} y1={padT} y2={H - padB} />
          <line x1={padL} x2={W - padR} y1={yToPx(t)} y2={yToPx(t)} />
        </g>
      ))}
      {driftPath && <path d={driftPath} className={`fcal-bt-rel-drift tone-${scoreTone}`} />}
      <line x1={xToPx(0)} y1={yToPx(0)} x2={xToPx(100)} y2={yToPx(100)} className="fcal-bt-rel-diagonal" />
      {actualPath && <path d={actualPath} className={`fcal-bt-rel-line tone-${scoreTone}`} />}
      {populated.map((b) => (
        <g key={b.key}>
          <circle cx={xToPx(b.expectedPct)} cy={yToPx(b.actualPct)} r="6" className={`fcal-bt-rel-dot tone-${scoreTone}`}>
            <title>{`${b.label}: predicted ~${b.expectedPct}%, actual ${b.actualPct}% (${b.count} deal${b.count === 1 ? "" : "s"})`}</title>
          </circle>
          <text x={xToPx(b.expectedPct)} y={yToPx(b.actualPct) - 12} textAnchor="middle" className="fcal-bt-rel-dot-label">
            {b.actualPct}%
          </text>
        </g>
      ))}
      {ticks.map((t) => (
        <text key={`xt-${t}`} x={xToPx(t)} y={H - padB + 18} textAnchor="middle" className="fcal-bt-rel-axis">{t}%</text>
      ))}
      {ticks.map((t) => (
        <text key={`yt-${t}`} x={padL - 8} y={yToPx(t) + 4} textAnchor="end" className="fcal-bt-rel-axis">{t}%</text>
      ))}
      <text x={padL + innerW / 2} y={H - 6} textAnchor="middle" className="fcal-bt-rel-axis-title">Predicted probability</text>
      <text x={14} y={padT + innerH / 2} textAnchor="middle" className="fcal-bt-rel-axis-title" transform={`rotate(-90 14 ${padT + innerH / 2})`}>Actual win rate</text>
    </svg>
  );
}

function BucketLegend({ buckets }) {
  return (
    <div className="fcal-bt-bucket-legend">
      {buckets.map((b) => {
        const expectedPct = Math.round(b.expected * 100);
        const actualPct = b.actual === null ? null : Math.round(b.actual * 100);
        const drift = actualPct === null ? 0 : actualPct - expectedPct;
        const tone = b.count === 0 ? "muted" : Math.abs(drift) <= 10 ? "good" : Math.abs(drift) <= 20 ? "amber" : "warn";
        return (
          <div key={b.key} className={`fcal-bt-bucket-chip tone-${tone}`}>
            <em className="fcal-bt-bucket-chip-band">{b.label}</em>
            <span className="fcal-bt-bucket-chip-count">{b.count} deal{b.count === 1 ? "" : "s"}</span>
            {actualPct !== null
              ? <span className={`fcal-bt-bucket-chip-drift tone-${tone}`}>
                  {Math.abs(drift) <= 10
                    ? "well-calibrated"
                    : drift > 0 ? `+${drift}pt under` : `${drift}pt over`}
                </span>
              : <span className="muted small">no data</span>}
          </div>
        );
      })}
    </div>
  );
}

// ---------- Formula editor ----------
// Chip-based editor for the risk-adjusted probability formula. Each enabled
// (non-base) variable is rendered as `name·weight` with an inline weight input
// and a remove button; disabled variables can be added back via a picker.
// State lives in `model.variables`, so edits here propagate to the table below
// and vice versa.
function FormulaEditor({ model, onSetVariable }) {
  const [pickerOpen, setPickerOpen] = useState(false);
  const others = model.variables.filter((v) => v.key !== "stageProbability");
  const enabled = others.filter((v) => v.enabled);
  const disabled = others.filter((v) => !v.enabled);

  return (
    <div className="fcal-formula-editor" role="group" aria-label="Formula editor">
      <div className="fcal-formula-line">
        <span className="fcal-formula-out">riskAdjustedProbability</span>
        <span className="fcal-formula-eq">=</span>
      </div>
      <div className="fcal-formula-body">
        <span className="fcal-formula-base">baseProbability(stage)</span>
        {enabled.map((v) => (
          <FormulaToken
            key={v.key}
            variable={v}
            onWeight={(w) => onSetVariable(v.key, { weight: w })}
            onRemove={() => onSetVariable(v.key, { enabled: false })}
          />
        ))}
        {disabled.length > 0 && (
          <FormulaAdd
            options={disabled}
            isOpen={pickerOpen}
            setOpen={setPickerOpen}
            onAdd={(key) => { onSetVariable(key, { enabled: true }); setPickerOpen(false); }}
          />
        )}
        <span className="fcal-formula-mul">×</span>
        <span className="fcal-formula-base">Π(riskBuffer[i])</span>
      </div>
      <div className="fcal-formula-line fcal-formula-line-secondary">
        <span className="fcal-formula-out">riskAdjustedAmount</span>
        <span className="fcal-formula-eq">=</span>
        <span className="fcal-formula-base">amount × riskAdjustedProbability</span>
      </div>
    </div>
  );
}

function FormulaToken({ variable, onWeight, onRemove }) {
  return (
    <span className="fcal-formula-token">
      <span className="fcal-formula-mul">×</span>
      <span className="fcal-formula-chip">
        <em>{variable.key}</em>
        <span className="fcal-formula-dot" aria-hidden="true">·</span>
        <input
          type="number"
          min="0"
          max="100"
          step="5"
          value={variable.weight}
          aria-label={`${variable.label} weight`}
          className="fcal-formula-weight"
          onChange={(e) => {
            const next = Number(e.target.value);
            if (Number.isNaN(next)) return;
            onWeight(clamp(next, 0, 100));
          }}
        />
        <button
          type="button"
          className="fcal-formula-remove"
          aria-label={`Remove ${variable.label} from formula`}
          title={`Remove ${variable.label}`}
          onClick={onRemove}
        >
          ×
        </button>
      </span>
    </span>
  );
}

function FormulaAdd({ options, isOpen, setOpen, onAdd }) {
  return (
    <span className="fcal-formula-add">
      <span className="fcal-formula-mul">×</span>
      <button
        type="button"
        className="fcal-formula-add-btn"
        aria-haspopup="menu"
        aria-expanded={isOpen}
        onClick={() => setOpen(!isOpen)}
      >
        + variable
      </button>
      {isOpen && (
        <div className="fcal-formula-add-menu" role="menu">
          {options.map((v) => (
            <button
              key={v.key}
              type="button"
              role="menuitem"
              className="fcal-formula-add-item"
              onClick={() => onAdd(v.key)}
            >
              <b>{v.key}</b>
              <em className="muted small">{v.label}</em>
            </button>
          ))}
        </div>
      )}
    </span>
  );
}

function Toggle({ checked, onChange }) {
  return (
    <button
      type="button"
      className={`fcal-toggle ${checked ? "is-on" : ""}`}
      role="switch"
      aria-checked={checked}
      onClick={() => onChange(!checked)}
    >
      <span />
    </button>
  );
}

window.ForecastModelScreen = ForecastModelScreen;
window.ProbabilityProgress = ProbabilityProgress;
window.recommendForRow = recommendForRow;
