/* global React, Icon, Brand, SEED_DEALS, DEAL_DIAGNOSIS, SEED_NUDGES_FEED, ForecastModelScreen */

// ---------- Section catalogue ----------
// All widget kinds the home page can display, plus their picker metadata.
const SECTION_TYPES = [
  { kind: "priority",  label: "Priority deal",      desc: "At-risk deal carousel with next move",   icon: "target" },
  { kind: "agenda",    label: "Today's agenda",     desc: "Next meetings across the pipeline",      icon: "calendar" },
  { kind: "addys-day", label: "Addy's day",         desc: "Ranked actions Addy recommends today",   icon: "sparkles" },
  { kind: "runway",    label: "This week's runway", desc: "Forward 7-day expected vs at-risk view", icon: "trend" },
  { kind: "nudges",    label: "Today's nudges",     desc: "Top buyer-proof gaps across pipeline",   icon: "bell" },
  { kind: "signals",   label: "Buyer signals",      desc: "Recent buyer-side movement",             icon: "sparkles" },
  { kind: "coaching",  label: "Coaching queue",     desc: "Stage-truth exceptions worth coaching",  icon: "users" },
  { kind: "won-loss",  label: "Won/Loss patterns",  desc: "Why deals closed — patterns to repeat or avoid", icon: "flag" },
];

function diagnosisForDeal(deal) {
  if (!deal) return null;
  return DEAL_DIAGNOSIS[deal.id] || DEAL_DIAGNOSIS[deal.sourceDealId] || null;
}

function isPriorityDeal(deal) {
  if (!deal) return false;
  if (/closed/i.test(`${deal.stage || ""} ${deal.statusLabel || ""}`)) return false;
  if (deal.status === "healthy") return false;
  const diag = diagnosisForDeal(deal);
  return Boolean(diag?.fix && (diag?.draft || (deal.nudges || []).length > 0));
}

// ---------- HOME — AI cockpit ----------
function HomeScreen({ deals: dealsProp, openDeal, dashboardView, setDashboardView, managerSection, setManagerSection, showPageHeader = true }) {
  // Subscribe to NudgeBackend so any executed action's reply flows back
  // through the deals list (gap narrows, status escalates, verdict line +
  // priority queue + weather all recompute). Overlay is applied at read
  // time — seed data stays untouched.
  const [, forceNudge] = useState(0);
  useEffect(() => {
    if (!window.NudgeBackend) return;
    return window.NudgeBackend.subscribe(() => forceNudge((n) => n + 1));
  }, []);
  const baseDeals = window.NudgeBackend
    ? window.NudgeBackend.applyImpactToList(dealsProp)
    : dealsProp;

  // Manager mode reuses the same homepage recipe (AgentBanner + slot grid)
  // as AE — only the data source + default slots differ. Manager sees the
  // teammate-extended deal list; AE sees their own. Slots are tracked per
  // view so toggling AE ↔ MGR doesn't clobber either layout.
  const isManager = dashboardView === "manager";
  const managerModel = useMemo(
    () => (isManager ? buildManagerDemoData(baseDeals) : null),
    [isManager, baseDeals]
  );
  const deals = isManager ? managerModel.deals : baseDeals;

  // Expose the manager-shape lookup so the deal room (in manager mode) can
  // surface coaching context and forecast math for the deal it just opened.
  useEffect(() => {
    if (!isManager || !managerModel) {
      delete window.findManagerDeal;
      return undefined;
    }
    window.findManagerDeal = (id) => {
      if (!id) return null;
      return managerModel.deals.find((d) => d.id === id || d.sourceDealId === id) || null;
    };
    return () => { delete window.findManagerDeal; };
  }, [isManager, managerModel]);

  const queue = useMemo(() => deals.filter(isPriorityDeal), [deals]);
  // null = no deal selected. The right pane shows a daily-read summary instead
  // of forcing the first deal into view. The rep picks a deal to inspect.
  const [idx, setIdx] = useState(null);
  const deal = idx != null ? queue[idx] : null;
  const diag = diagnosisForDeal(deal);

  useEffect(() => {
    // Clamp the selected index when the queue shrinks (e.g., a deal moves
    // out of the priority bucket after an action). Selection stays cleared
    // when the user hasn't picked one yet.
    setIdx((current) => {
      if (current == null) return null;
      if (queue.length === 0) return null;
      return Math.min(current, queue.length - 1);
    });
  }, [queue.length]);

  const next = () => setIdx((i) => {
    if (queue.length === 0) return null;
    if (i == null) return 0;
    return (i + 1) % queue.length;
  });
  const prev = () => setIdx((i) => {
    if (queue.length === 0) return null;
    if (i == null) return queue.length - 1;
    return (i - 1 + queue.length) % queue.length;
  });

  const bucketCounts = useMemo(() => ({
    far: deals.filter((d) => Math.abs(d.gap) >= 25).length,
    watch: deals.filter((d) => Math.abs(d.gap) >= 10 && Math.abs(d.gap) < 25).length,
    aligned: deals.filter((d) => Math.abs(d.gap) < 10).length,
  }), [deals]);

  // Slot grid — extensible array of widget kinds (or null for empty).
  // Drag is slot-index based so multiple instances of the same widget
  // can co-exist. Per-view defaults: AE leads with the priority carousel,
  // manager leads with the coaching queue. Everything else (Addy's day,
  // runway, signals, buyer-signals…) is in the picker for either view.
  const [aeSlots, setAeSlots] = useState(["priority", "agenda"]);
  const [mgrSlots, setMgrSlots] = useState(["priority", "agenda"]);
  const slots = isManager ? mgrSlots : aeSlots;
  const setSlots = isManager ? setMgrSlots : setAeSlots;
  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 = (slotIdx, kind) => {
    setSlots((curr) => {
      const out = [...curr];
      out[slotIdx] = kind;
      return out;
    });
    setPickerSlot(null);
  };
  const addSlot = () => setSlots((curr) => [...curr, null]);
  const removeSlot = (slotIdx) => setSlots((curr) => curr.filter((_, i) => i !== slotIdx));

  const cardClass = (slotIdx) => [
    "cp-card",
    dragSlotIdx === slotIdx ? "is-dragging" : "",
  ].filter(Boolean).join(" ");

  // MGR mode routes section-by-section. "home" reuses the AE slot-grid
  // cockpit (with manager-side data) so the manager opens to the same UX as
  // the AE; other sections (recalc / deals / coaching / config) hand off to
  // ManagerView.
  if (isManager && managerSection !== "home") {
    return (
      <ManagerView
        deals={baseDeals}
        openDeal={openDeal}
        managerSection={managerSection}
        setManagerSection={setManagerSection}
      />
    );
  }

  const renderCard = (kind, slotIdx) => {
    const cls = cardClass(slotIdx);
    const drag = dragSourceProps(slotIdx);
    const onRemove = () => removeSlot(slotIdx);
    switch (kind) {
      case "priority":
        return (
          <PriorityDeal
            deal={deal} diag={diag} idx={idx} total={queue.length}
            onNext={next} onPrev={prev} openDeal={openDeal} queue={queue} onJump={setIdx}
            cardClass={cls} cardDragProps={drag} onRemove={onRemove}
          />
        );
      case "agenda":
        if (isManager && managerModel) {
          return (
            <TodaysAgenda
              items={buildManagerAgenda(managerModel)}
              isManager
              openDeal={openDeal}
              cardClass={cls}
              cardDragProps={drag}
              onRemove={onRemove}
            />
          );
        }
        return <TodaysAgenda deals={deals} openDeal={openDeal} cardClass={cls} cardDragProps={drag} onRemove={onRemove} />;
      case "nudges":
        return <TodaysNudges openDeal={openDeal} cardClass={cls} cardDragProps={drag} onRemove={onRemove} />;
      case "signals":
        return <BuyerSignals deals={deals} openDeal={openDeal} cardClass={cls} cardDragProps={drag} onRemove={onRemove} />;
      case "coaching":
        return <CoachingQueue deals={deals} openDeal={openDeal} cardClass={cls} cardDragProps={drag} onRemove={onRemove} />;
      case "addys-day":
        return <AddysDay deals={deals} openDeal={openDeal} cardClass={cls} cardDragProps={drag} onRemove={onRemove} />;
      case "runway":
        return <WeeklyRunway deals={deals} openDeal={openDeal} cardClass={cls} cardDragProps={drag} onRemove={onRemove} />;
      case "won-loss": {
        const WL = window.WonLossAnalysis;
        if (!WL) return null;
        return <WL deals={deals} openDeal={openDeal} cardClass={cls} cardDragProps={drag} onRemove={onRemove} />;
      }
      default:
        return null;
    }
  };

  return (
    <div className="scroll cockpit" data-screen-label="01 Home">
      {showPageHeader && (
        <>
          <div className="topbar">
            <Brand />
            <div className="dashboard-top-actions">
              {typeof window.TourTrigger === "function" && (
                <window.TourTrigger defaultMode={isManager ? "manager" : "ae"} />
              )}
              <div className="muted mono" style={{ fontSize: 11 }}>
                Pipeline check · 10:02 AM
              </div>
            </div>
          </div>

          <header className="platform-page-header cockpit-page-header">
            <div className="platform-page-copy">
              <span className="platform-eyebrow">
                <span className="cp-eyebrow-dot good" aria-hidden />
                AE cockpit
              </span>
              <h1 className="platform-page-title">Today&rsquo;s deal command</h1>
              <p className="platform-page-subtitle">Buyer proof, next actions, and forecast risk in one ranked workspace.</p>
            </div>
          </header>
        </>
      )}

      <AgentBanner deals={deals} />

      <div className="cp-stack">
        {slots.map((slotKind, slotIdx) => (
          <div
            key={slotIdx}
            className={`cp-slot ${slotKind ? "is-occupied" : "is-empty"} ${dragOverSlot === slotIdx && dragSlotIdx != null ? "is-drop-target" : ""}`}
            {...slotDropProps(slotIdx)}
          >
            {slotKind ? (
              renderCard(slotKind, slotIdx)
            ) : (
              <button
                type="button"
                className="cp-slot-empty"
                onClick={() => setPickerSlot(slotIdx)}
                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 && (
        <SectionPicker
          onSelect={(kind) => fillSlot(pickerSlot, kind)}
          onClose={() => setPickerSlot(null)}
        />
      )}
    </div>
  );
}

// Frontend-only manager demo model. Reuses Ridha's AE dashboard deals first,
// then adds three isolated teammate deals so founders can demo a 2-AE team.
const managerDemoAEs = [
  { id: "ridha", name: "Ridha Mansour", role: "AE 1" },
  { id: "leah", name: "Leah Novak", role: "AE 2" },
];

const managerExistingDealOverrides = {
  acme: {
    aeId: "ridha",
    progressionState: "Verified",
    stateLabel: "Buyer proof strong",
    nudgeReality: "Buyer proof supports the current stage.",
    crmVsNudge: "CRM stage and buyer proof agree.",
    topGap: "Keep buyer-owned close plan current",
    suggestedManagerAction: "Protect speed to close",
    managerNextAction: "Ask Ridha to secure the verbal commit date and keep the buyer-owned close plan explicit.",
    evidenceAvailable: ["Procurement confirmed security review passed", "CTO engaged", "Clear next action with Rina"],
    evidenceMissing: ["Final verbal commit date"],
    coachingQuestions: [
      "What buyer-owned action happened since the last meeting?",
      "Who owns the final commit on the buyer side?",
      "What proof supports this negotiation stage?",
    ],
    severity: "good",
    forecastRisk: false,
    triggerType: "Buyer proof strong",
    triggerAge: "Today",
    suggestedIntervention: "Do not over-manage. Keep the buyer-owned close plan visible.",
  },
  atlas: {
    aeId: "ridha",
    progressionState: "Unverified",
    stateLabel: "Seller-active, buyer-unverified",
    nudgeReality: "The rep has activity. The buyer has not committed.",
    crmVsNudge: "CRM stage says Negotiation, but buyer proof is weak.",
    topGap: "No buyer-owned next step",
    suggestedManagerAction: "Ask rep for buyer-owned next step",
    managerNextAction: "Do not inspect activity. Inspect buyer movement: who on the buyer side owns the next dated action?",
    evidenceAvailable: ["Pricing thread exists", "Prior mutual action workshop happened"],
    evidenceMissing: ["Buyer-confirmed next meeting", "Decision owner", "Approval path"],
    coachingQuestions: [
      "What buyer-owned action happened since the last meeting?",
      "Who owns the decision on the buyer side?",
      "Is this a real next step or a seller task?",
    ],
    severity: "critical",
    forecastRisk: true,
    triggerType: "No buyer-owned next step",
    triggerAge: "9 days",
    suggestedIntervention: "Message Ridha before forecast review and require buyer-side owner + date.",
    criticalCopy: "Critical: Atlas has had no buyer signal for 9 days. AE activity continues, but buyer progression is missing.",
    stageException: true,
    missingProof: "No buyer-confirmed next meeting",
    aeExplanation: "Sarah said procurement was close, but no owner or date is confirmed.",
  },
  global: {
    aeId: "ridha",
    progressionState: "Unverified",
    stateLabel: "Economic buyer missing",
    nudgeReality: "Seller work is ahead of buyer commitment.",
    crmVsNudge: "Discovery is active, but the finance owner is still outside the deal.",
    topGap: "Economic buyer missing",
    suggestedManagerAction: "Coach decision owner discovery",
    managerNextAction: "Ask Ridha to identify who owns budget validation before more proposal work.",
    evidenceAvailable: ["Updated proposal sent", "Champion identified"],
    evidenceMissing: ["Economic buyer", "Decision process", "Budget validation"],
    coachingQuestions: [
      "Who owns the decision on the buyer side?",
      "What proof supports this stage?",
      "What happens next if the buyer does nothing?",
    ],
    severity: "critical",
    forecastRisk: true,
    triggerType: "Missing economic buyer",
    triggerAge: "4 days",
    suggestedIntervention: "Require CFO path before any commit language.",
  },
  techstart: {
    aeId: "ridha",
    progressionState: "Weak",
    stateLabel: "Close date at risk",
    nudgeReality: "Buyer engagement exists, but legal is blocking proof of close path.",
    crmVsNudge: "Proposal is plausible, but the approval path needs proof.",
    topGap: "Approval path unclear",
    suggestedManagerAction: "Ask for legal owner and date",
    managerNextAction: "Have Ridha confirm legal owner, redline date, and what happens if counsel stays silent.",
    evidenceAvailable: ["Pricing aligned", "Champion engaged"],
    evidenceMissing: ["Legal response date", "Approval path"],
    coachingQuestions: [
      "What proof supports this stage?",
      "What happens next if the buyer does nothing?",
      "Who owns the approval path?",
    ],
    severity: "warn",
    forecastRisk: true,
    triggerType: "Silent stall",
    triggerAge: "6 days",
    suggestedIntervention: "Join legal unblock plan if counsel stays silent.",
  },
  startupco: {
    aeId: "ridha",
    progressionState: "Unverified",
    stateLabel: "Economic buyer missing",
    nudgeReality: "Champion likes the fit but cannot sign.",
    crmVsNudge: "Qualification is real, but budget authority is not proven.",
    topGap: "No economic buyer path",
    suggestedManagerAction: "Request finance path",
    managerNextAction: "Ask for the decision owner before letting the deal become proposal work.",
    evidenceAvailable: ["Qualification call completed", "Pain mapped"],
    evidenceMissing: ["Finance owner", "Budget validation", "Dated buyer-owned next step"],
    coachingQuestions: [
      "Who owns the decision on the buyer side?",
      "What buyer-owned action happened since the last meeting?",
      "Is this a real next step or a seller task?",
    ],
    severity: "warn",
    forecastRisk: true,
    triggerType: "Missing economic buyer",
    triggerAge: "3 days",
    suggestedIntervention: "Coach economic-buyer discovery before proposal work.",
  },
};

// Each Leah deal maps to an existing seed deal for click-through demo. The
// data shows Leah's specific story, but the deal page opens against a real
// AE-side deal so the prototype navigates fully.
const managerSecondAeDeals = [
  {
    id: "mgr-meridian",
    sourceDealId: "atlas",
    aeId: "leah",
    company: "Meridian Foods",
    dealName: "Warehouse Forecasting Rollout",
    stage: "Proposal",
    amount: 275000,
    closeDate: "May 24, 2026",
    buyerScore: 44,
    sellerScore: 79,
    activityCount: 11,
    progressionState: "Unverified",
    stateLabel: "Stage advanced without proof",
    nudgeReality: "CRM stage says Proposal, but buyer proof is weak.",
    crmVsNudge: "CRM stage says Proposal. Nudge reality says buyer-unverified.",
    topGap: "Stage advanced without proof",
    suggestedManagerAction: "Reject stage move until proof exists",
    managerNextAction: "Ask Leah for decision owner, approval path, and a buyer-confirmed next meeting before keeping Proposal.",
    evidenceAvailable: ["Two seller follow-ups", "Demo recap sent"],
    evidenceMissing: ["Decision owner", "Approval path", "Buyer-confirmed next meeting"],
    coachingQuestions: [
      "What proof supports this stage?",
      "What buyer-owned action happened since the last meeting?",
      "Is this a real next step or a seller task?",
    ],
    severity: "critical",
    forecastRisk: true,
    triggerType: "Stage inflation",
    triggerAge: "2 days",
    suggestedIntervention: "Review in 1:1 and request buyer proof before forecast inclusion.",
    stageException: true,
    missingProof: "No decision process",
    aeExplanation: "Leah moved it after a strong demo; buyer has not confirmed ownership or date.",
    committee: [
      { role: "Champion",  name: "Owen Kerr",  state: "amber", signal: "Engaged on demo · no follow-up scheduled" },
      { role: "Econ buyer", name: "—",          state: "warn",  signal: "No decision owner identified" },
      { role: "Approver",  name: "—",          state: "warn",  signal: "No approval path mapped" },
    ],
  },
  {
    id: "mgr-evergreen",
    sourceDealId: "acme",
    aeId: "leah",
    company: "Evergreen Bank",
    dealName: "Risk Ops Expansion",
    stage: "Negotiation",
    amount: 390000,
    closeDate: "Jun 7, 2026",
    buyerScore: 86,
    sellerScore: 82,
    activityCount: 7,
    progressionState: "Verified",
    stateLabel: "Buyer proof strong",
    nudgeReality: "Buyer is carrying the next step.",
    crmVsNudge: "CRM stage and buyer proof agree.",
    topGap: "Keep approval path explicit",
    suggestedManagerAction: "Protect buyer momentum",
    managerNextAction: "Have Leah keep the CFO-owned approval path dated and visible.",
    evidenceAvailable: ["CFO owns approval", "Buyer sent redlines", "Next meeting is buyer-scheduled"],
    evidenceMissing: ["Final procurement SLA"],
    coachingQuestions: [
      "What buyer-owned action happened since the last meeting?",
      "What proof supports this stage?",
      "Who owns the final approval path?",
    ],
    severity: "good",
    forecastRisk: false,
    triggerType: "Buyer proof strong",
    triggerAge: "Today",
    suggestedIntervention: "No intervention. Keep next buyer step protected.",
    committee: [
      { role: "Champion",  name: "Eve Marchand", state: "good", signal: "Sent redlines · driving close" },
      { role: "Econ buyer", name: "Marc Devlin",  state: "good", signal: "CFO owns approval path" },
      { role: "Procurement", name: "Hugo Klein",  state: "amber", signal: "Awaiting SLA" },
    ],
  },
  {
    id: "mgr-kestrel",
    sourceDealId: "techstart",
    aeId: "leah",
    company: "Kestrel Systems",
    dealName: "Security Workflow Pilot",
    stage: "Demo",
    amount: 118000,
    closeDate: "Jun 18, 2026",
    buyerScore: 53,
    sellerScore: 76,
    activityCount: 9,
    progressionState: "Weak",
    stateLabel: "Seller-active, buyer-unverified",
    nudgeReality: "High seller activity, low buyer progression.",
    crmVsNudge: "Demo is complete, but buyer-side ownership is thin.",
    topGap: "No buyer-owned next step",
    suggestedManagerAction: "Coach the blocker, not the story",
    managerNextAction: "Ask Leah to convert demo interest into a buyer-owned next action with date and owner.",
    evidenceAvailable: ["Demo completed", "Security lead attended"],
    evidenceMissing: ["Buyer-owned next step", "Economic buyer path"],
    coachingQuestions: [
      "What happens next if the buyer does nothing?",
      "Who owns the decision on the buyer side?",
      "Is this a real next step or a seller task?",
    ],
    severity: "warn",
    forecastRisk: true,
    triggerType: "Critical risk ignored",
    triggerAge: "5 days",
    suggestedIntervention: "Ask for buyer-owned action before more seller follow-up.",
    committee: [
      { role: "Champion",  name: "Yara Bishop", state: "amber", signal: "Engaged on demo · no buyer step" },
      { role: "Security",  name: "Liam Soto",   state: "good",  signal: "Attended demo" },
      { role: "Econ buyer", name: "—",           state: "warn",  signal: "No path identified" },
    ],
  },
];

const managerCoachingPatterns = {
  ridha: {
    pattern: "Advances forecast confidence before buyer-owned next steps are confirmed",
    evidenceCount: 3,
    exampleDealIds: ["atlas", "global", "startupco"],
    action: "Coach on decision sequencing: require decision owner + dated next buyer step before moving from Demo to Proposal.",
  },
  leah: {
    pattern: "Creates proposal work before approval path is clear",
    evidenceCount: 2,
    exampleDealIds: ["mgr-meridian", "mgr-kestrel"],
    action: "Coach the blocker, not the story: approval path, decision owner, and buyer-side date before proposal work.",
  },
};

function formatManagerMoney(value = 0) {
  if (value >= 1000000) return `$${(value / 1000000).toFixed(value >= 10000000 ? 0 : 1)}M`;
  return `$${Math.round(value / 1000)}K`;
}

// Build a manager-flavoured agenda from the manager model. Manager doesn't
// ride along on buyer meetings — they run forecast reviews, 1:1s, and
// deeper deal reviews. Synthesises 3-4 items from interventions + AE list.
function buildManagerAgenda(model) {
  if (!model || !model.aes || !model.deals) return [];
  const items = [];

  const criticalDeals = model.deals
    .filter((d) => d.severity === "critical" || d.forecastRisk)
    .slice(0, 4);
  const atRiskValue = criticalDeals.reduce((sum, d) => sum + (d.amount || d.value || 0), 0);

  // Weekly forecast review — anchor of the manager week.
  items.push({
    id: "mgr-forecast-review",
    kind: "forecast-review",
    title: "Weekly forecast review",
    subtitle: "Lock the call before EOW",
    date: "Fri, 5:00 PM PT",
    duration: "60 min",
    attendees: model.aes.map((a) => a.name),
    goal: "Walk the at-risk roster, lock category moves for next week's call.",
    intel: {
      pulse: `${(model.interventions || []).length} interventions queued · ${formatManagerMoney(atRiskValue)} sitting in at-risk forecast.`,
      risks: criticalDeals.slice(0, 3).map((d) => `${d.company} · ${d.topGap || d.statusLabel || "needs review"}`),
      talkingPoints: [
        "Walk the top 3 critical interventions — declared vs evidence",
        "Confirm category moves (Best Case ↔ Pipeline ↔ Commit)",
        "Identify the dominant coaching pattern for the week",
      ],
      docs: ["Forecast dashboard", "At-risk roster", "Last week's call deltas"],
    },
  });

  // One 1:1 per AE — pre-loaded with that rep's worst deal.
  model.aes.forEach((ae, i) => {
    const aeDeals = model.deals.filter((d) => d.aeId === ae.id);
    if (aeDeals.length === 0) return;
    const top = aeDeals.find((d) => d.severity === "critical") || aeDeals[0];
    const dayLabel = i === 0 ? "Tue, 10:00 AM PT" : "Wed, 11:30 AM PT";
    items.push({
      id: `mgr-1on1-${ae.id}`,
      kind: "one-on-one",
      title: `1:1 with ${ae.name}`,
      subtitle: top ? `Coach on ${top.topGap || "deal progression"}` : "Coaching block",
      date: dayLabel,
      duration: "30 min",
      attendees: [ae.name],
      relatedDealId: top ? (top.sourceDealId || top.id) : null,
      goal: top
        ? `Pressure-test ${top.company} · ${top.suggestedManagerAction || "ask for buyer-owned next step"}.`
        : "Walk the pipeline together.",
      intel: {
        pulse: `${ae.name} has ${aeDeals.length} active deals · ${aeDeals.filter((d) => d.severity === "critical").length} flagged critical.`,
        risks: aeDeals
          .filter((d) => d.severity === "critical")
          .slice(0, 2)
          .map((d) => `${d.company} · ${d.topGap || d.statusLabel}`),
        talkingPoints: top && top.coachingQuestions
          ? top.coachingQuestions.slice(0, 3)
          : [
              "What buyer-owned action happened since the last meeting?",
              "Where is the next step happening on the buyer side?",
              "What proof would move this from Best Case to Commit?",
            ],
        docs: aeDeals.slice(0, 3).map((d) => `${d.company} brief`),
      },
    });
  });

  // Single top-deal review — the deepest dive for the week.
  const topReview = (model.interventions || [])[0] || criticalDeals[0];
  if (topReview) {
    items.push({
      id: `mgr-deal-review-${topReview.id}`,
      kind: "deal-review",
      title: `${topReview.company} deal review`,
      subtitle: topReview.suggestedManagerAction || "Pressure-test the path to close",
      date: "Thu, 2:00 PM PT",
      duration: "45 min",
      attendees: [topReview.aeName || "AE", "You"],
      relatedDealId: topReview.sourceDealId || topReview.id,
      goal: topReview.managerNextAction || "Confirm the next buyer-owned action and lock the forecast call.",
      intel: {
        pulse: topReview.crmVsNudge || `${topReview.company} is ${topReview.statusLabel || topReview.stateLabel}.`,
        risks: (topReview.evidenceMissing || []).map((e) => `${e} missing`),
        talkingPoints: topReview.coachingQuestions || [
          "What changed since last review?",
          "What buyer-owned action is the next step?",
          "If nothing changes by Friday, what's the recommendation?",
        ],
        docs: [`${topReview.company} brief`, `${topReview.company} forecast row`, "Stakeholder map"],
      },
    });
  }

  return items;
}

function baseManagerDealFromSeed(deal, override) {
  const firstGap = deal.nudges?.[0];
  const ae = managerDemoAEs.find((rep) => rep.id === override.aeId) || managerDemoAEs[0];
  const diag = (typeof DEAL_DIAGNOSIS !== "undefined" ? DEAL_DIAGNOSIS : window.DEAL_DIAGNOSIS)?.[deal.id];
  return {
    // Spread the AE-shape seed deal first so manager deals carry every AE
    // field (value, status, statusLabel, summary, nudges, activity,
    // champion, gap, trend, stakeholders, …) and render correctly through
    // the AE DealsScreen.
    ...deal,
    id: deal.id,
    sourceDealId: deal.id,
    aeId: ae.id,
    aeName: ae.name,
    committee: diag?.committee || deal.committee,
    // Manager-specific layered on top of the AE shape.
    amount: deal.value,
    activityCount: deal.activity?.length || 0,
    progressionState: "Weak",
    stateLabel: "Seller-active, buyer-unverified",
    nudgeReality: deal.summary,
    crmVsNudge: `${deal.stage} needs buyer proof before manager confidence.`,
    topGap: firstGap?.title || "Buyer proof missing",
    suggestedManagerAction: firstGap?.action || "Ask rep for evidence",
    managerNextAction: "Ask the rep what buyer-owned action happened since the last meeting.",
    evidenceAvailable: (deal.activity || []).filter((item) => item.tag === "buyer" || item.tag === "milestone").slice(0, 2).map((item) => item.title),
    evidenceMissing: ["Buyer-owned next step"],
    coachingQuestions: ["What buyer-owned action happened since the last meeting?"],
    severity: deal.status === "at-risk" ? "critical" : deal.status === "needs-attention" ? "warn" : "good",
    forecastRisk: deal.status !== "healthy",
    triggerType: "No buyer-owned next step",
    triggerAge: "This week",
    suggestedIntervention: "Ask for buyer proof before forecast review.",
    ...override,
  };
}

// Enrich a manager-only synthetic deal (AE2 deals — Meridian, Evergreen,
// Kestrel) with AE-compatible fields. The synthetic deals are authored in
// manager-shape (severity/stateLabel/topGap/etc.) — these projections let
// the same DealsScreen / Kanban / list views render them next to the
// existing-overlaid deals without special-casing.
function enrichManagerSecondAe(deal) {
  const ae = managerDemoAEs.find((rep) => rep.id === deal.aeId);
  const status =
    deal.severity === "critical" ? "at-risk"
  : deal.severity === "warn"     ? "needs-attention"
  : "healthy";
  const statusLabel =
    deal.severity === "critical" ? "At risk"
  : deal.severity === "warn"     ? "Needs attention"
  : "On track";
  const champion = deal.committee?.[0] || {};
  return {
    ...deal,
    aeName: ae?.name || "AE",
    // AE-shape projections — match the field names DealsScreen / Pipeline
    // / DealTable / lens helpers consume.
    value: deal.amount,
    status,
    statusLabel,
    summary: deal.nudgeReality || deal.stateLabel || "",
    gap: (deal.buyerScore || 0) - (deal.sellerScore || 0),
    champion: champion.name || "—",
    championRole: champion.role || "",
    arr: `$${Math.round((deal.amount || 0) / 1000)}K ARR`,
    nudges: deal.nudges || [],
    activity: deal.activity || [],
    trend: deal.trend || [],
    sellerTrend: deal.sellerTrend || [],
    scoreEvents: deal.scoreEvents || [],
    stakeholders: deal.stakeholders || (deal.committee || []).map((c) => ({
      name: c.name,
      role: c.role,
      power: 3,
      support: c.state === "good" ? 4 : c.state === "amber" ? 3 : 2,
      missing: c.name === "—",
    })),
  };
}

function buildManagerDemoData(deals = []) {
  const existingOrder = ["atlas", "global", "techstart", "acme", "startupco"];
  const existing = existingOrder
    .map((id) => {
      const deal = deals.find((item) => item.id === id);
      const override = managerExistingDealOverrides[id];
      if (!deal || !override) return null;
      return baseManagerDealFromSeed(deal, override);
    })
    .filter(Boolean);
  const secondAe = managerSecondAeDeals.map(enrichManagerSecondAe);
  const managerDeals = [...existing, ...secondAe];
  const openDeals = managerDeals.filter((deal) => !/^closed/i.test(deal.stage || ""));
  const sum = (items) => items.reduce((total, deal) => total + (deal.amount || 0), 0);
  return {
    aes: managerDemoAEs,
    deals: managerDeals,
    riskyDeals: managerDeals
      .filter((deal) => deal.progressionState !== "Verified" || deal.severity === "critical")
      .sort((a, b) => (b.severity === "critical") - (a.severity === "critical") || (b.amount || 0) - (a.amount || 0)),
    interventions: managerDeals
      .filter((deal) => deal.severity === "critical" || ["Silent stall", "Missing economic buyer", "No buyer-owned next step", "Critical risk ignored", "Stage inflation"].includes(deal.triggerType))
      .filter((deal) => deal.progressionState !== "Verified"),
    stageExceptions: managerDeals.filter((deal) => deal.stageException),
    metrics: {
      activePipeline: sum(openDeals),
      verifiedPipeline: sum(openDeals.filter((deal) => deal.progressionState === "Verified")),
      buyerUnverifiedPipeline: sum(openDeals.filter((deal) => deal.progressionState !== "Verified")),
      criticalInterventions: managerDeals.filter((deal) => deal.severity === "critical").length,
      forecastAtRisk: sum(openDeals.filter((deal) => deal.forecastRisk)),
      noBuyerNextStepCount: openDeals.filter((deal) =>
        deal.triggerType === "No buyer-owned next step" ||
        (deal.evidenceMissing || []).some((item) => /buyer-owned next step/i.test(item))
      ).length,
      stageAheadCount: openDeals.filter((deal) => deal.stageException || deal.progressionState === "Unverified").length,
    },
  };
}

const MANAGER_MOMENTS = [
  {
    key: "truth",
    index: "01",
    label: "Pipeline truth",
    question: "Which deals look active, but aren't backed by buyer proof?",
    cta: "Inspect risky deals",
    icon: "target",
    tone: "warn",
  },
  {
    key: "review",
    index: "02",
    label: "Deal review",
    question: "What exactly should I challenge the AE on?",
    cta: "Open Deal Review Pack",
    icon: "note",
    tone: "amber",
  },
  {
    key: "coaching",
    index: "03",
    label: "Coaching & stage truth",
    question: "One-off issue, or a team pattern to enforce?",
    cta: "Coach & decide",
    icon: "users",
    tone: "good",
  },
];

// Severity → tone triad, shared by every coaching surface so the colour
// language matches the AE side (warn / amber / good).
function severityTone(severity) {
  return severity === "critical" ? "warn" : severity === "warn" ? "amber" : "good";
}
function aeInitials(name = "") {
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").toUpperCase();
}
function aeShortName(name = "") {
  const parts = name.split(/\s+/).filter(Boolean);
  return parts.length < 2 ? name : `${parts[0]} ${parts[1][0]}.`;
}

// Buying committee — the named stakeholders on the buyer side, each with
// a role, a state, and a one-line signal. Replaces vague "methodology
// score" callouts with concrete people the manager can act on. Rendered
// inside the coach-prep collapse on coaching + AE priority cards.
function Committee({ rows = [] }) {
  if (!rows.length) return null;
  return (
    <div className="coach-prep-block coach-committee">
      <span className="coach-mini-label">Buying committee</span>
      <ul className="coach-committee-list">
        {rows.map((r, i) => (
          <li key={i} className={`coach-committee-row tone-${r.state || "good"}`}>
            <span className={`coach-committee-dot tone-${r.state || "good"}`} aria-hidden="true" />
            <span className="coach-committee-role">{r.role}</span>
            <span className="coach-committee-name">{r.name || "—"}</span>
            <span className="coach-committee-signal">{r.signal}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

// One coaching conversation. The essentials stay on the card — who, the
// buyer reality, the manager's next move. The coaching prep (questions,
// missing proof, recommended intervention) tucks behind a disclosure so the
// card stays calm until the manager is actually prepping the conversation.
// The active card's body is the click target for "open deal room"; inner
// buttons stop propagation so they keep their own actions.
function CoachCard({ deal, coached, onToggleCoached, openDeal, active = true, onAction }) {
  const tone = severityTone(deal.severity);
  const canOpen = !!deal.sourceDealId;
  const CategoryStrip = window.DealCategoryStrip;
  const [prepOpen, setPrepOpen] = useState(true);
  const interactive = active && canOpen;
  const openRoom = () => { if (canOpen) openDeal(deal.sourceDealId); };

  const questions = deal.coachingQuestions || [];
  const missing = deal.evidenceMissing || [];
  const committee = deal.committee || [];
  const hasPrep = questions.length > 0 || missing.length > 0 || committee.length > 0;

  const prepBits = [];
  if (committee.length) prepBits.push(`${committee.length} stakeholder${committee.length === 1 ? "" : "s"}`);
  if (questions.length) prepBits.push(`${questions.length} question${questions.length === 1 ? "" : "s"}`);

  return (
    <article
      className={`coach-card tone-${tone} ${coached ? "is-coached" : ""} ${interactive ? "is-clickable" : ""}`}
      role={interactive ? "button" : undefined}
      tabIndex={interactive ? 0 : -1}
      onClick={interactive ? openRoom : undefined}
      onKeyDown={interactive ? (e) => {
        if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openRoom(); }
      } : undefined}
      aria-label={interactive ? `Open ${deal.company} deal room` : 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} · {aeShortName(deal.aeName)}</span>
        </div>
        <div className="coach-card-meta-r">
          {CategoryStrip && <CategoryStrip deal={deal} compact className="coach-cat-strip" />}
          <b>{formatManagerMoney(deal.amount)}</b>
        </div>
      </header>

      <button
        type="button"
        className="coach-cta"
        onClick={(e) => {
          e.stopPropagation();
          if (!active) return;
          // CTA fires the coaching action (toast + mark coached + carousel
          // advances). Body click stays as "open deal room" navigation.
          if (onAction) onAction();
          else if (canOpen) openRoom();
        }}
        disabled={!active}
      >
        <span className="coach-cta-text">{deal.suggestedManagerAction || deal.managerNextAction}</span>
        <CoachCtaChannel channel="slack" />
        <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><b>{deal.triggerAge}</b> · {deal.triggerType}</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>Deal context</span>
            {prepBits.length > 0 && <em>{prepBits.join(" · ")}</em>}
          </button>
          <div className="coach-prep-body">
            <div className="coach-prep-inner">
              {committee.length > 0 && <Committee rows={committee} />}
              {missing.length > 0 && (
                <div className="coach-prep-block">
                  <span className="coach-mini-label warn">Missing proof</span>
                  <p className="coach-evidence-line">{missing.join("  ·  ")}</p>
                </div>
              )}
              {questions.length > 0 && (
                <div className="coach-prep-block coach-prep-block--hero">
                  <span className="coach-mini-label">Ask the rep</span>
                  <ul className="coach-q-list">
                    {questions.map((q, i) => <li key={i}>{q}</li>)}
                  </ul>
                </div>
              )}
            </div>
          </div>
        </div>
      )}

      <footer className="coach-card-foot">
        {canOpen && (
          <button
            type="button"
            className="coach-btn ghost coach-foot-lead"
            onClick={(e) => { e.stopPropagation(); openDeal(deal.sourceDealId); }}
          >
            Open deal room <Icon name="arrow" size={11} />
          </button>
        )}
        <button
          type="button"
          className={`coach-btn ${coached ? "is-done" : ""}`}
          onClick={(e) => { e.stopPropagation(); onToggleCoached(deal.id); }}
          aria-pressed={coached}
        >
          <Icon name="check" size={11} /> {coached ? "Coached" : "Mark coached"}
        </button>
      </footer>
    </article>
  );
}

// Rotative horizontal carousel for the coaching queue. The active card sits
// centred; the prev/next deals peek in at both edges. Navigate by dragging
// the stage, the flanking arrows, the pips, or the arrow keys — and it loops
// around in whichever direction is shorter.
function CoachCarousel({ deals, coached, onToggleCoached, openDeal }) {
  const n = deals.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 activeCardRef = useRef(null);

  // Keep the index in range if the queue shrank under it (e.g. filter change).
  useEffect(() => { setIdx((i) => (i > n - 1 ? 0 : i)); }, [n]);

  // Measure the active card so the stage can animate its height between
  // cards whose content (evidence, questions) makes them different heights.
  React.useLayoutEffect(() => {
    const el = activeCardRef.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, n]);

  if (n === 0) {
    return <div className="coach-empty">Pipeline&rsquo;s clean. Buyer proof is holding.</div>;
  }

  const go = (delta) => setIdx((i) => (i + delta + n) % n);

  // Primary CTA — opens an action preview showing the drafted coaching DM
  // with evidence + expected impact. Confirm fires: toast, mark coached,
  // advance carousel, then a delayed outcome toast simulating the rep's
  // follow-through so the demo closes the loop.
  const fireAction = (deal) => {
    const who = aeShortName(deal.aeName || "the rep");
    const fullName = deal.aeName || who;
    const questions = deal.coachingQuestions || [];
    const missing = deal.evidenceMissing || [];
    const messageText = [
      `${deal.company} · ${deal.stage}`,
      "",
      "Quick check before forecast review:",
      ...questions.map((q) => "• " + q),
    ].join("\n");

    // Coaching impact — Slack DMs land fast, severity dictates the lift.
    const sev = deal.severity === "critical" ? "high" : deal.severity === "warn" ? "mid" : "good";
    const likelihood = { high: 78, mid: 85, good: 92 }[sev];
    const firmnessPct = { high: 0.55, mid: 0.70, good: 0.85 }[sev];
    const firmnessRaw = Math.round((deal.amount || 0) * firmnessPct);
    const firmness = formatManagerMoney(firmnessRaw);

    const commit = () => {
      if (typeof window.fireToast === "function") {
        window.fireToast(`Coaching prep sent to ${who}`);
      }
      if (!coached.has(deal.id)) onToggleCoached(deal.id);
      setTimeout(() => go(1), 500);
      // Simulated follow-through: rep responds, gap closes, forecast firms.
      setTimeout(() => {
        if (typeof window.fireToast === "function") {
          window.fireToast(`✓ ${who} confirmed ${deal.company} · gap closed · +${firmness} firmed`);
        }
      }, 5200);
    };
    if (typeof window.previewAction === "function") {
      window.previewAction({
        kicker: "Preview · Coaching prep",
        title: `Send to ${fullName}`,
        meta: [
          { label: "TO", value: fullName },
          { label: "CHANNEL", value: "Slack DM" },
          { label: "CONTEXT", value: `${deal.company} · ${deal.stage}` },
        ],
        body: {
          label: "Drafted message",
          text: messageText,
        },
        impact: {
          likelihood,
          timeHorizon: "24h",
          firmness,
          firmnessRaw,
        },
        evidence: [
          { label: "Trigger", value: `${deal.triggerType} · ${deal.triggerAge}` },
          ...(missing.length ? [{ label: "Missing proof", value: missing.join(", ") }] : []),
          ...(deal.suggestedIntervention ? [{ label: "Recommended", value: deal.suggestedIntervention }] : []),
        ],
        primaryLabel: `Send to ${who}`,
        onConfirm: commit,
      });
    } else {
      commit();
    }
  };

  // Signed shortest-path offset of card i from the active index, so the deck
  // wraps in whichever direction is closer (true rotative feel).
  const signedOffset = (i) => {
    let d = i - idx;
    if (d > n / 2) d -= n;
    if (d < -n / 2) d += n;
    return d;
  };

  // Pointer tracking WITHOUT setPointerCapture — capture retargets the
  // trailing mouseup/click onto the stage and the card's click handler
  // never fires. Without capture, the pointer stays on the original target
  // during normal swipes (release usually happens within the stage), so
  // clicks reach the card and drag still works.
  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);
  };
  // Pointer left the stage mid-drag — release without committing.
  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); }
  };

  return (
    <div className="coach-carousel">
      <div className="coach-stage-wrap">
        <button type="button" className="coach-nav prev" onClick={() => go(-1)} aria-label="Previous deal">
          <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={`Coaching deal ${idx + 1} of ${n}`}
          onPointerDown={onPointerDown}
          onPointerMove={onPointerMove}
          onPointerUp={endDrag}
          onPointerCancel={endDrag}
          onPointerLeave={onPointerLeave}
          onClickCapture={onClickCapture}
          onKeyDown={onKeyDown}
        >
          <div className="coach-track" style={{ transform: `translateX(${dragDx}px)` }}>
            {deals.map((d, i) => {
              const off = signedOffset(i);
              const pos = off === 0 ? "active" : Math.abs(off) === 1 ? "peek" : "hidden";
              return (
                <div
                  key={d.id}
                  className="coach-slide"
                  data-pos={pos}
                  style={{ "--off": off }}
                  aria-hidden={pos !== "active"}
                >
                  <div ref={pos === "active" ? activeCardRef : null}>
                    <CoachCard
                      deal={d}
                      coached={coached.has(d.id)}
                      onToggleCoached={onToggleCoached}
                      openDeal={openDeal}
                      active={pos === "active"}
                      onAction={pos === "active" ? () => fireAction(d) : undefined}
                    />
                  </div>
                </div>
              );
            })}
          </div>
        </div>

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

      <div className="coach-rail">
        <div className="coach-pips" role="tablist" aria-label="Coaching deals">
          {deals.map((d, i) => (
            <button
              key={d.id}
              type="button"
              className={`coach-pip ${i === idx ? "is-on" : ""} ${coached.has(d.id) ? "is-coached" : ""}`}
              data-tone={severityTone(d.severity)}
              onClick={() => setIdx(i)}
              aria-label={`Go to ${d.company}`}
              aria-selected={i === idx}
            />
          ))}
        </div>
        <span className="coach-counter">
          {String(idx + 1).padStart(2, "0")} / {String(n).padStart(2, "0")}
        </span>
      </div>
    </div>
  );
}

// Coaching queue — the manager's "what should I actually do" surface.
// Two parts: team patterns (one-off vs habit) up top, then the prioritised
// intervention queue. Clicking a rep's pattern filters the queue to them.
function CoachingScreen({ model, openDeal }) {
  const { aes, deals, interventions, metrics } = model;
  const [coached, setCoached] = useState(() => new Set());

  const toggleCoached = (id) => setCoached((curr) => {
    const next = new Set(curr);
    if (next.has(id)) next.delete(id); else next.add(id);
    return next;
  });

  // Per-rep pattern cards — the static coaching patterns enriched with the
  // rep's live open-intervention count so the manager sees scale at a glance.
  const patterns = useMemo(() => aes.map((ae) => {
    const p = managerCoachingPatterns[ae.id];
    const openCount = interventions.filter((d) => d.aeId === ae.id).length;
    return { ae, openCount, ...(p || {}) };
  }).filter((p) => p.pattern), [aes, interventions]);

  // Queue: sort critical-first by amount. Order stays stable as cards get
  // coached so the carousel index never jumps under the manager mid-swipe.
  // AE filtering happens upstream (via Recalc's By-AE rollup), not here.
  const rank = (d) => (d.severity === "critical" ? 2 : d.severity === "warn" ? 1 : 0);
  const queue = useMemo(() => interventions
    .slice()
    .sort((a, b) => rank(b) - rank(a) || (b.amount || 0) - (a.amount || 0)),
  [interventions]);

  const dueCount = queue.filter((d) => !coached.has(d.id)).length;

  return (
    <div className="coach-screen" data-screen-label="Coaching queue">
      {/* KPI strip — only metrics unique to the coaching workflow. The
          critical count and forecast-at-risk dollars live in Recalc, so
          they don't repeat here. */}
      <div className="coach-kpis">
        <div className="coach-kpi tone-warn">
          <span className="coach-kpi-label">Conversations due</span>
          <span className="coach-kpi-value">{dueCount}</span>
        </div>
        <div className="coach-kpi tone-amber">
          <span className="coach-kpi-label">Reps showing a pattern</span>
          <span className="coach-kpi-value">{patterns.length}<em> / {aes.length}</em></span>
        </div>
      </div>

      <section className="coach-block">
        <header className="coach-block-head">
          <div className="coach-block-title-row">
            <span className="coach-eyebrow-dot warn" aria-hidden />
            <h2 className="coach-block-title">
              {dueCount} {dueCount === 1 ? "conversation" : "conversations"} to have
            </h2>
          </div>
          <p className="coach-block-sub">
            Open deals where seller activity is racing ahead of buyer signals — exactly where coaching pays off.
            {coached.size > 0 && (
              <em> · {coached.size} marked coached today</em>
            )}
          </p>
        </header>
        <CoachCarousel
          deals={queue}
          coached={coached}
          onToggleCoached={toggleCoached}
          openDeal={openDeal}
        />
      </section>
    </div>
  );
}

// Manager view = forecast experience. The sidebar sections map to forecast
// modules (recalc / deals / backtest / config), plus a coaching queue that
// surfaces the per-deal coaching layer. The manager view itself is just thin
// chrome around whichever section is active.
function ManagerView({ deals, openDeal, managerSection = "recalc", setManagerSection }) {
  const model = useMemo(() => buildManagerDemoData(deals), [deals]);

  return (
    <div className="manager-view">
      {managerSection === "coaching" ? (
        <CoachingScreen model={model} openDeal={openDeal} />
      ) : (
        <ForecastModelScreen
          deals={model.deals}
          openDeal={(id, tab = "overview") => openDeal(id, tab)}
          section={managerSection}
          setSection={setManagerSection}
        />
      )}
    </div>
  );
}


// ── Today's agenda ───────────────────────────────────────────────────────
// A meeting briefing card per upcoming meeting. Each card answers four
// questions at a glance: WHAT (meeting + company), WHEN (date/time block),
// WITH WHOM (attendees enriched with their role), WHAT TO DO (a row of
// prep actions, each opening a tailored ActionPreview modal so the rep can
// actually execute the prep from the card).

// "Apr 30, 3:00 PM PT" → { dateLabel: "Apr 30", timeLabel: "3:00 PM", tz: "PT" }
function parseMeetingDate(dateStr) {
  if (!dateStr) return { dateLabel: "TBD", timeLabel: "", tz: "" };
  const parts = dateStr.split(",").map((s) => s.trim());
  const dateLabel = parts[0] || dateStr;
  const rest = parts.slice(1).join(", ");
  const timeMatch = rest.match(/(\d{1,2}:\d{2}\s*(?:AM|PM))/i);
  const tzMatch = rest.match(/\b([A-Z]{2,4})\s*$/);
  return {
    dateLabel,
    timeLabel: timeMatch ? timeMatch[1] : "",
    tz: tzMatch ? tzMatch[1] : "",
  };
}

function agendaToneFor(deal, meeting) {
  const intel = meeting.intel || {};
  const riskCount = (intel.risks || []).length;
  if (riskCount >= 2 || deal.status === "at-risk") return "warn";
  if (riskCount >= 1 || deal.status === "needs-attention") return "amber";
  return "good";
}

// Build a list of attendees from the meeting, enriched with their role
// from the deal's stakeholder map. Strips out the rep ("You", "Ridha").
function formatAttendees(meeting, deal) {
  const me = /^(you|ridha)\b/i;
  const raw = (meeting.attendees || []).filter((a) => !me.test(a || ""));
  if (raw.length === 0) return { display: "Solo prep · no external attendees", count: 0 };
  const enriched = raw.map((name) => {
    const stk = (deal.stakeholders || []).find((s) => s && s.name === name);
    return { name, role: stk?.role || null };
  });
  let display;
  if (enriched.length === 1) {
    display = enriched[0].role
      ? `${enriched[0].name} (${enriched[0].role})`
      : enriched[0].name;
  } else if (enriched.length === 2) {
    display = enriched.map((p) => p.name).join(" + ");
  } else {
    display = `${enriched[0].name} + ${enriched.length - 1} others`;
  }
  return { display, count: enriched.length, list: enriched };
}

// Return up to 3 prep actions per meeting, ordered by relevance: invite
// missing stakeholder → review doc → send agenda → save brief.
function deriveMeetingActions(meeting, deal) {
  const intel = meeting.intel || {};
  const risks = intel.risks || [];
  const docs = intel.docs || [];
  const stakeholders = deal.stakeholders || [];
  const actions = [];

  const missingStakeholder = stakeholders.find((s) => s && s.missing && s.name && !s.name.startsWith("—"));
  const stakeholderRisk = risks.find((r) => /cfo|economic buyer|finance|stakeholder|loop/i.test(r));
  if (missingStakeholder) {
    actions.push({
      id: "invite",
      label: `Invite ${missingStakeholder.name.split(" ")[0]}`,
      icon: "users",
      tone: "warn",
      primary: true,
      primaryLabel: "Send invite",
      build: () => ({
        kicker: "Pre-meeting outreach",
        title: `Invite ${missingStakeholder.name} to ${meeting.title}`,
        meta: [
          { label: "WHEN",    value: meeting.date },
          { label: "DEAL",    value: `${deal.company} · ${deal.stage}` },
          { label: "TO",      value: `${missingStakeholder.name} · ${missingStakeholder.role}` },
          { label: "CHANNEL", value: "Email" },
        ],
        body: {
          subject: `Quick ask — join us for ${meeting.title}`,
          label: "Drafted invite",
          text: `${missingStakeholder.name.split(" ")[0]} — wanted to loop you into the ${meeting.title.toLowerCase()} on ${meeting.date}. Given your role as ${missingStakeholder.role}, having you in the room means we can resolve any ${stakeholderRisk ? "approval / timing" : "decision"} questions live instead of scheduling a second pass. 30 min — happy to share the brief beforehand.`,
        },
        evidence: [
          { label: "Why now", value: stakeholderRisk || `${missingStakeholder.role} hasn't yet engaged in the deal.` },
          { label: "Outcome", value: `Get ${missingStakeholder.name.split(" ")[0]} in the room so the next step doesn't wait on a separate intro.` },
        ],
      }),
    });
  }

  const reviewableDoc = docs.find((d) => /redline|proposal|brief|battlecard|deck|spec/i.test(d));
  if (reviewableDoc) {
    actions.push({
      id: "doc",
      label: `Review ${reviewableDoc.length > 22 ? reviewableDoc.slice(0, 22) + "…" : reviewableDoc}`,
      icon: "book",
      tone: "amber",
      primaryLabel: "Open doc",
      build: () => ({
        kicker: "Doc prep",
        title: `Review ${reviewableDoc} before ${meeting.title}`,
        meta: [
          { label: "WHEN", value: meeting.date },
          { label: "DEAL", value: `${deal.company} · ${deal.stage}` },
          { label: "DOC",  value: reviewableDoc },
        ],
        body: intel.pulse ? { label: "Why this doc", text: intel.pulse } : null,
        evidence: [
          ...docs.slice(0, 3).map((d) => ({ label: d === reviewableDoc ? "Open" : "Reference", value: d })),
          ...(intel.talkingPoints || []).slice(0, 2).map((p) => ({ label: "Cover", value: p })),
        ],
      }),
    });
  }

  // Always offer "Send agenda" and "Save brief" — bread-and-butter prep actions.
  const attendeesSummary = formatAttendees(meeting, deal).display;
  actions.push({
    id: "agenda",
    label: "Send agenda",
    icon: "mail",
    tone: "info",
    primaryLabel: "Send agenda",
    build: () => ({
      kicker: "Pre-meeting agenda",
      title: `Send agenda for ${meeting.title}`,
      meta: [
        { label: "WHEN",      value: meeting.date },
        { label: "DEAL",      value: `${deal.company} · ${deal.stage}` },
        { label: "ATTENDEES", value: attendeesSummary },
      ],
      body: {
        subject: `Agenda · ${meeting.title}`,
        label: "Drafted agenda",
        text: [
          `Hi all — ahead of our ${meeting.title.toLowerCase()} on ${meeting.date}, here's the rough agenda so we can use the time well:`,
          "",
          ...((intel.talkingPoints || []).slice(0, 4).map((p, i) => `${i + 1}. ${p}`)),
          "",
          `Duration: ${meeting.duration || "30 min"}. Anything you'd add — say the word and I'll re-share.`,
        ].join("\n"),
      },
      evidence: (intel.talkingPoints || []).slice(0, 3).map((p) => ({ label: "Item", value: p })),
    }),
  });

  if (intel.pulse || (intel.talkingPoints || []).length > 0) {
    actions.push({
      id: "prep",
      label: "Save brief",
      icon: "note",
      tone: "info",
      primaryLabel: "Save brief",
      build: () => ({
        kicker: "Meeting brief",
        title: `Save your prep brief · ${meeting.title}`,
        meta: [
          { label: "WHEN",     value: meeting.date },
          { label: "DEAL",     value: `${deal.company} · ${deal.stage}` },
          { label: "DURATION", value: meeting.duration || "30 min" },
        ],
        body: intel.pulse ? { label: "Read on the deal", text: intel.pulse } : null,
        evidence: [
          ...(intel.talkingPoints || []).slice(0, 4).map((p) => ({ label: "Talking point", value: p })),
          ...(intel.risks || []).slice(0, 2).map((r) => ({ label: "Watch", value: r })),
        ],
      }),
    });
  }

  return actions.slice(0, 3);
}

function openMeetingAction(meeting, deal, action) {
  if (typeof window.previewAction !== "function") return;
  const payload = action.build();
  window.previewAction({
    ...payload,
    primaryLabel: action.primaryLabel,
    onConfirm: () => {
      if (typeof window.fireToast === "function") {
        window.fireToast(`✓ ${action.label} · ${deal.company}`);
      }
    },
  });
}

// Manager-flavoured prep actions for an agenda item. Same pattern as
// deriveMeetingActions (one primary + 1-2 secondaries) but the verbs are
// coaching/forecast moves instead of buyer-side actions.
function deriveManagerMeetingActions(item, model) {
  const intel = item.intel || {};
  const actions = [];

  if (item.kind === "forecast-review") {
    actions.push({
      id: "pull-at-risk",
      label: "Pull at-risk roster",
      icon: "flag",
      primary: true,
      primaryLabel: "Pull list",
      build: () => ({
        kicker: "Forecast prep",
        title: `Pull at-risk roster for ${item.title}`,
        meta: [
          { label: "WHEN",     value: item.date },
          { label: "DURATION", value: item.duration || "60 min" },
          { label: "WITH",     value: (item.attendees || []).join(", ") },
        ],
        body: intel.pulse ? { label: "Why this list", text: intel.pulse } : null,
        evidence: (intel.risks || []).map((r) => ({ label: "At risk", value: r })),
      }),
    });
    actions.push({
      id: "send-agenda",
      label: "Send agenda",
      icon: "mail",
      primaryLabel: "Send agenda",
      build: () => ({
        kicker: "Pre-meeting agenda",
        title: `Send agenda · ${item.title}`,
        meta: [
          { label: "WHEN",      value: item.date },
          { label: "ATTENDEES", value: (item.attendees || []).join(", ") },
        ],
        body: {
          subject: `Agenda · ${item.title}`,
          label: "Drafted agenda",
          text: [
            `Team — ahead of Friday's forecast review, here's the rough agenda:`,
            "",
            ...((intel.talkingPoints || []).map((p, i) => `${i + 1}. ${p}`)),
            "",
            "Bring deltas + at-risk decisions you want a second pair of eyes on.",
          ].join("\n"),
        },
        evidence: (intel.talkingPoints || []).slice(0, 3).map((p) => ({ label: "Item", value: p })),
      }),
    });
  } else if (item.kind === "one-on-one") {
    actions.push({
      id: "coaching-questions",
      label: "Draft coaching questions",
      icon: "users",
      primary: true,
      primaryLabel: "Save questions",
      build: () => ({
        kicker: "1:1 prep",
        title: `Coaching questions · ${item.title}`,
        meta: [
          { label: "WHEN",     value: item.date },
          { label: "REP",      value: (item.attendees || [])[0] || "AE" },
          { label: "DURATION", value: item.duration || "30 min" },
        ],
        body: intel.pulse ? { label: "Read on the rep", text: intel.pulse } : null,
        evidence: (intel.talkingPoints || []).slice(0, 4).map((q) => ({ label: "Ask", value: q })),
      }),
    });
    actions.push({
      id: "review-deals",
      label: "Pre-read deals",
      icon: "book",
      primaryLabel: "Open briefs",
      build: () => ({
        kicker: "Deal pre-read",
        title: `Pre-read deals · ${item.title}`,
        meta: [
          { label: "REP",  value: (item.attendees || [])[0] || "AE" },
          { label: "WHEN", value: item.date },
        ],
        body: { label: "Why pre-read", text: "Skim the briefs so the 1:1 spends time on next steps, not catch-up." },
        evidence: (intel.docs || []).map((d) => ({ label: "Brief", value: d })),
      }),
    });
  } else if (item.kind === "deal-review") {
    actions.push({
      id: "pull-deal-room",
      label: "Open deal room",
      icon: "target",
      primary: true,
      primaryLabel: "Open",
      build: () => ({
        kicker: "Deal review prep",
        title: `Open ${item.title}`,
        meta: [
          { label: "WHEN", value: item.date },
          { label: "WITH", value: (item.attendees || []).join(", ") },
        ],
        body: intel.pulse ? { label: "Read on the deal", text: intel.pulse } : null,
        evidence: (intel.risks || []).map((r) => ({ label: "Risk", value: r })),
      }),
    });
    actions.push({
      id: "coaching-questions",
      label: "Coaching questions",
      icon: "users",
      primaryLabel: "Save questions",
      build: () => ({
        kicker: "Deal review prep",
        title: `Coaching questions · ${item.title}`,
        meta: [
          { label: "WHEN", value: item.date },
        ],
        body: intel.pulse ? { label: "Read", text: intel.pulse } : null,
        evidence: (intel.talkingPoints || []).slice(0, 4).map((q) => ({ label: "Ask", value: q })),
      }),
    });
  }

  // Always-on: send agenda or save brief depending on type.
  if (item.kind !== "forecast-review") {
    actions.push({
      id: "brief",
      label: "Save brief",
      icon: "note",
      primaryLabel: "Save brief",
      build: () => ({
        kicker: "Briefing",
        title: `Save brief · ${item.title}`,
        meta: [
          { label: "WHEN",     value: item.date },
          { label: "DURATION", value: item.duration || "30 min" },
        ],
        body: intel.pulse ? { label: "Read", text: intel.pulse } : null,
        evidence: [
          ...(intel.talkingPoints || []).slice(0, 3).map((p) => ({ label: "Talking point", value: p })),
          ...(intel.risks || []).slice(0, 2).map((r) => ({ label: "Watch", value: r })),
        ],
      }),
    });
  }

  return actions.slice(0, 3);
}

function TodaysAgenda({ deals, items: managerItems, isManager = false, openDeal, cardClass = "", cardDragProps = {}, onRemove }) {
  const upcoming = useMemo(() => {
    if (managerItems) {
      // Manager items are already a flat list — wrap them in the same
      // { deal, meeting } shape the render loop consumes, with deal: null.
      return managerItems.slice(0, 4).map((m) => ({ deal: null, meeting: m, isManager: true }));
    }
    const all = [];
    deals.forEach((d) => {
      (d.meetings || []).forEach((m) => {
        if (m.isPast) return;
        all.push({ deal: d, meeting: m });
      });
    });
    return all.slice(0, 4);
  }, [deals, managerItems]);

  return (
    <section className={`cp-feed cp-agenda ${cardClass}`} aria-label="Today's agenda" {...cardDragProps}>
      <header className="cp-feed-head">
        <div className="cp-feed-headings">
          <div className="cp-feed-affordance">
            <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          </div>
          <h2 className="cp-feed-title">Today&rsquo;s agenda</h2>
          <p className="cp-feed-helper">What you have, who's in the room, what to prep for each.</p>
        </div>
        <SlotRemoveBtn onRemove={onRemove} />
      </header>
      {upcoming.length === 0 ? (
        <div className="cp-agenda-empty">No upcoming meetings scheduled.</div>
      ) : (
        <ul className="cp-agenda-list" role="list" aria-label="Upcoming meetings">
          {upcoming.map((row) => {
            const meeting = row.meeting;
            const isMgr = !!row.isManager;
            const deal = row.deal;
            // Manager rows don't ride a buyer meeting — tone is derived
            // from the kind (deal review / 1:1 = amber, forecast review =
            // warn when there are interventions, good otherwise).
            const tone = isMgr
              ? (meeting.kind === "forecast-review"
                  ? ((meeting.intel?.risks || []).length > 1 ? "warn" : "amber")
                  : meeting.kind === "deal-review" ? "warn" : "amber")
              : agendaToneFor(deal, meeting);
            const dealId = isMgr ? meeting.relatedDealId : (deal.sourceDealId || deal.id);
            const { dateLabel, timeLabel } = parseMeetingDate(meeting.date);
            const attendeesText = isMgr
              ? (meeting.attendees || []).join(" + ") || "Team"
              : formatAttendees(meeting, deal).display;
            const headline = isMgr ? meeting.title : deal.company;
            const meta = isMgr ? meeting.subtitle : meeting.title;
            const actions = isMgr
              ? deriveManagerMeetingActions(meeting)
              : deriveMeetingActions(meeting, deal);
            const onHeadClick = isMgr
              ? (dealId ? () => openDeal(dealId, "overview") : undefined)
              : () => openDeal(dealId, "meetings");
            const fireAction = (action) => {
              if (!isMgr) return openMeetingAction(meeting, deal, action);
              if (typeof window.previewAction !== "function") return;
              const payload = action.build();
              window.previewAction({
                ...payload,
                primaryLabel: action.primaryLabel,
                onConfirm: () => {
                  if (typeof window.fireToast === "function") {
                    window.fireToast(`✓ ${action.label} · ${meeting.title}`);
                  }
                },
              });
            };
            return (
              <li
                key={meeting.id || `${(deal && deal.id) || "mgr"}-${meeting.title}`}
                className={`agenda-card tone-${tone}`}
              >
                {/* WHEN — left rail, calendar-style date / time / duration */}
                <div className="agenda-card-when" aria-label={meeting.date}>
                  <span className="agenda-card-when-day">{dateLabel}</span>
                  {timeLabel && (
                    <span className="agenda-card-when-time">{timeLabel}</span>
                  )}
                  <span className="agenda-card-when-dur">{meeting.duration || "30 min"}</span>
                </div>

                {/* WHAT + WHO — center column */}
                <div className="agenda-card-body">
                  {onHeadClick ? (
                    <button
                      type="button"
                      className="agenda-card-head"
                      onClick={onHeadClick}
                      aria-label={isMgr
                        ? `Open ${headline}`
                        : `Open ${deal.company} deal room`}
                    >
                      <h3 className="agenda-card-co">{headline}</h3>
                      <span className="agenda-card-meeting">{meta}</span>
                    </button>
                  ) : (
                    <div className="agenda-card-head agenda-card-head-static">
                      <h3 className="agenda-card-co">{headline}</h3>
                      <span className="agenda-card-meeting">{meta}</span>
                    </div>
                  )}

                  <div className="agenda-card-who">
                    <Icon name="users" size={12} />
                    <span>
                      <em>with</em> {attendeesText}
                    </span>
                  </div>
                </div>

                {/* DO — right column, prep action chips */}
                <div className="agenda-card-actions" role="group" aria-label="Prep actions">
                  {actions.map((action, idx) => (
                    <button
                      key={action.id}
                      type="button"
                      className={`agenda-card-action ${action.primary || idx === 0 ? "is-primary" : ""} tone-${action.tone || tone}`}
                      onClick={() => fireAction(action)}
                      aria-label={`${action.label} for ${headline}`}
                    >
                      <Icon name={action.icon} size={11} />
                      <span>{action.label}</span>
                    </button>
                  ))}
                </div>
              </li>
            );
          })}
        </ul>
      )}
    </section>
  );
}

// ── Agent status banner ──────────────────────────────────────────────────
// Greeting + pipeline weather + buyer-proof verdict, fused into one fixed
// top header. The weather glyph replaces the old live-pulse indicator —
// the sun rotates and the rain bounces, so the surface is alive without a
// separate ping. State headline ("Stable pipeline" / "Drift forming" /
// "Heavy drift") tags onto the greeting; the verdict carries the numbers.
function AgentBanner({ deals }) {
  const greeting = useMemo(() => {
    const h = new Date().getHours();
    if (h < 12) return "Good morning";
    if (h < 18) return "Good afternoon";
    return "Good evening";
  }, []);

  const verdict = useMemo(() => {
    const open = deals.filter((d) => !/closed/i.test(`${d.stage || ""} ${d.statusLabel || ""}`));
    if (open.length === 0) return { kind: "empty" };
    const unverified = open.filter((d) => (window.isBuyerUnverifiedDeal ? window.isBuyerUnverifiedDeal(d) : false));
    if (unverified.length === 0) return { kind: "clean", openCount: open.length };
    const valueAtRisk = unverified.reduce((sum, d) => sum + (d.value || 0), 0);
    return {
      kind: "verdict",
      unverifiedCount: unverified.length,
      openCount: open.length,
      valueAtRisk: formatPipelineValue(valueAtRisk),
    };
  }, [deals]);

  const weather = useMemo(() => {
    const openDeals = deals.filter((d) => !isClosedPipelineDeal(d));
    const totalValue = openDeals.reduce((sum, d) => sum + d.value, 0);
    const weighted = (key) => {
      if (!openDeals.length || totalValue <= 0) return 0;
      return Math.round(openDeals.reduce((sum, d) => sum + (d[key] || 0) * d.value, 0) / totalValue);
    };
    const signedGap = weighted("buyerScore") - weighted("sellerScore");
    const gap = Math.abs(signedGap);
    const tone = pipelineTone(gap);
    if (gap < 10)  return { kind: "sunny",  headline: "Stable pipeline", tone };
    if (gap < 25)  return signedGap < 0
      ? { kind: "partly", headline: "Drift forming", tone }
      : { kind: "cloudy", headline: "Buyer running ahead", tone };
    return { kind: "rainy", headline: "Heavy drift", tone };
  }, [deals]);

  // Banner doubles as the entry point to the buyer-progression forecast —
  // since the forecast is itself a weather report on the pipeline, the
  // banner IS the affordance. Click the whole surface to open it.
  const openForecast = () => {
    if (typeof window.openBuyerForecast === "function") window.openBuyerForecast();
  };
  return (
    <section
      className={`cp-banner cp-banner--weather tone-${weather.tone} weather-${weather.kind} is-clickable`}
      role="button"
      tabIndex={0}
      onClick={openForecast}
      onKeyDown={(e) => {
        if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openForecast(); }
      }}
      aria-label="Open buyer-progression forecast"
    >
      <div className={`cp-banner-glyph forecast-glyph-${weather.kind}`} aria-hidden="true">
        <WeatherGlyph kind={weather.kind} size={48} />
      </div>
      <div className="cp-banner-text">
        <div className="cp-banner-greet">
          {greeting}, Ridha.
          <span className="cp-banner-weather-tag"> — {weather.headline}</span>
        </div>
        <div className="cp-banner-status" data-tour="verdict">
          {verdict.kind === "empty" && "No open deals to defend."}
          {verdict.kind === "clean" && (
            <>All <b>{verdict.openCount}</b> open deals carry buyer proof.</>
          )}
          {verdict.kind === "verdict" && (
            <><b>{verdict.unverifiedCount}</b> of {verdict.openCount} open deals are buyer-unverified · <b>{verdict.valueAtRisk}</b> depends on proof you don&rsquo;t have</>
          )}
        </div>
      </div>
      <span className="cp-banner-cta" aria-hidden="true">
        Open forecast <Icon name="arrow" size={12} />
      </span>
    </section>
  );
}

const isClosedPipelineDeal = (deal) => /^closed/i.test(deal.stage || "") || /^closed/i.test(deal.statusLabel || "");

function pipelineTone(gap) {
  return gap >= 25 ? "warn" : gap >= 10 ? "amber" : "good";
}

function formatPipelineValue(value) {
  if (value >= 1000000) {
    return `$${(value / 1000000).toFixed(value % 1000000 === 0 ? 0 : 1)}M`;
  }
  return `$${Math.round(value / 1000)}K`;
}

// One priority card — same coach-card recipe as the manager-side coaching +
// theatre cards, just bound to AE-side deal data. Header carries the
// company + stage + the diag headline as the severity-tinted "trigger" line;
// reality holds the diag.why impact sentence; "Your move" holds diag.fix.
// The buyer↔seller progression and signal chips tuck into a "Deal context"
// disclosure so the card stays calm until the AE wants the proof.
function CoachCtaChannel({ channel }) {
  if (!channel) return null;
  const label = (window.CHANNEL_LABEL && window.CHANNEL_LABEL[channel]) || channel;
  const icon = (window.CHANNEL_ICON && window.CHANNEL_ICON[channel]) || "mail";
  return (
    <span className="coach-cta-channel" title={`Channel: ${label}`} aria-label={`Channel: ${label}`}>
      <Icon name={icon} size={12} />
      <span>{label}</span>
    </span>
  );
}

function PriorityDealCard({ deal, openDeal, active = true, onAction }) {
  const diag = diagnosisForDeal(deal);
  const tone = Math.abs(deal.gap) >= 25 ? "warn" : Math.abs(deal.gap) >= 10 ? "amber" : "good";
  const CategoryStrip = window.DealCategoryStrip;
  const ctaChannel = useMemo(() => {
    if (!window.recommendChannel || !window.executionContextFor) return "email";
    return window.recommendChannel(window.executionContextFor(deal, "diag")).primary || "email";
  }, [deal.id]);

  const [prepOpen, setPrepOpen] = useState(true);
  const committee = diag?.committee || [];
  // When the committee is present, it replaces the "Signals" chip block —
  // same coverage, more concrete (named stakeholders + state).
  const symptoms = committee.length > 0 ? [] : (diag?.symptoms || []);
  const hasAlignment = deal.buyerScore != null && deal.sellerScore != null;
  const hasPrep = hasAlignment || !!diag?.what || committee.length > 0 || symptoms.length > 0;

  const prepBits = [];
  if (committee.length) prepBits.push(`${committee.length} stakeholder${committee.length === 1 ? "" : "s"}`);
  if (symptoms.length) prepBits.push(`${symptoms.length} signal${symptoms.length === 1 ? "" : "s"}`);

  const targetDealId = deal.sourceDealId || deal.id;
  const displayValue = deal.arr || formatManagerMoney(deal.value || deal.amount || 0);
  const open = () => openDeal(targetDealId);

  const gapAbs = Math.abs(deal.gap || 0);
  return (
    <article
      className={`coach-card tone-${tone} ${active ? "is-clickable" : ""}`}
      role={active ? "button" : undefined}
      tabIndex={active ? 0 : -1}
      onClick={active ? open : undefined}
      onKeyDown={active ? (e) => {
        if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); }
      } : undefined}
      aria-label={active ? `Open ${deal.company} deal room` : 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} · {deal.closeDate}</span>
        </div>
        <div className="coach-card-meta-r">
          {CategoryStrip && <CategoryStrip deal={deal} compact className="coach-cat-strip" />}
          <b>{displayValue}</b>
        </div>
      </header>

      {diag?.fix && (
        <button
          type="button"
          className="coach-cta"
          onClick={(e) => {
            e.stopPropagation();
            if (!active) return;
            // CTA fires the suggested action; body click is still "open deal".
            if (onAction) onAction();
            else open();
          }}
          disabled={!active}
        >
          <span className="coach-cta-text">{diag.fix}</span>
          <CoachCtaChannel channel={ctaChannel} />
          <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>
      )}

      {diag?.headline && (
        <div className={`coach-trigger-line tone-${tone}`}>
          <span className="coach-trigger-dot" aria-hidden="true" />
          <span>{diag.headline}</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>Deal context</span>
            {prepBits.length > 0 && <em>{prepBits.join(" · ")}</em>}
          </button>
          <div className="coach-prep-body">
            <div className="coach-prep-inner">
              <p className="coach-prep-reality">{diag?.why || deal.summary}</p>
              {hasAlignment && (
                <div className="coach-prep-block">
                  <span className="coach-mini-label">Buyer ↔ Seller alignment</span>
                  <BuyerSellerProgress
                    buyer={deal.buyerScore}
                    seller={deal.sellerScore}
                    gap={Math.abs(deal.gap)}
                    tone={tone}
                  />
                </div>
              )}
              {committee.length > 0 && <Committee rows={committee} />}
              {symptoms.length > 0 && (
                <div className="coach-prep-block">
                  <span className="coach-mini-label">Proof points</span>
                  <div className="coach-chips">
                    {symptoms.map((s, i) => (
                      <span key={i} className={`coach-chip tone-${s.tone || "good"}`}>{s.label}</span>
                    ))}
                  </div>
                </div>
              )}
            </div>
          </div>
        </div>
      )}

      <footer className="coach-card-foot">
        <button
          type="button"
          className="coach-btn coach-btn-icon"
          onClick={(e) => { e.stopPropagation(); open(); }}
          aria-label={`Open ${deal.company} deal room`}
          title="Open deal room"
        >
          <Icon name="arrow" size={14} />
        </button>
      </footer>
    </article>
  );
}

// ── Priority deals — stack + selected deal brief ──
// Left side keeps every eligible priority deal visible. Right side owns the
// selected deal's diagnosis, proof, and action preview CTA.
function PriorityDeal({ deal, diag, idx, total, onNext, onPrev, openDeal, queue, onJump, cardClass, cardDragProps, onRemove }) {
  const n = queue.length;
  const [swipingOut, setSwipingOut] = useState(false);

  // Tinder-like advance: the current detail swipes right, then idx moves
  // forward and the next deal mounts with the entrance animation. Used by
  // the next-arrow and the CTA confirm. Prev / jump skip the exit phase
  // and just snap to the new card (entrance animation still plays via the
  // article's key change).
  const swipeAdvance = () => {
    if (swipingOut) return;
    setSwipingOut(true);
    setTimeout(() => {
      onNext();
      setSwipingOut(false);
    }, 280);
  };

  // Empty state — verified silence. Brand principle: when nothing is
  // wrong, the surface stays calm and says so plainly.
  if (n === 0) {
    return (
      <section className={`cp-priority-shell cp-priority-shell-empty ${cardClass}`} data-tour="priority">
        <header className="cp-priority-shell-head" {...cardDragProps}>
          <div className="cp-feed-headings">
            <div className="cp-feed-affordance">
              <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
            </div>
            <h2 className="cp-feed-title">Top priority deals</h2>
            <p className="cp-feed-helper">No deals at risk right now — verified silence is the success state.</p>
          </div>
          <SlotRemoveBtn onRemove={onRemove} />
        </header>
        <div className="cp-priority-empty">
          <span className="cp-priority-empty-mark" aria-hidden>· · ·</span>
          <p className="cp-priority-empty-headline">No deals need defending.</p>
          <p className="cp-priority-empty-sub">Verified silence.</p>
        </div>
      </section>
    );
  }

  // Manager flavour — coaching move on a team deal. The manager doesn't
  // send the buyer email themselves; they coach the AE, request proof, or
  // pull the deal into a 1:1 / forecast review. Modal payload is a Slack
  // draft to the rep, not an email to the buyer.
  const managerFireAction = (q) => {
    const repName = q.aeName || "the rep";
    const firstName = repName.split(" ")[0];
    const value = q.amount || q.value || 0;
    const fmtK = (n) => n >= 1000000 ? `$${(n / 1000000).toFixed(1)}M` : `$${Math.round(n / 1000)}K`;
    const gapAbs = Math.abs(q.gap || 0);
    const firmnessRaw = Math.round(value * 0.45);
    const firmness = fmtK(firmnessRaw);
    const ask = q.suggestedManagerAction || "Ask rep for buyer-owned evidence";
    const question = (q.coachingQuestions && q.coachingQuestions[0])
      || q.managerNextAction
      || "What buyer-owned action happened since the last meeting?";
    const missing = q.evidenceMissing && q.evidenceMissing[0];
    const stageLine = `${q.company} · ${q.stage}`;
    const body = `${firstName} — quick one on ${q.company}. Wanted to check what buyer-owned proof we have on this before forecast. ${question} If we don't have it yet, let's pull it into Friday's review so we can make the call together.`;

    const commit = () => {
      if (typeof window.fireToast === "function") {
        window.fireToast(`Coaching note sent · ${repName} · ${q.company}`);
      }
      setTimeout(swipeAdvance, 500);
      setTimeout(() => {
        if (typeof window.fireToast === "function") {
          window.fireToast(`✓ ${firstName} replied · forecast firmed +${firmness}`);
        }
      }, 5200);
    };

    if (typeof window.previewAction === "function") {
      const evidence = [];
      if (q.topGap) evidence.push({ label: "Gap", value: q.topGap });
      if (missing) evidence.push({ label: "Missing", value: missing });
      if (q.crmVsNudge) evidence.push({ label: "Read", value: q.crmVsNudge });
      if (gapAbs) evidence.push({ label: "Buyer/seller gap", value: `${gapAbs} pt` });
      window.previewAction({
        kicker: `Coaching move · Slack to ${firstName}`,
        title: `${ask} · ${repName}`,
        meta: [
          { label: "TO",      value: `${repName} · AE` },
          { label: "CHANNEL", value: "Slack DM" },
          { label: "DEAL",    value: stageLine },
          { label: "AMOUNT",  value: fmtK(value) },
        ],
        body: {
          label: "Drafted Slack to the rep",
          text: body,
        },
        impact: {
          likelihood: 76,
          timeHorizon: "24h",
          firmness,
          firmnessRaw,
          gapLabel: "Probability gap",
          gapBefore: gapAbs || null,
          gapAfter: gapAbs > 0 ? Math.max(2, Math.round(gapAbs * 0.3)) : null,
        },
        evidence,
        primaryLabel: `Send to ${firstName}`,
        onConfirm: commit,
      });
    } else {
      commit();
    }
  };

  // Primary CTA — opens an action preview with the drafted message,
  // evidence, and expected impact. Confirm fires the action and a delayed
  // outcome toast that simulates the buyer's response. For manager-shape
  // deals (q.aeName set) we route through managerFireAction so the CTA
  // becomes a coaching move, not an AE-driven outbound.
  const fireAction = (q) => {
    if (q && q.aeName) return managerFireAction(q);
    const diagFor = diagnosisForDeal(q);
    const fix = diagFor && diagFor.fix;
    const draft = diagFor && diagFor.draft;
    const outcome = diagFor && diagFor.outcome;
    const gapAbs = Math.abs(q.gap || 0);
    const value = q.value || q.amount || 0;

    const sev = diagFor && diagFor.severity;
    const sevKey = sev === "warn" ? "high" : sev === "amber" ? "mid" : "good";
    const likelihood = { high: 58, mid: 70, good: 86 }[sevKey];
    const firmnessPct = { high: 0.55, mid: 0.70, good: 0.85 }[sevKey];
    const firmnessRaw = Math.round(value * firmnessPct);
    const fmtK = (n) => n >= 1000000 ? `$${(n / 1000000).toFixed(1)}M` : `$${Math.round(n / 1000)}K`;
    const firmness = fmtK(firmnessRaw);
    const horizon = draft && (draft.channel === "Slack DM" || draft.channel === "LinkedIn DM") ? "24h" : "48h";
    const gapAfter = gapAbs > 0 ? Math.max(2, Math.round(gapAbs * 0.3)) : null;

    const commit = () => {
      if (window.NudgeBackend) {
        window.NudgeBackend.execute(q, {
          kind: diagFor?.kind || "Recommended action",
          channel: draft?.channel || null,
          recipient: draft?.to || null,
          action: fix || `Action on ${q.company}`,
          subject: draft?.subject || null,
          body: draft?.text || null,
          outcome,
        });
      }
      if (typeof window.fireToast === "function") {
        window.fireToast(fix ? `Sent · ${fix}` : `Action taken on ${q.company}`);
      }
      setTimeout(swipeAdvance, 500);
      setTimeout(() => {
        if (typeof window.fireToast === "function") {
          const tail = outcome ? `${q.company} · ${outcome} · +${firmness} firmed`
                               : `${q.company} · response received · +${firmness} firmed`;
          window.fireToast(`✓ ${tail}`);
        }
      }, 5200);
    };

    if (typeof window.previewAction === "function" && draft) {
      const evidence = [];
      if (diagFor.headline) evidence.push({ label: "Trigger", value: diagFor.headline });
      if (diagFor.what) evidence.push({ label: "Signal", value: diagFor.what });
      if (gapAbs) evidence.push({ label: "Gap", value: `${gapAbs}pt buyer-seller drift` });
      if (diagFor.fixMeta) evidence.push({ label: "Source", value: diagFor.fixMeta });
      window.previewAction({
        kicker: `Preview · ${draft.channel || "Message"}`,
        title: fix || `Action on ${q.company}`,
        meta: [
          { label: "TO", value: draft.to },
          { label: "CHANNEL", value: draft.channel || "Email" },
          { label: "DEAL", value: `${q.company} · ${q.stage}` },
        ],
        body: {
          subject: draft.subject || null,
          label: draft.subject ? "Body" : "Message",
          text: draft.text,
        },
        impact: {
          likelihood,
          timeHorizon: horizon,
          firmness,
          firmnessRaw,
          gapLabel: "Buyer-seller gap",
          gapBefore: gapAbs || null,
          gapAfter,
        },
        evidence,
        primaryLabel: draft.channel === "LinkedIn DM" ? "Send DM" : "Send",
        onConfirm: commit,
      });
    } else if (typeof window.previewAction === "function") {
      window.previewAction({
        kicker: "Preview · Action",
        title: fix || `Action on ${q.company}`,
        meta: [{ label: "DEAL", value: `${q.company} · ${q.stage}` }],
        body: { label: "What will happen", text: fix || "Send the suggested next move." },
        impact: {
          likelihood,
          timeHorizon: horizon,
          firmness,
          firmnessRaw,
          gapLabel: "Buyer-seller gap",
          gapBefore: gapAbs || null,
          gapAfter,
        },
        evidence: diagFor && diagFor.headline ? [{ label: "Trigger", value: diagFor.headline }] : [],
        primaryLabel: "Confirm",
        onConfirm: commit,
      });
    } else {
      commit();
    }
  };

  // idx may be null (nothing selected yet — show the daily-read PriorityEmpty)
  // or a valid index in queue. Clamp defensively in case queue shrank.
  const activeIdx = idx == null ? null : Math.min(idx, n - 1);
  const activeQ = activeIdx == null ? null : queue[activeIdx];
  const activeDiag = activeQ ? (diagnosisForDeal(activeQ) || {}) : {};
  const activeTone = activeQ
    ? (activeQ.status === "at-risk" ? "warn"
       : activeQ.status === "needs-attention" ? "amber" : "good")
    : "good";
  const activeTargetId = activeQ ? (activeQ.sourceDealId || activeQ.id) : null;
  const CategoryStrip = window.DealCategoryStrip;
  // Manager-shape deals (with aeName) get coaching-flavoured copy. AE deals
  // keep the existing buyer-action label.
  const isManagerDeal = !!(activeQ && activeQ.aeName);
  const actionLabel = activeQ
    ? (isManagerDeal
        ? (activeQ.suggestedManagerAction || `Coach ${activeQ.aeName} on ${activeQ.company}`)
        : (activeDiag.fix || activeQ.nudges?.[0]?.action || "Review next move"))
    : "";
  const actionChannel = activeQ
    ? (isManagerDeal ? "slack" : (activeDiag.draft?.channel || "email"))
    : null;
  const activeValue = activeQ ? (activeQ.arr || formatManagerMoney(activeQ.value || activeQ.amount || 0)) : "";
  const gapAbs = activeQ ? Math.abs(activeQ.gap || 0) : 0;
  const hasAlignment = activeQ && activeQ.buyerScore != null && activeQ.sellerScore != null;

  return (
    <section
      className={`cp-priority-shell cp-priority-shell-active tone-${activeTone} ${cardClass}`}
      data-tour="priority"
    >
      <header className="cp-priority-shell-head" {...cardDragProps}>
        <div className="cp-feed-headings">
          <div className="cp-feed-affordance">
            <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          </div>
          <h2 className="cp-feed-title">Top priority deals</h2>
          <p className="cp-feed-helper">Deals that need attention right now — pick one to inspect the diagnosis and next move.</p>
        </div>
        <SlotRemoveBtn onRemove={onRemove} />
      </header>

      <div className="priority-split">
        <div className="priority-stack" role="listbox" aria-label="Priority deals">
          {queue.map((q, i) => {
            const qTone = q.status === "at-risk" ? "warn"
              : q.status === "needs-attention" ? "amber" : "good";
            const selected = i === activeIdx;
            // Riskiest = the at-risk deal with the largest buyer/seller gap.
            // Pulses its orb so the queue has a clear "look here first" anchor.
            const isRiskiest = q.status === "at-risk" && queue
              .filter((d) => d.status === "at-risk")
              .sort((a, b) => Math.abs(b.gap || 0) - Math.abs(a.gap || 0))[0]?.id === q.id;
            const value = q.arr || formatManagerMoney(q.value || q.amount || 0);
            return (
              <button
                key={q.id}
                type="button"
                role="option"
                aria-selected={selected}
                className={`priority-stack-card tone-${qTone} ${selected ? "is-selected" : ""}`}
                data-tour={i === 0 ? "priority-select" : undefined}
                onClick={() => onJump(i)}
              >
                <span className={`priority-stack-sev tone-${qTone} ${isRiskiest ? "is-pulsing" : ""}`} aria-hidden="true" />
                <span className="priority-stack-main">
                  <span className="priority-stack-name">{q.company}</span>
                  <span className="priority-stack-meta">{q.stage} · {q.closeDate}</span>
                </span>
                <span className="priority-stack-side">
                  {CategoryStrip && <CategoryStrip deal={q} compact className="priority-stack-cats" />}
                  <span className="priority-stack-value">{value}</span>
                </span>
              </button>
            );
          })}
        </div>

        {activeQ ? (
        <article
          key={activeQ.id}
          className={`priority-detail tone-${activeTone} ${swipingOut ? "is-swiping-out" : ""}`}
          data-tour="priority-detail"
        >
          <header className="priority-detail-head">
            <div className="priority-detail-title">
              <span className="priority-detail-kicker">Selected deal</span>
              <h3>{activeQ.company}</h3>
              <p>{activeQ.stage} · {activeQ.closeDate} · {activeValue}</p>
            </div>
            <div className="priority-detail-actions">
              {CategoryStrip && <CategoryStrip deal={activeQ} compact className="priority-detail-cats" />}
              <button
                type="button"
                className="priority-detail-room"
                onClick={() => openDeal(activeTargetId)}
                aria-label={`Open ${activeQ.company} deal room`}
              >
                <span>Deal room</span>
                <Icon name="expand" size={10} />
              </button>
            </div>
          </header>

          <div className="priority-detail-body">
            <section className="priority-detail-block priority-detail-diagnosis">
              <span className="coach-mini-label">Why this is priority</span>
              <h4>{activeDiag.headline || activeQ.statusLabel}</h4>
              <p>{activeDiag.why || activeQ.summary}</p>
            </section>

            {hasAlignment && (
              <section className="priority-detail-block">
                <span className="coach-mini-label">Buyer ↔ Seller alignment</span>
                <BuyerSellerProgress
                  buyer={activeQ.buyerScore}
                  seller={activeQ.sellerScore}
                  gap={gapAbs}
                  tone={gapAbs >= 25 ? "warn" : gapAbs >= 10 ? "amber" : "good"}
                />
              </section>
            )}

            {activeDiag.symptoms?.length > 0 && (
              <section className="priority-detail-block">
                <span className="coach-mini-label">Proof points</span>
                <div className="coach-chips">
                  {activeDiag.symptoms.map((s, i) => (
                    <span key={i} className={`coach-chip tone-${s.tone || "good"}`}>{s.label}</span>
                  ))}
                </div>
              </section>
            )}

            <button
              type="button"
              className="coach-cta priority-detail-cta"
              data-tour="priority-cta"
              onClick={() => fireAction(activeQ)}
            >
              <span className="coach-cta-text">{actionLabel}</span>
              <CoachCtaChannel channel={actionChannel} />
              <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>

            {(() => {
              // "+X more" affordance — additional recommended moves beyond
              // the primary CTA. In manager view the label flips to
              // "coaching moves"; both route to the deal room's Next-move
              // tab where every move is listed and fireable.
              const moreCount = Math.max(0, (activeQ.nudges || []).length - 1);
              if (moreCount === 0) return null;
              const noun = isManagerDeal
                ? (moreCount === 1 ? "coaching move" : "coaching moves")
                : (moreCount === 1 ? "more action" : "more actions");
              return (
                <button
                  type="button"
                  className="priority-detail-more-actions"
                  onClick={() => openDeal(activeTargetId, "ai")}
                  aria-label={`See ${moreCount} ${noun} for ${activeQ.company}`}
                >
                  <span>+{moreCount} {noun}</span>
                  <em>· in Next move</em>
                  <Icon name="arrow" size={11} />
                </button>
              );
            })()}
          </div>

        </article>
        ) : (
          <PriorityEmpty queue={queue} onJump={onJump} />
        )}
      </div>
    </section>
  );
}

// PriorityEmpty — daily-read panel rendered on the right side of the Top
// Priority widget when no deal is selected yet. Surfaces the shape of the
// day (count, $ at stake, worst gap) and a single recommended starting
// deal so the rep doesn't land on a blank canvas.
function PriorityEmpty({ queue, onJump }) {
  const n = queue.length;
  const atRiskDeals = useMemo(() => queue.filter((q) => q.status === "at-risk"), [queue]);
  const totalAtStake = useMemo(
    () => queue.reduce((s, q) => s + (q.value || q.amount || 0), 0),
    [queue]
  );
  const largestGap = useMemo(
    () => queue.reduce((max, q) => Math.max(max, Math.abs(q.gap || 0)), 0),
    [queue]
  );
  // Recommendation: at-risk first, then biggest absolute gap × value.
  const recommended = useMemo(() => {
    if (!queue.length) return null;
    return [...queue].sort((a, b) => {
      if (a.status !== b.status) {
        if (a.status === "at-risk") return -1;
        if (b.status === "at-risk") return 1;
        if (a.status === "needs-attention") return -1;
        if (b.status === "needs-attention") return 1;
      }
      const aImpact = (Math.abs(a.gap || 0) + 1) * (a.value || a.amount || 0);
      const bImpact = (Math.abs(b.gap || 0) + 1) * (b.value || b.amount || 0);
      return bImpact - aImpact;
    })[0];
  }, [queue]);
  const recIdx = recommended ? queue.findIndex((q) => q.id === recommended.id) : 0;
  const recDiag = recommended ? diagnosisForDeal(recommended) : null;
  const recValue = recommended
    ? (recommended.arr || formatManagerMoney(recommended.value || recommended.amount || 0))
    : "";
  const recTone = recommended
    ? (recommended.status === "at-risk" ? "warn"
       : recommended.status === "needs-attention" ? "amber" : "good")
    : "good";

  return (
    <article className={`priority-detail priority-detail-empty tone-${recTone}`} aria-label="Today's read">
      <header className="priority-detail-head">
        <div className="priority-detail-title">
          <span className="priority-detail-kicker">Today's read · {n} pending</span>
          <h3>
            {atRiskDeals.length > 0
              ? `${atRiskDeals.length} ${atRiskDeals.length === 1 ? "deal needs" : "deals need"} defending right now`
              : `${n} ${n === 1 ? "deal" : "deals"} on your radar`}
          </h3>
          <p>{formatManagerMoney(totalAtStake)} total at stake · widest buyer/seller gap {largestGap} pt</p>
        </div>
      </header>

      <div className="priority-detail-body">
        <section className="priority-detail-block priority-empty-stats">
          <span className="coach-mini-label">Shape of the day</span>
          <div className="priority-empty-grid">
            <div className="priority-empty-stat tone-warn">
              <b>{atRiskDeals.length}</b>
              <em>at-risk</em>
            </div>
            <div className="priority-empty-stat">
              <b>{formatManagerMoney(totalAtStake)}</b>
              <em>at stake</em>
            </div>
            <div className="priority-empty-stat">
              <b>{largestGap}<small>pt</small></b>
              <em>worst gap</em>
            </div>
          </div>
        </section>

        {recommended && (() => {
          const recIsManager = !!recommended.aeName;
          const recLabel = recIsManager
            ? `Coach ${recommended.aeName.split(" ")[0]} on ${recommended.company}`
            : `Inspect ${recommended.company}`;
          const recHeadline = recIsManager
            ? (recommended.topGap || (recDiag && recDiag.headline) || recommended.nudgeReality || `${recommended.stateLabel || recommended.statusLabel} · ${recommended.stage}`)
            : ((recDiag && recDiag.headline) || recommended.summary || `${recommended.statusLabel} · ${recommended.stage}`);
          return (
            <section className="priority-detail-block priority-empty-rec">
              <span className="coach-mini-label">
                {recIsManager ? "Start here · biggest coaching lift" : "Start here · biggest at-stake"}
              </span>
              <h4>{recommended.company}{recIsManager ? <em style={{ fontStyle: "normal", color: "var(--text-3)", fontWeight: 500, fontSize: "13px", marginLeft: 8 }}> · {recommended.aeName}</em> : null}</h4>
              <p>{recHeadline}</p>
              <button
                type="button"
                className="coach-cta priority-detail-cta"
                onClick={() => onJump(recIdx)}
              >
                <span className="coach-cta-text">{recLabel}</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>
            </section>
          );
        })()}

        <p className="priority-empty-hint">
          <Icon name="arrowL" size={11} /> Or pick any deal from the list to inspect.
        </p>
      </div>
    </article>
  );
}

// Buyer ↔ Seller progression — tug-of-war design. The track has a fixed
// centre line; Buyer pushes left, Seller pushes right. Each side's fill
// extends from the centre outward by score/100 of its half, so whoever's
// "pulling the deal" visually dominates the track. The leading side is
// tinted with the state tone (warn / amber / good); the trailing side
// stays muted. A caption below carries the numeric gap + direction.
function BuyerSellerProgress({ buyer, seller, gap, tone }) {
  const sellerAhead = seller > buyer;
  const buyerAhead = buyer > seller;
  const stateLabel = tone === "warn" ? "Drift" : tone === "amber" ? "Watch" : "Aligned";
  const direction = sellerAhead
    ? "seller pulling ahead"
    : buyerAhead
      ? "buyer pulling ahead"
      : "even pull";
  const buyerWidth = Math.max(0, Math.min(100, buyer)) / 2;
  const sellerWidth = Math.max(0, Math.min(100, seller)) / 2;
  return (
    <div
      className={`cp-bsp tone-${tone}`}
      aria-label={`Buyer ${buyer}%, Seller ${seller}% — ${gap} point gap (${stateLabel}, ${direction})`}
    >
      <div className="cp-bsp-bar">
        <span className={`cp-bsp-num cp-bsp-num-buyer ${buyerAhead ? "is-leading" : ""}`}>
          <em>Buyer</em>
          {buyer}%
        </span>
        <div className="cp-bsp-track" aria-hidden="true">
          <span
            className={`cp-bsp-fill cp-bsp-fill-buyer ${buyerAhead ? "is-leading" : "is-trailing"}`}
            style={{ width: `${buyerWidth}%` }}
          />
          <span
            className={`cp-bsp-fill cp-bsp-fill-seller ${sellerAhead ? "is-leading" : "is-trailing"}`}
            style={{ width: `${sellerWidth}%` }}
          />
          <span className="cp-bsp-centre" />
        </div>
        <span className={`cp-bsp-num cp-bsp-num-seller ${sellerAhead ? "is-leading" : ""}`}>
          {seller}%
          <em>Seller</em>
        </span>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────
// Slot widget extras: section picker + 3 new feed-style widgets
// ─────────────────────────────────────────────────────────────────────────

// Modal picker with the section catalogue. Closes on Esc or backdrop click.
function SectionPicker({ 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">
          {SECTION_TYPES.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>
  );
}

// Tiny remove button placed in widget eyebrows (shows on hover).
function SlotRemoveBtn({ onRemove }) {
  if (!onRemove) return null;
  return (
    <button
      type="button"
      className="cp-slot-remove"
      onClick={(e) => { e.stopPropagation(); onRemove(); }}
      aria-label="Remove section"
      title="Remove section"
    >
      <Icon name="close" size={11} />
    </button>
  );
}

// ── Today's nudges — top buyer-proof gaps across pipeline ─────────────
function TodaysNudges({ openDeal, cardClass = "", cardDragProps = {}, onRemove }) {
  const items = useMemo(() => {
    const feed = (typeof SEED_NUDGES_FEED !== "undefined" ? SEED_NUDGES_FEED : window.SEED_NUDGES_FEED) || [];
    const score = (n) => (n.severity === "warn" ? 3 : n.severity === "amber" ? 2 : 1);
    return [...feed].sort((a, b) => score(b) - score(a)).slice(0, 6);
  }, []);
  return (
    <section className={`cp-feed ${cardClass}`} aria-label="Today's nudges" {...cardDragProps}>
      <header className="cp-feed-head">
        <div className="cp-feed-headings">
          <div className="cp-feed-affordance">
            <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          </div>
          <h2 className="cp-feed-title">What needs you today</h2>
          <p className="cp-feed-helper">Ranked by deal risk and decay. Click a row to open the deal.</p>
        </div>
        <SlotRemoveBtn onRemove={onRemove} />
      </header>
      <ul className="cp-feed-list">
        {items.map((n) => (
          <li key={n.id} className={`cp-feed-row tone-${n.severity}`}>
            <button type="button" className="cp-feed-row-btn" onClick={() => openDeal(n.dealId)}>
              <span className="cp-feed-dot" />
              <span className="cp-feed-text">
                <b>{n.deal}</b>
                <em>{n.title}</em>
              </span>
              <span className="cp-feed-attribution" onClick={(e) => e.stopPropagation()}>
                <AgentChip nudge={n} compact />
              </span>
              <span className="cp-feed-time">{n.time}</span>
            </button>
          </li>
        ))}
      </ul>
    </section>
  );
}

// ── Buyer signals — recent buyer-side activity across deals ────────────
function BuyerSignals({ deals, openDeal, cardClass = "", cardDragProps = {}, onRemove }) {
  const items = useMemo(() => {
    const out = [];
    for (const d of deals) {
      if (/closed/i.test(`${d.stage || ""} ${d.statusLabel || ""}`)) continue;
      for (const a of d.activity || []) {
        if (a.tag === "buyer" || a.tag === "milestone") {
          out.push({ ...a, dealId: d.id, company: d.company });
        }
      }
    }
    return out.slice(0, 6);
  }, [deals]);
  if (items.length === 0) {
    return (
      <section className={`cp-feed ${cardClass}`} aria-label="Buyer signals" {...cardDragProps}>
        <header className="cp-feed-head">
          <div className="cp-feed-headings">
            <div className="cp-feed-affordance">
              <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
            </div>
            <h2 className="cp-feed-title">Buyer-side activity</h2>
            <p className="cp-feed-helper">Replies, meetings, milestones your buyers made — proof the deal is progressing on their side, not just yours.</p>
          </div>
          <SlotRemoveBtn onRemove={onRemove} />
        </header>
        <div className="cp-feed-empty">No recent buyer-side movement.</div>
      </section>
    );
  }
  return (
    <section className={`cp-feed ${cardClass}`} aria-label="Buyer signals" {...cardDragProps}>
      <header className="cp-feed-head">
        <div className="cp-feed-headings">
          <div className="cp-feed-affordance">
            <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          </div>
          <h2 className="cp-feed-title">Buyer-side activity</h2>
          <p className="cp-feed-helper">Replies, meetings, milestones your buyers made — proof the deal is progressing on their side, not just yours.</p>
        </div>
        <SlotRemoveBtn onRemove={onRemove} />
      </header>
      <ul className="cp-feed-list">
        {items.map((a, i) => (
          <li key={`${a.dealId}-${i}`} className="cp-feed-row tone-good">
            <button type="button" className="cp-feed-row-btn" onClick={() => openDeal(a.dealId)}>
              <span className="cp-feed-dot" />
              <span className="cp-feed-text">
                <b>{a.company}</b>
                <em>{a.title}</em>
              </span>
              <span className="cp-feed-time">{a.time}</span>
            </button>
          </li>
        ))}
      </ul>
    </section>
  );
}

// ── Coaching queue — biggest stage-truth gaps (seller ahead of buyer) ──
function CoachingQueue({ deals, openDeal, cardClass = "", cardDragProps = {}, onRemove }) {
  const items = useMemo(() => {
    return deals
      .filter((d) => !/closed/i.test(`${d.stage || ""} ${d.statusLabel || ""}`))
      .map((d) => ({ ...d, sellerLead: (d.sellerScore || 0) - (d.buyerScore || 0) }))
      .filter((d) => d.sellerLead >= 15)
      .sort((a, b) => b.sellerLead - a.sellerLead)
      .slice(0, 4);
  }, [deals]);
  if (items.length === 0) {
    return (
      <section className={`cp-feed ${cardClass}`} aria-label="Coaching queue" {...cardDragProps}>
        <header className="cp-feed-head">
          <div className="cp-feed-headings">
            <div className="cp-feed-affordance">
              <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
            </div>
            <h2 className="cp-feed-title">Deals where your story is ahead of theirs</h2>
            <p className="cp-feed-helper">Seller activity is racing ahead of buyer signals — exactly where coaching pays off.</p>
          </div>
          <SlotRemoveBtn onRemove={onRemove} />
        </header>
        <div className="cp-feed-empty">No stage-truth exceptions right now.</div>
      </section>
    );
  }
  return (
    <section className={`cp-feed ${cardClass}`} aria-label="Coaching queue" {...cardDragProps}>
      <header className="cp-feed-head">
        <div className="cp-feed-headings">
          <div className="cp-feed-affordance">
            <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          </div>
          <h2 className="cp-feed-title">Deals where your story is ahead of theirs</h2>
          <p className="cp-feed-helper">Seller activity is racing ahead of buyer signals — exactly where coaching pays off.</p>
        </div>
        <SlotRemoveBtn onRemove={onRemove} />
      </header>
      <ul className="cp-feed-list">
        {items.map((d) => (
          <li key={d.id} className="cp-feed-row tone-warn">
            <button type="button" className="cp-feed-row-btn" onClick={() => openDeal(d.id)}>
              <span className="cp-feed-lead">+{d.sellerLead} pt</span>
              <span className="cp-feed-text">
                <b>{d.company}</b>
                <em>seller-active, buyer trailing</em>
              </span>
              <span className="cp-feed-time">{d.stage}</span>
            </button>
          </li>
        ))}
      </ul>
    </section>
  );
}

// ── Addy's day — ranked list of today's recommended actions ───────────
// Picks deals where Addy's intervention has highest expected value
// (close proximity × forecast risk × seller-lead size × dollar weight)
// and renders a short imperative line per row.
function AddysDay({ deals, openDeal, cardClass = "", cardDragProps = {}, onRemove }) {
  const items = useMemo(() => {
    const now = new Date();
    const parseClose = (s) => {
      const d = new Date(s);
      return isNaN(d.getTime()) ? null : d;
    };
    const daysTo = (d) => d ? Math.round((d - now) / 86400000) : 999;
    const open = deals.filter((d) => !/closed/i.test(`${d.stage || ""} ${d.statusLabel || ""}`));
    const scored = open.map((d) => {
      const close = parseClose(d.closeDate);
      const dt = daysTo(close);
      const sellerLead = Math.max(0, (d.sellerScore || 0) - (d.buyerScore || 0));
      const valueK = Math.log10(Math.max(d.value || d.amount || 10000, 10000));
      const proximity = dt <= 7 ? 3 : dt <= 21 ? 2 : dt <= 45 ? 1 : 0.4;
      const riskBoost = d.forecastRisk ? 1.5 : 1;
      const score = (sellerLead * 0.6 + valueK * 6) * proximity * riskBoost;
      const action = (() => {
        if (d.forecastRisk && sellerLead >= 25) return "Lock buyer-owned next step";
        if (d.forecastRisk && dt <= 14)         return "Pull finance forward";
        if (sellerLead >= 20)                   return "Get buyer to own a date";
        if (Math.abs(d.gap || 0) >= 10)         return "Reconfirm buyer commitment";
        return "Confirm next buyer action";
      })();
      const tone = d.forecastRisk ? "warn" : sellerLead >= 15 ? "amber" : "good";
      return { id: d.id, company: d.company, stage: d.stage, action, tone, score, dt };
    });
    return scored.sort((a, b) => b.score - a.score).slice(0, 4);
  }, [deals]);

  if (items.length === 0) {
    return (
      <section className={`cp-feed ${cardClass}`} aria-label="Addy's day" {...cardDragProps}>
        <header className="cp-feed-head">
          <div className="cp-feed-headings">
            <div className="cp-feed-affordance">
              <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
            </div>
            <h2 className="cp-feed-title">Addy&rsquo;s plan for today</h2>
            <p className="cp-feed-helper">What Addy will work on if you don&rsquo;t intervene — ranked by value × proximity to close.</p>
          </div>
          <SlotRemoveBtn onRemove={onRemove} />
        </header>
        <div className="cp-feed-empty">Nothing to act on today.</div>
      </section>
    );
  }
  return (
    <section className={`cp-feed ${cardClass}`} aria-label="Addy's day" {...cardDragProps}>
      <header className="cp-feed-head">
        <div className="cp-feed-headings">
          <div className="cp-feed-affordance">
            <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          </div>
          <h2 className="cp-feed-title">Addy&rsquo;s plan for today</h2>
          <p className="cp-feed-helper">What Addy will work on if you don&rsquo;t intervene — ranked by value × proximity to close.</p>
        </div>
        <SlotRemoveBtn onRemove={onRemove} />
      </header>
      <ul className="cp-feed-list">
        {items.map((it, i) => (
          <li key={it.id} className={`cp-feed-row tone-${it.tone}`}>
            <button type="button" className="cp-feed-row-btn" onClick={() => openDeal(it.id)}>
              <span className="cp-feed-rank">{String(i + 1).padStart(2, "0")}</span>
              <span className="cp-feed-text">
                <b>{it.action}</b>
                <em>{it.company} · {it.stage}</em>
              </span>
              <span className="cp-feed-time">
                {it.dt <= 0 ? "overdue" : it.dt <= 7 ? `${it.dt}d` : it.dt <= 30 ? `${it.dt}d` : "later"}
              </span>
            </button>
          </li>
        ))}
      </ul>
    </section>
  );
}

// ── This week's runway — 7-day forward view ───────────────────────────
// Groups deals closing in the next 7 days into verified / at-risk and
// summarises dollar weight per bucket. Short list under the stats so
// the user can jump straight into the deals driving the number.
function WeeklyRunway({ deals, openDeal, cardClass = "", cardDragProps = {}, onRemove }) {
  const data = useMemo(() => {
    const now = new Date();
    const weekEnd = new Date(now.getTime() + 7 * 86400000);
    const parseClose = (s) => {
      const d = new Date(s);
      return isNaN(d.getTime()) ? null : d;
    };
    const open = deals.filter((d) => !/closed/i.test(`${d.stage || ""} ${d.statusLabel || ""}`));
    const inWindow = open
      .map((d) => ({ d, close: parseClose(d.closeDate) }))
      .filter((x) => x.close && x.close >= new Date(now.getTime() - 86400000) && x.close <= weekEnd)
      .map((x) => x.d);
    const sum = (xs) => xs.reduce((a, d) => a + (d.value || d.amount || 0), 0);
    const verified = inWindow.filter((d) => !d.forecastRisk && Math.abs(d.gap || 0) < 15);
    const atRisk   = inWindow.filter((d) => d.forecastRisk || Math.abs(d.gap || 0) >= 15);
    const fmt = (n) => {
      if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
      if (n >= 1_000)     return `$${Math.round(n / 1_000)}K`;
      return `$${n}`;
    };
    return {
      count: inWindow.length,
      verifiedCount: verified.length,
      riskCount: atRisk.length,
      verifiedSum: fmt(sum(verified)),
      riskSum: fmt(sum(atRisk)),
      list: inWindow
        .slice()
        .sort((a, b) => (parseClose(a.closeDate) - parseClose(b.closeDate)))
        .slice(0, 4)
        .map((d) => {
          const sellerLead = Math.max(0, (d.sellerScore || 0) - (d.buyerScore || 0));
          const dt = Math.round((parseClose(d.closeDate) - now) / 86400000);
          const action = (() => {
            if (d.forecastRisk && sellerLead >= 25) return "Lock buyer-owned next step";
            if (d.forecastRisk && dt <= 3)          return "Escalate to economic buyer";
            if (d.forecastRisk)                     return "Pull finance forward";
            if (sellerLead >= 20)                   return "Get buyer to own a date";
            if (Math.abs(d.gap || 0) >= 10)         return "Reconfirm buyer commitment";
            return "Protect the next buyer step";
          })();
          return {
            id: d.id,
            company: d.company,
            stage: d.stage,
            tone: d.forecastRisk ? "warn" : Math.abs(d.gap || 0) >= 15 ? "amber" : "good",
            value: fmt(d.value || d.amount || 0),
            close: d.closeDate,
            dt,
            action,
          };
        }),
    };
  }, [deals]);

  return (
    <section className={`cp-feed cp-runway ${cardClass}`} aria-label="This week's runway" {...cardDragProps}>
      <header className="cp-feed-head">
        <div className="cp-feed-headings">
          <div className="cp-feed-affordance">
            <span className="cp-drag-handle" aria-hidden><Icon name="grip" size={12} /></span>
          </div>
          <h2 className="cp-feed-title">Closing in the next 7 days</h2>
          <p className="cp-feed-helper">Verified close vs at-risk dollars for the week, plus the deals driving each bucket.</p>
        </div>
        <SlotRemoveBtn onRemove={onRemove} />
      </header>
      <div className="cp-runway-stats">
        <div className="cp-runway-stat tone-good">
          <em>Verified close</em>
          <b>{data.verifiedSum}</b>
          <span>{data.verifiedCount} deal{data.verifiedCount === 1 ? "" : "s"}</span>
        </div>
        <div className="cp-runway-stat tone-warn">
          <em>At risk</em>
          <b>{data.riskSum}</b>
          <span>{data.riskCount} deal{data.riskCount === 1 ? "" : "s"}</span>
        </div>
        <div className="cp-runway-stat">
          <em>In window</em>
          <b>{data.count}</b>
          <span>next 7 days</span>
        </div>
      </div>
      {data.list.length > 0 ? (
        <ul className="cp-feed-list">
          {data.list.map((d) => (
            <li key={d.id} className={`cp-feed-row tone-${d.tone}`}>
              <button type="button" className="cp-feed-row-btn" onClick={() => openDeal(d.id)}>
                <span className="cp-feed-dot" />
                <span className="cp-feed-text">
                  <b>{d.action}</b>
                  <em>{d.company} · {d.value} · due {d.close.split(",")[0]}</em>
                </span>
                <span className="cp-feed-time">
                  {d.dt <= 0 ? "today" : d.dt === 1 ? "tomorrow" : `${d.dt}d`}
                </span>
              </button>
            </li>
          ))}
        </ul>
      ) : (
        <div className="cp-feed-empty">Nothing closing in the next 7 days.</div>
      )}
    </section>
  );
}

window.HomeScreen = HomeScreen;
window.BuyerSellerProgress = BuyerSellerProgress;
