/* NutriDMS, Meal Programs: shared pieces + program detail
   MpValidationPanel · MpScheduleGrid · MpWorkflowTimeline ·
   MpAssignmentBoard · MealProgramDetail · MpDistributeDrawer
   ════════════════════════════════════════════════════════════════════════ */
const { useState: dUseState, useEffect: dUseEffect, useMemo: dUseMemo } = React;

/* ─────────── Collapsible section (dropdown panel) ─────────── */
function MpSection({ icon, title, count, defaultOpen = true, children }) {
  const [open, setOpen] = dUseState(defaultOpen);
  return (
    <div className={`mp-collapse ${open ? "open" : ""}`}>
      <button className="mp-section-h mp-section-toggle" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
        <Icon name={icon} size={18} className="ic" />
        <h3>{title}</h3>
        {count != null && <span className="count">{count}</span>}
        <Icon name="chevron-down" size={18} className="mp-section-caret" />
      </button>
      {open && <div className="mp-section-body">{children}</div>}
    </div>
  );
}

/* ─────────── Per-recipe org-compliance check (Loraa health rules) ───────────
   When ingSwaps are present, the recipe's nutrition is recomputed and its
   ingredient list is rewritten (original → swapped-in Library ingredient) so
   nutrient, allergen, and ingredient-block rules all re-evaluate against the
   swapped plan, a swap can newly raise or clear a flag. */
function mpRecipeComplianceFlags(recipeId, opts) {
  const r = mpRecipeById(recipeId);
  if (!r || typeof complianceCheck !== "function" || typeof compRecipeItem !== "function") return [];
  try {
    const o = window.mpNormOpts ? window.mpNormOpts(opts) : { ingSwaps: {}, ingPortions: {}, servings: 1 };
    let item = compRecipeItem(r);
    const hasAdj = Object.keys(o.ingSwaps).length || Object.keys(o.ingPortions).length || o.servings !== 1;
    if (hasAdj) {
      const n = mpRecipeNutr(recipeId, o);
      const swapName = window.mpSwapName || ((s) => (typeof s === "string" ? s : (s && s.name) || ""));
      item = {
        ...item,
        nutr: { ...item.nutr, calories: n.calories, protein: n.protein, carbs: n.carbs, fat: n.fat, fiber: n.fiber, sodium: n.sodium },
        ingredients: (r.ingredients || []).map((x) => {
          const txt = typeof x === "string" ? x : (x.name || "");
          const sw = o.ingSwaps[txt];
          return { name: sw ? swapName(sw) : txt };
        }),
      };
    }
    return complianceCheck(item, "recipe") || [];
  } catch (e) { return []; }
}

/* ─────────── Loraa transparency: data sources + swap reasoning ───────────
   Every Loraa suggestion shows WHY it was made and WHERE the numbers come
   from, so reviewers can trust (and verify) the guidance. */
const MP_DATA_SOURCE = {
  nutrition: { label: "Recipe nutrition", detail: "Per-serving values from this recipe's published record in the NutriDMS Library (USDA FoodData Central-aligned)." },
  rules: { label: "Org Health Rules", detail: "Nutrient limits & targets from your organization's active Health Rules and this program's restrictions." },
  approval: { label: "Approval status", detail: "Live publishing-workflow state, only Approved or Published recipes can ship in a program." },
};
const MP_SOURCE_FOR = {
  Sodium: ["nutrition", "rules"], "Added Sugar": ["nutrition", "rules"], Fiber: ["nutrition", "rules"],
  Approval: ["approval"], Duplicate: ["nutrition"], Rotation: ["nutrition"],
};

/* Best approved, org-clean swap candidates for a flag (shared by popup + schedule). */
function mpTopSwaps(flagType, origId, count) {
  const pool = (window.RECIPES || []).filter((r) => (r.status === "approved" || r.status === "published") && r.id !== origId);
  const scored = pool.map((r) => {
    const n = mpRecipeNutr(r.id);
    const clean = mpRecipeComplianceFlags(r.id).length === 0;
    let metric = 0;
    if (flagType === "Sodium") metric = n.sodium;
    else if (flagType === "Added Sugar") metric = n.addedSugar;
    else if (flagType === "Fiber") metric = -n.fiber;
    else metric = n.sodium;
    return { r, n, clean, metric };
  }).filter((x) => x.clean);
  scored.sort((a, b) => a.metric - b.metric);
  return scored.slice(0, count || 3);
}

/* Plain-language reason a specific candidate is a good swap vs. the original. */
function mpSwapReason(origNutr, candNutr, flagType) {
  if (!origNutr) return "Approved, org-clean alternative.";
  const dNa = origNutr.sodium - candNutr.sodium;
  const dSug = origNutr.addedSugar - candNutr.addedSugar;
  const dFib = candNutr.fiber - origNutr.fiber;
  const dKcal = origNutr.calories - candNutr.calories;
  const lead = flagType === "Sodium" && dNa > 0 ? `Cuts sodium by ${dNa} mg`
    : flagType === "Added Sugar" && dSug > 0 ? `Cuts added sugar by ${dSug} g`
    : flagType === "Fiber" && dFib > 0 ? `Adds ${dFib} g of fiber`
    : dNa > 0 ? `Lower sodium (−${dNa} mg)` : dFib > 0 ? `Higher fiber (+${dFib} g)` : "Comparable nutrition";
  const extras = [];
  if (flagType !== "Fiber" && dFib > 0) extras.push(`+${dFib} g fiber`);
  if (flagType !== "Sodium" && dNa > 0) extras.push(`−${dNa} mg sodium`);
  if (dKcal > 0) extras.push(`−${dKcal} kcal`);
  const tail = extras.length ? `, also ${extras.slice(0, 2).join(" and ")}` : "";
  return `${lead}${tail}, and it clears every org health rule.`;
}

/* Source-attribution chips, used under Loraa suggestions for transparency. */
function MpSourceNote({ flagType }) {
  const keys = MP_SOURCE_FOR[flagType] || ["nutrition", "rules"];
  return (
    <div className="mp-src">
      <span className="mp-src-h"><Icon name="book-open" size={11} /> Loraa checked</span>
      <div className="mp-src-chips">
        {keys.map((k) => { const s = MP_DATA_SOURCE[k]; return <span key={k} className="mp-src-chip" title={s.detail}><Icon name="circle-check" size={11} /> {s.label}</span>; })}
      </div>
    </div>
  );
}

/* Selected-recipe card, thumbnail, nutrition values, ingredients, org-rule badge. */
function MpSelectedRecipeCard({ recipeId, onRemove }) {
  const r = mpRecipeById(recipeId);
  const n = mpRecipeNutr(recipeId);
  const flags = dUseMemo(() => mpRecipeComplianceFlags(recipeId), [recipeId]);
  const blocking = flags.filter((f) => ["Critical", "Severe"].includes(f.severity)).length;
  const clean = flags.length === 0;
  if (!r) return null;
  const approved = r.status === "approved" || r.status === "published";
  const ings = r.ingredients || [];
  return (
    <div className="mp-selrec">
      <div className="mp-selrec-thumb" style={{ backgroundImage: `url("${r.cover}")` }}>
        <span className={`mp-recipe-st ${approved ? "approved" : "other"}`}>{(r.status || "draft").replace("-", " ")}</span>
      </div>
      <div className="mp-selrec-body">
        <div className="mp-selrec-top">
          <div className="mp-selrec-nm">{r.name}</div>
          {onRemove && <button className="icon-btn sm" title="Remove" onClick={onRemove}><Icon name="x" size={14} /></button>}
        </div>
        <div className="mp-selrec-nutr">
          <span className="mp-nutr-chip kcal"><b>{n.calories}</b> kcal</span>
          <span className="mp-nutr-chip"><b>{n.protein}g</b> protein</span>
          <span className="mp-nutr-chip"><b>{n.carbs}g</b> carbs</span>
          <span className="mp-nutr-chip"><b>{n.fat}g</b> fat</span>
          <span className="mp-nutr-chip"><b>{n.fiber}g</b> fiber</span>
          <span className="mp-nutr-chip"><b>{n.sodium}mg</b> sodium</span>
          <span className="mp-nutr-chip"><b>{n.addedSugar}g</b> sugar</span>
        </div>
        <div className="mp-selrec-ings">
          <div className="mp-selrec-ings-h"><Icon name="carrot" size={12} /> Ingredients</div>
          <ul>{ings.map((ing, i) => <li key={i}>{typeof ing === "string" ? ing : (ing.name || "")}</li>)}</ul>
        </div>
        <div className={`mp-selrec-comp ${clean ? "ok" : blocking ? "err" : "warn"}`}>
          <Icon name={clean ? "shield-check" : blocking ? "alert-triangle" : "alert-circle"} size={13} />
          {clean ? "Passes org health rules" : `${flags.length} health-rule ${flags.length === 1 ? "flag" : "flags"}`}
        </div>
        {!clean && (
          <ul className="mp-selrec-flags">
            {flags.slice(0, 3).map((f, i) => <li key={i}><span className={`mp-sev-dot ${["Critical", "Severe"].includes(f.severity) ? "err" : "warn"}`}></span>{f.message}</li>)}
          </ul>
        )}
      </div>
    </div>
  );
}

/* ─────────── Loraa "why flagged + how to fix" popup (rolling hard popup) ─────────── */
function MpFixPopup({ prog, flag, onClose, onApplyFix }) {
  dUseEffect(() => { const k = (e) => e.key === "Escape" && onClose(); document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, [onClose]);
  const sevClass = flag.severity;
  // The recipe currently in the flagged slot (live, used when applying a swap)
  const cell = (prog.days || []).find((d) => d.day === flag.day && d.meal === flag.meal);
  // Freeze the ORIGINAL recipe at open time so the "before" state survives the swap
  const [origId] = dUseState(() => (cell ? cell.recipeId : null));
  const origRecipe = origId ? mpRecipeById(origId) : null;
  const origNutr = origId ? mpRecipeNutr(origId) : null;
  const [applied, setApplied] = dUseState(null);
  const doApply = (r, n) => { setApplied({ recipe: r, nutr: n }); onApplyFix && onApplyFix(r, cell); };

  // Benefit breakdown for a candidate vs. the original recipe
  const primaryKey = flag.type === "Added Sugar" ? "sugar" : flag.type === "Fiber" ? "fiber" : "sodium";
  const benefitsFor = (n) => {
    if (!origNutr) return [];
    const rows = [
      { key: "sodium", good: origNutr.sodium - n.sodium > 0, d: origNutr.sodium - n.sodium, txt: `${origNutr.sodium - n.sodium >= 0 ? "−" : "+"}${Math.abs(origNutr.sodium - n.sodium)} mg sodium` },
      { key: "sugar", good: origNutr.addedSugar - n.addedSugar > 0, d: origNutr.addedSugar - n.addedSugar, txt: `${origNutr.addedSugar - n.addedSugar >= 0 ? "−" : "+"}${Math.abs(origNutr.addedSugar - n.addedSugar)} g sugar` },
      { key: "fiber", good: n.fiber - origNutr.fiber > 0, d: n.fiber - origNutr.fiber, txt: `${n.fiber - origNutr.fiber >= 0 ? "+" : "−"}${Math.abs(n.fiber - origNutr.fiber)} g fiber` },
      { key: "kcal", good: origNutr.calories - n.calories > 0, d: origNutr.calories - n.calories, txt: `${origNutr.calories - n.calories >= 0 ? "−" : "+"}${Math.abs(origNutr.calories - n.calories)} kcal` },
    ].filter((x) => x.d !== 0);
    rows.sort((a, b) => (a.key === primaryKey ? -1 : b.key === primaryKey ? 1 : 0));
    return rows;
  };

  // "Why", plain-language reason behind the flag
  const why = {
    Sodium: "Each meal contributes sodium toward the daily limit set in this program's restrictions. The meals on this day add up above that ceiling, which matters for the population this plan targets.",
    "Added Sugar": "Added sugar across the day's meals exceeds the program's daily limit. High added sugar works against the program's glycemic and weight goals.",
    Fiber: "The day's meals fall short of the fiber target. Fiber supports satiety and glycemic control for this population.",
    Approval: "Only approved or published recipes can ship in a program. This recipe is still in the review pipeline, so it can't be distributed yet.",
    Duplicate: "The same recipe repeats in this meal slot on multiple days. Repetition hurts adherence over a multi-week program.",
    Rotation: "There are too few unique recipes across the plan. More variety keeps participants engaged for the full duration.",
  }[flag.type] || "This item doesn't meet one of the program's targets or rules.";

  // Suggested swaps, approved recipes that best address the flag (prefer org-clean, fall back to best-ranked)
  const suggestions = dUseMemo(() => {
    const pool = (window.RECIPES || []).filter((r) => (r.status === "approved" || r.status === "published") && r.id !== origId);
    const scored = pool.map((r) => {
      const n = mpRecipeNutr(r.id);
      const clean = mpRecipeComplianceFlags(r.id).length === 0;
      let metric = 0;
      if (flag.type === "Sodium") metric = n.sodium;
      else if (flag.type === "Added Sugar") metric = n.addedSugar;
      else if (flag.type === "Fiber") metric = -n.fiber;
      else metric = n.sodium;
      return { r, n, clean, metric };
    });
    // For a fiber flag, only meaningful candidates actually add fiber vs. the original
    const relevant = flag.type === "Fiber" && origNutr ? scored.filter((x) => x.n.fiber > origNutr.fiber) : scored;
    const base = relevant.length ? relevant : scored;
    const clean = base.filter((x) => x.clean);
    const list = clean.length ? clean : base;   // prefer org-clean, but never come back empty
    list.sort((a, b) => a.metric - b.metric);
    return list.slice(0, 3);
  }, [flag.type, origId]);

  const stepFix = {
    Sodium: ["Open the day in the schedule and identify the highest-sodium meal.", "Swap it for one of the lower-sodium approved recipes below.", "Re-run Loraa, the day's sodium total drops under the limit."],
    "Added Sugar": ["Find the meal contributing the most added sugar.", "Replace it with a lower-sugar approved recipe below.", "Re-validate to confirm the day is under the sugar limit."],
    Fiber: ["Add or swap in a high-fiber recipe (legumes, whole veg).", "Pick one of the fiber-rich approved options below.", "Re-run Loraa to confirm the fiber target is met."],
    Approval: ["Replace this recipe with an already-approved one below, or", "Fast-track the current recipe's review from the Library.", "Once approved, the blocking flag clears automatically."],
    Duplicate: ["Choose the day where the repeat is least needed.", "Swap in a different approved recipe for variety.", "Re-validate to clear the monotony flag."],
    Rotation: ["Introduce more distinct recipes across the weeks.", "Use the suggestions below to diversify the menu.", "Re-run Loraa to lift the rotation warning."],
  }[flag.type] || ["Adjust the flagged meal.", "Pick a compliant alternative.", "Re-run Loraa validation."];

  return ReactDOM.createPortal(
    <div className="qad-bg" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="mp-fix" role="dialog" aria-label="Fix this flag">
        <div className={`mp-fix-head ${sevClass}`}>
          <MpLoraaMark size={26} />
          <div style={{ minWidth: 0, flex: 1 }}>
            <div className="mp-fix-kicker"><span className={`mp-sev-pill ${sevClass}`}>{sevClass}</span> <span className="mp-fix-type">{flag.type}</span>{flag.day && <span className="mp-flag-loc">Day {flag.day}{flag.meal ? ` · ${flag.meal}` : ""}</span>}</div>
            <h3 className="mp-fix-title">{flag.issue}</h3>
          </div>
          <button className="icon-btn" onClick={onClose}><Icon name="x" size={18} /></button>
        </div>

        <div className="mp-fix-body">
          <section className="mp-fix-sec">
            <div className="mp-fix-sec-h"><Icon name="help-circle" size={15} /> Why Loraa flagged this</div>
            <p className="mp-fix-why">{why}</p>
            <MpSourceNote flagType={flag.type} />
            {origRecipe && origNutr && (
              <div className={`mp-fix-current ${applied ? "swapped" : ""}`}>
                <div className="mp-fix-cur-side">
                  <div className="mp-fix-current-thumb" style={{ backgroundImage: `url("${origRecipe.cover}")` }} />
                  <div style={{ minWidth: 0 }}>
                    <div className={`mp-fix-current-nm ${applied ? "struck" : ""}`}>{applied ? <s>{origRecipe.name}</s> : <>Currently: {origRecipe.name}</>}</div>
                    <div className="mp-cell-meta"><span>{origNutr.calories} kcal</span><span>{origNutr.sodium}mg Na</span><span>{origNutr.addedSugar}g sugar</span><span>{origNutr.fiber}g fib</span></div>
                  </div>
                </div>
                {applied && (
                  <>
                    <Icon name="arrow-right" size={16} className="mp-fix-swaparrow" />
                    <div className="mp-fix-cur-side">
                      <div className="mp-fix-current-thumb" style={{ backgroundImage: `url("${applied.recipe.cover}")` }} />
                      <div style={{ minWidth: 0 }}>
                        <div className="mp-fix-current-nm ok"><Icon name="check" size={12} stroke={3} /> {applied.recipe.name}</div>
                        <div className="mp-cell-meta"><span>{applied.nutr.calories} kcal</span><span>{applied.nutr.sodium}mg Na</span><span>{applied.nutr.addedSugar}g sugar</span><span>{applied.nutr.fiber}g fib</span></div>
                      </div>
                    </div>
                  </>
                )}
              </div>
            )}
          </section>

          <section className="mp-fix-sec">
            <div className="mp-fix-sec-h"><Icon name="list-checks" size={15} /> How to fix it</div>
            <ol className="mp-fix-steps">{stepFix.map((s, i) => <li key={i}>{s}</li>)}</ol>
            <div className="mp-fix-rec"><Icon name="lightbulb" size={13} className="ic" /> <span>{flag.recommendation}</span></div>
          </section>

          {suggestions.length > 0 && (flag.type === "Sodium" || flag.type === "Added Sugar" || flag.type === "Fiber" || flag.type === "Approval" || flag.type === "Duplicate" || flag.type === "Rotation") && (
            <section className="mp-fix-sec">
              <div className="mp-fix-sec-h"><Icon name="replace" size={15} /> Suggested approved swaps</div>
              <div className="mp-fix-swaps">
                {suggestions.map(({ r, n, clean }) => {
                  const isApplied = applied && applied.recipe.id === r.id;
                  const ings = r.ingredients || [];
                  const shown = ings.slice(0, 6);
                  const benefits = benefitsFor(n);
                  return (
                    <div key={r.id} className={`mp-fix-swap ${isApplied ? "applied" : ""}`}>
                      <div className="mp-fix-swap-head">
                        <div className="mp-fix-swap-thumb" style={{ backgroundImage: `url("${r.cover}")` }} />
                        <div className="mp-fix-swap-main">
                          <div className="mp-fix-swap-nm">{r.name} {clean ? <span className="mp-fix-swap-ok"><Icon name="shield-check" size={11} /> org-clean</span> : <span className="mp-fix-swap-ok" style={{ background: "var(--amber-50,#fef7ec)", color: "#B45309" }}><Icon name="alert-triangle" size={11} /> review</span>}</div>
                          <div className="mp-cell-meta"><span>{n.calories} kcal</span><span>{n.sodium}mg Na</span><span>{n.addedSugar}g sugar</span><span>{n.fiber}g fib</span></div>
                        </div>
                        {onApplyFix && (
                          isApplied
                            ? <span className="mp-fix-swap-applied"><Icon name="check" size={13} stroke={3} /> Applied</span>
                            : <button className="btn primary sm mp-fix-swap-apply" onClick={() => doApply(r, n)}><Icon name="replace" size={13} /> Apply</button>
                        )}
                      </div>
                      {benefits.length > 0 && (
                        <div className="mp-fix-benefits">
                          {benefits.map((b, i) => (
                            <span key={i} className={`mp-fix-benefit ${b.good ? "good" : "bad"} ${b.key === primaryKey ? "primary" : ""}`}>
                              <Icon name={b.good ? "trending-down" : "trending-up"} size={11} /> {b.txt}
                            </span>
                          ))}
                        </div>
                      )}
                      <div className="mp-fix-reason"><MpLoraaMark size={15} /> <span><strong>Why Loraa suggests this:</strong> {mpSwapReason(origNutr, n, flag.type)}</span></div>
                      <div className="mp-fix-ings">
                        <div className="mp-fix-ings-h"><Icon name="carrot" size={12} /> Ingredients</div>
                        <ul className="mp-fix-ings-list">
                          {shown.map((ing, i) => <li key={i}>{ing}</li>)}
                          {ings.length > shown.length && <li className="more">+{ings.length - shown.length} more</li>}
                        </ul>
                      </div>
                    </div>
                  );
                })}
              </div>
            </section>
          )}
        </div>

        <div className="mp-fix-foot">
          <span className="mp-fix-foot-note">{applied ? <><Icon name="check-circle" size={13} /> Swapped in {applied.recipe.name}, re-validating the plan.</> : <><Icon name="info" size={12} /> Loraa is a supportive guide, you make the final call.</>}</span>
          <button className="btn primary" onClick={onClose}><Icon name="check" size={14} /> {applied ? "Done" : "Got it"}</button>
        </div>
      </div>
    </div>, document.body);
}

/* ─────────── Loraa validation panel (shared: builder + detail) ─────────── */
function MpValidationPanel({ prog, onFix, onSwap }) {
  const flags = dUseMemo(() => mpValidate(prog), [JSON.stringify(prog)]);
  const [fixFlag, setFixFlag] = dUseState(null);
  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
  flags.forEach((f) => counts[f.severity]++);
  const clean = flags.length === 0;
  return (
    <div className="mp-val">
      <div className="mp-val-head">
        <MpLoraaMark size={26} />
        <div style={{ minWidth: 0 }}>
          <div className="mp-val-title">Loraa Validation</div>
          <div className="mp-val-sub">Checked against {prog.conditions && prog.conditions.length ? prog.conditions.join(", ") + " · " : ""}{(prog.restrictions || []).length} restriction{(prog.restrictions || []).length === 1 ? "" : "s"} + your compliance rules</div>
        </div>
        <div className="mp-val-score">
          {clean ? <span className="mp-flagdot ok"><Icon name="shield-check" size={13} /> Program compliant</span>
            : counts.critical ? <span className="mp-flagdot err"><Icon name="alert-triangle" size={13} /> {counts.critical} blocking</span>
              : <span className="mp-flagdot warn"><Icon name="alert-circle" size={13} /> {flags.length} to review</span>}
        </div>
      </div>
      {!clean && (
        <div className="mp-val-summary">
          {["critical", "high", "medium", "low"].filter((s) => counts[s]).map((s) => (
            <span key={s} className={`mp-sev-pill ${s}`}>{counts[s]} {s}</span>
          ))}
        </div>
      )}
      {clean ? (
        <div className="mp-val-clean">
          <Icon name="shield-check" size={34} />
          <p style={{ margin: "10px 0 0", fontWeight: 700 }}>No issues found, every meal meets the program's targets.</p>
        </div>
      ) : (
        <div className="mp-val-list">
          {flags.map((f, i) => (
            <div key={i} className={`mp-flag ${f.severity}`}>
              <div className="mp-flag-top">
                <span className={`mp-sev-pill ${f.severity}`} style={{ fontSize: 10.5, padding: "2px 8px" }}>{f.severity}</span>
                <span className="mp-flag-type">{f.type}</span>
                {f.day && <span className="mp-flag-loc">Day {f.day}{f.meal ? ` · ${f.meal}` : ""}</span>}
              </div>
              <div className="mp-flag-issue">{f.issue}</div>
              <div className="mp-flag-rec"><Icon name="lightbulb" size={13} className="ic" /> <span>{f.recommendation}</span></div>
              <button className="btn secondary sm mp-flag-fix" style={{ marginTop: 10 }} onClick={() => setFixFlag(f)}><Icon name="wand-2" size={13} /> Why &amp; how to fix</button>
            </div>
          ))}
        </div>
      )}
      {fixFlag && <MpFixPopup prog={prog} flag={fixFlag} onClose={() => setFixFlag(null)} onApplyFix={onSwap ? (r, cell) => { onSwap(fixFlag, r, cell); } : null} />}
      <div style={{ padding: "10px 18px 14px", fontSize: 11.5, color: "#8478B0", display: "flex", gap: 6, alignItems: "center" }}>
        <Icon name="info" size={12} /> Loraa is a supportive guide, a qualified reviewer completes the final approval.
      </div>
    </div>
  );
}

/* ─────────── Schedule, per-day accordion (narrow, single column) ─────────── */
const MP_MEAL_ORDER = ["Breakfast", "Lunch", "Dinner", "Snack"];
function MpScheduleGrid({ prog }) {
  const flags = dUseMemo(() => mpValidate(prog), [JSON.stringify(prog)]);
  const flagged = new Set(flags.filter((f) => f.day && f.meal).map((f) => `${f.day}|${f.meal}`));
  const flagBySlot = {};
  flags.filter((f) => f.day && f.meal).forEach((f) => { const k = `${f.day}|${f.meal}`; if (!flagBySlot[k]) flagBySlot[k] = f; });
  const [fixFlag, setFixFlag] = dUseState(null);
  const dayNums = Array.from(new Set((prog.days || []).map((d) => d.day))).sort((a, b) => a - b);
  const [open, setOpen] = dUseState(() => (dayNums.length ? [dayNums[0]] : []));
  const toggle = (dn) => setOpen((o) => o.includes(dn) ? o.filter((x) => x !== dn) : [...o, dn]);
  const allOpen = open.length === dayNums.length;
  return (
    <div className="mp-sched-acc">
      <div className="mp-sched-acc-bar">
        <span className="mp-sched-acc-count">{dayNums.length} day{dayNums.length === 1 ? "" : "s"}</span>
        <button className="mp-sched-expand" onClick={() => setOpen(allOpen ? [] : dayNums.slice())}>
          <Icon name={allOpen ? "chevrons-down-up" : "chevrons-up-down"} size={13} /> {allOpen ? "Collapse all" : "Expand all"}
        </button>
      </div>
      {dayNums.map((dn) => {
        const meals = (prog.days || []).filter((d) => d.day === dn)
          .sort((a, b) => MP_MEAL_ORDER.indexOf(a.meal) - MP_MEAL_ORDER.indexOf(b.meal));
        const dayFlags = meals.filter((m) => flagged.has(`${dn}|${m.meal}`)).length;
        const kcal = meals.reduce((s, m) => s + (mpRecipeNutr(m.recipeId).calories || 0), 0);
        const isOpen = open.includes(dn);
        return (
          <div key={dn} className={`mp-day ${isOpen ? "open" : ""}`}>
            <button className="mp-day-head" onClick={() => toggle(dn)} aria-expanded={isOpen}>
              <Icon name="chevron-right" size={15} className="mp-day-caret" />
              <span className="mp-day-label">Day {dn}</span>
              <span className="mp-day-sum">{meals.length} meals · {kcal} kcal</span>
              {dayFlags > 0
                ? <span className="mp-flagdot err sm"><Icon name="alert-triangle" size={11} /> {dayFlags}</span>
                : <span className="mp-flagdot ok sm"><Icon name="check" size={11} /></span>}
            </button>
            {isOpen && (
              <div className="mp-day-body">
                {meals.map((m) => {
                  const n = mpRecipeNutr(m.recipeId);
                  const slotKey = `${dn}|${m.meal}`;
                  const isFlag = flagged.has(slotKey);
                  const mFlag = flagBySlot[slotKey];
                  const swap = isFlag && mFlag ? mpTopSwaps(mFlag.type, m.recipeId, 1)[0] : null;
                  return (
                    <div key={m.meal} className={`mp-meal-row ${isFlag ? "flagged" : ""}`}>
                      <span className="mp-meal-slot">{m.meal}</span>
                      <div className="mp-meal-main">
                        <div className={`mp-cell-nm ${swap ? "struck" : ""}`}>{swap ? <s>{n.name}</s> : n.name}</div>
                        <div className="mp-cell-meta"><span>{n.calories} kcal</span><span>{n.sodium}mg Na</span><span>{n.fiber}g fib</span></div>
                        {swap && (
                          <button className="mp-meal-swap" onClick={() => setFixFlag(mFlag)} title="See why Loraa suggests this">
                            <Icon name="arrow-right" size={13} className="mp-meal-swap-arrow" />
                            <div className="mp-meal-swap-thumb" style={{ backgroundImage: `url("${swap.r.cover}")` }} />
                            <div className="mp-meal-swap-body">
                              <div className="mp-meal-swap-nm"><MpLoraaMark size={13} /> {swap.r.name} <span className="mp-meal-swap-tag"><Icon name="shield-check" size={10} /> org-clean</span></div>
                              <div className="mp-meal-swap-why">{mpSwapReason(n, swap.n, mFlag.type)}</div>
                            </div>
                          </button>
                        )}
                      </div>
                      {isFlag && <button className="mp-cell-flag btn-reset" onClick={() => setFixFlag(mFlag)}><Icon name="alert-triangle" size={11} /> needs attention</button>}
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        );
      })}
      {fixFlag && <MpFixPopup prog={prog} flag={fixFlag} onClose={() => setFixFlag(null)} onApplyFix={null} />}
    </div>
  );
}

/* ═══════════════════════ Meal Planner (enterprise weekly board + Loraa flags) ═══════════════════════ */
/* Rich "selected recipe" card for the planner grid, photo, badge, nutrition
   pills, ingredients, and a rolling-pill health-rule flag banner (click to
   open the full Loraa fix popup). */
/* Ingredient list with per-row portion stepper + swap → Library picker. */
function MpIngList({ cell, ings, canEdit, onSwapIng, onPortion }) {
  const [picking, setPicking] = dUseState(null);   // original ingredient text being replaced
  const [menuFor, setMenuFor] = dUseState(null);   // ingredient row whose control popover is open
  const swaps = cell.ingSwaps || {};
  const portions = cell.ingPortions || {};
  const list = ings.slice(0, 8);
  const parse = window.mpParseQty || (() => ({ qty: null, unit: "", rest: "", step: 1 }));
  const unitLabel = (u) => (u ? (u.length <= 2 ? u : " " + u) : "");
  dUseEffect(() => {
    if (menuFor === null) return;
    const close = () => setMenuFor(null);
    document.addEventListener("click", close);
    return () => document.removeEventListener("click", close);
  }, [menuFor]);
  return (
    <>
      <ul className="mp-selrec-ings-list uniform">
        {list.map((ing, i) => {
          const txt = typeof ing === "string" ? ing : (ing.name || "");
          const sw = swaps[txt];
          const swappedName = window.mpSwapName ? window.mpSwapName(sw) : (typeof sw === "string" ? sw : (sw && sw.name) || "");
          const mult = portions[txt] != null ? portions[txt] : 1;
          const p = parse(txt);
          const canPortion = canEdit && p.qty != null;
          const curQty = p.qty != null ? +(p.qty * mult).toFixed(p.unit && p.unit.toLowerCase() === "ml" ? 0 : 2) : null;
          const setQty = (q) => { if (q <= 0 || !p.qty) return; onPortion && onPortion(cell, txt, +(q / p.qty).toFixed(4)); };
          const edited = sw || mult !== 1;
          const open = menuFor === txt;
          return (
            <li key={i} className={`mp-ingrow ${sw ? "swapped" : ""} ${mult !== 1 ? "reportioned" : ""} ${edited ? "edited" : ""}`}>
              <span className="mp-ingrow-txt" title={sw ? `${txt} → ${swappedName}` : txt}>
                {canPortion && <span className="mp-ingrow-qty">{curQty}{unitLabel(p.unit)}</span>}
                {sw
                  ? (<><s>{p.qty != null ? p.rest : txt}</s> <Icon name="arrow-right" size={11} className="mp-ing-arrow" /> <b>{swappedName}</b></>)
                  : (canPortion ? p.rest : txt)}
              </span>
              {canEdit && (
                <span className="mp-ingrow-cell">
                  <button className={`mp-ingrow-edit ${open ? "on" : ""} ${edited ? "edited" : ""}`} title="Portion & swap"
                    onClick={(e) => { e.stopPropagation(); setMenuFor(open ? null : txt); }}>
                    <Icon name="sliders-horizontal" size={13} />
                  </button>
                  {open && (
                    <div className="mp-ingpop" onClick={(e) => e.stopPropagation()}>
                      <div className="mp-ingpop-nm">{sw ? swappedName : (canPortion ? p.rest : txt)}</div>
                      {canPortion && (
                        <div className="mp-ingpop-row">
                          <span className="mp-ingpop-lbl">Portion</span>
                          <span className="mp-stepper sm">
                            <button onClick={() => setQty(+(curQty - p.step).toFixed(2))} disabled={curQty - p.step <= 0} aria-label="Less"><Icon name="minus" size={11} stroke={2.8} /></button>
                            <span className="mp-stepper-val">{curQty}{unitLabel(p.unit) || "×"}</span>
                            <button onClick={() => setQty(+(curQty + p.step).toFixed(2))} aria-label="More"><Icon name="plus" size={11} stroke={2.8} /></button>
                          </span>
                        </div>
                      )}
                      <button className="mp-ingpop-swap" onClick={() => { setMenuFor(null); setPicking(txt); }}><Icon name="replace" size={13} /> Swap from Library</button>
                      {edited && <button className="mp-ingpop-reset" onClick={() => { if (sw) onSwapIng(cell, txt, null); if (mult !== 1) onPortion && onPortion(cell, txt, 1); setMenuFor(null); }}><Icon name="rotate-ccw" size={12} /> Reset to original</button>}
                    </div>
                  )}
                </span>
              )}
            </li>
          );
        })}
        {ings.length > 8 && <li className="mp-ingrow more">+{ings.length - 8} more</li>}
      </ul>
      {picking !== null && (
        <MpIngSwapDrawer
          cell={cell} orig={picking}
          onClose={() => setPicking(null)}
          onPick={(lib) => { onSwapIng(cell, picking, { id: lib.id, name: lib.name }); setPicking(null); }}
        />
      )}
    </>
  );
}

function MpPlannerCard({ cell, cellKey, flag, date, defaultOpen, canEdit, dragKey, onSwapMeal, onSwapIng, onPortion, onServings, onDragStart, onDragEndCell, onDropCell }) {
  const r = mpRecipeById(cell.recipeId);
  const opts = window.mpCellOpts ? window.mpCellOpts(cell) : { ingSwaps: cell.ingSwaps, ingPortions: cell.ingPortions, servings: cell.servings || 1 };
  const n = mpRecipeNutr(cell.recipeId, opts);
  const baseN = mpRecipeNutr(cell.recipeId);
  const kcalDelta = n.calories - baseN.calories;
  const servings = cell.servings || 1;
  const compFlags = dUseMemo(() => mpRecipeComplianceFlags(cell.recipeId, opts), [cell.recipeId, JSON.stringify(opts)]);
  const [open, setOpen] = dUseState(!!defaultOpen);
  const [over, setOver] = dUseState(false);
  if (!r) return null;
  const approved = r.status === "approved" || r.status === "published";
  const ings = r.ingredients || [];
  const hasProgFlag = !!flag;
  const blocking = compFlags.filter((f) => ["Critical", "Severe"].includes(f.severity)).length;
  const totalFlags = compFlags.length + (hasProgFlag ? 1 : 0);
  const tone = totalFlags === 0 ? "ok" : (blocking ? "err" : "warn");
  const swap = hasProgFlag ? mpTopSwaps(flag.type, cell.recipeId, 1)[0] : null;
  const dragging = dragKey === cellKey;
  return (
    <div
      className={`mp-plancard ${hasProgFlag ? `flag ${flag.severity}` : ""} ${open ? "open" : ""} ${blocking ? "blocking" : ""} ${dragging ? "dragging" : ""} ${over ? "dropover" : ""}`}
      draggable={!!canEdit}
      onDragStart={canEdit ? (e) => { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", cellKey); onDragStart && onDragStart(); } : undefined}
      onDragEnd={canEdit ? () => { setOver(false); onDragEndCell && onDragEndCell(); } : undefined}
      onDragOver={canEdit ? (e) => { e.preventDefault(); if (!over) setOver(true); } : undefined}
      onDragLeave={canEdit ? () => setOver(false) : undefined}
      onDrop={canEdit ? (e) => { e.preventDefault(); setOver(false); const src = e.dataTransfer.getData("text/plain") || dragKey; onDropCell && onDropCell(src, cellKey); } : undefined}
    >
      {blocking > 0 && <span className="mp-plancard-block" title="Has a critical flag, blocks distribution"><Icon name="lock" size={11} /> Blocking</span>}
      <div className="mp-plancard-thumb" style={{ backgroundImage: `url("${r.cover}")` }}>
        {canEdit && <span className="mp-plancard-grip" title="Drag to move"><Icon name="grip-vertical" size={14} /></span>}
        <span className={`mp-recipe-st ${approved ? "approved" : "other"}`}>{(r.status || "draft").replace("-", " ")}</span>
        <span className="mp-plancard-slot">{cell.meal} · Day {cell.day}</span>
      </div>
      <button className="mp-plancard-head" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
        <div className="mp-plancard-headmain">
          <div className="mp-plancard-date"><Icon name="calendar" size={12} /> {date}</div>
          <div className="mp-plancard-nm">{r.name}</div>
          <div className="mp-plancard-summary">
            <span className="mp-plancard-kcal">{n.calories} kcal{kcalDelta !== 0 && <span className={`mp-plancard-kdelta ${kcalDelta > 0 ? "up" : "down"}`}>{kcalDelta > 0 ? "+" : ""}{kcalDelta}</span>}</span>
            {totalFlags === 0
              ? <span className="mp-plancard-flagchip ok"><Icon name="shield-check" size={11} /> Clean</span>
              : <span className={`mp-plancard-flagchip ${tone}`}><Icon name={blocking ? "alert-triangle" : "alert-circle"} size={11} /> {totalFlags} {totalFlags === 1 ? "flag" : "flags"}</span>}
          </div>
        </div>
        <Icon name="chevron-down" size={18} className="mp-plancard-caret" />
      </button>
      {canEdit && onSwapMeal && (
        <button className="mp-plancard-swapmeal" onClick={onSwapMeal} title="Replace this meal with another approved recipe">
          <Icon name="replace" size={13} /> Swap meal
        </button>
      )}
      {open && (
        <div className="mp-plancard-detail-in">
          {canEdit && (
            <div className="mp-servings">
              <span className="mp-servings-lbl"><Icon name="users" size={13} /> Servings</span>
              <div className="mp-stepper">
                <button onClick={() => onServings && onServings(cell, Math.max(0.5, +(servings - 0.5).toFixed(1)))} disabled={servings <= 0.5} aria-label="Fewer servings"><Icon name="minus" size={13} stroke={2.6} /></button>
                <span className="mp-stepper-val">{servings}</span>
                <button onClick={() => onServings && onServings(cell, +(servings + 0.5).toFixed(1))} aria-label="More servings"><Icon name="plus" size={13} stroke={2.6} /></button>
              </div>
              {servings !== 1 && <button className="mp-servings-reset" onClick={() => onServings && onServings(cell, 1)} title="Reset to 1 serving"><Icon name="rotate-ccw" size={12} /></button>}
              <span className="mp-servings-note">nutrition shown is per plate × servings</span>
            </div>
          )}
          <div className="mp-selrec-nutr">
            <span className="mp-nutr-chip kcal"><b>{n.calories}</b> kcal</span>
            <span className="mp-nutr-chip"><b>{n.protein}g</b> protein</span>
            <span className="mp-nutr-chip"><b>{n.carbs}g</b> carbs</span>
            <span className="mp-nutr-chip"><b>{n.fat}g</b> fat</span>
            <span className="mp-nutr-chip"><b>{n.fiber}g</b> fiber</span>
            <span className="mp-nutr-chip"><b>{n.sodium}mg</b> sodium</span>
            <span className="mp-nutr-chip"><b>{n.addedSugar}g</b> sugar</span>
          </div>
          <div className="mp-selrec-ings">
            <div className="mp-selrec-ings-h"><Icon name="carrot" size={12} /> Ingredients {canEdit && <span className="mp-selrec-ings-hint">adjust portion · hover to swap</span>}</div>
            <MpIngList cell={cell} ings={ings} canEdit={canEdit} onSwapIng={onSwapIng} onPortion={onPortion} />
          </div>
          {totalFlags === 0 ? (
            <div className="mp-selrec-comp ok"><Icon name="shield-check" size={13} /> Passes org health rules</div>
          ) : (
            <>
              <button
                className={`mp-plancard-flagpill ${tone} ${hasProgFlag ? "actionable" : ""}`}
                onClick={() => hasProgFlag && window.__mpResolve && window.__mpResolve(flag)}
                disabled={!hasProgFlag}
                title={hasProgFlag ? "Open Loraa, why & how to fix" : "Health-rule flags on this recipe"}
              >
                <Icon name={blocking ? "alert-triangle" : "alert-circle"} size={14} />
                <span>{totalFlags} health-rule {totalFlags === 1 ? "flag" : "flags"}</span>
                {hasProgFlag && <span className="mp-plancard-resolve">Resolve <Icon name="arrow-right" size={12} /></span>}
              </button>
              <ul className="mp-selrec-flags">
                {hasProgFlag && <li><span className="mp-sev-dot warn"></span>{flag.issue}</li>}
                {compFlags.slice(0, hasProgFlag ? 2 : 3).map((f, i) => <li key={i}><span className={`mp-sev-dot ${["Critical", "Severe"].includes(f.severity) ? "err" : "warn"}`}></span>{f.message}</li>)}
              </ul>
              {swap && (
                <button className="mp-plancard-swap" onClick={() => hasProgFlag && window.__mpResolve && window.__mpResolve(flag)} title="See Loraa's suggested swap">
                  <MpLoraaMark size={14} />
                  <span className="mp-plancard-swap-tx"><strong>Loraa suggests:</strong> {swap.r.name}</span>
                  <Icon name="chevron-right" size={14} />
                </button>
              )}
            </>
          )}
        </div>
      )}
    </div>
  );
}

/* ─────────── Ingredient-swap drawer (right slide-in Library picker) ───────────
   Lists approved/published Library ingredients with per-serving nutrition, and
   previews the net kcal change + any new health-rule flag the swap would cause. */
function MpIngSwapDrawer({ cell, orig, onClose, onPick }) {
  const [q, setQ] = dUseState("");
  dUseEffect(() => { const k = (e) => e.key === "Escape" && onClose(); document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, [onClose]);
  const pool = dUseMemo(() => (window.mpIngredientPool ? window.mpIngredientPool() : []), []);
  const before = window.mpOrigContribution ? window.mpOrigContribution(orig) : { calories: 0 };
  const ql = q.trim().toLowerCase();
  const list = ql ? pool.filter((i) => i.name.toLowerCase().includes(ql) || (i.category || "").toLowerCase().includes(ql)) : pool;
  // preview compliance impact for a candidate
  const previewFlags = (lib) => {
    const trial = { ...(cell.ingSwaps || {}), [orig]: { id: lib.id, name: lib.name } };
    const after = mpRecipeComplianceFlags(cell.recipeId, trial);
    const now = mpRecipeComplianceFlags(cell.recipeId, cell.ingSwaps);
    return { after: after.length, delta: after.length - now.length };
  };
  return ReactDOM.createPortal(
    <div className="mp-drawer-bg" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="mp-drawer mp-swapmeal" role="dialog" aria-label="Swap ingredient">
        <div className="mp-drawer-head">
          <div style={{ minWidth: 0 }}>
            <span className="pill" style={{ background: "var(--green-50)", color: "var(--green-700)", border: "1px solid var(--green-200)", marginBottom: 8 }}><Icon name="replace" size={11} stroke={2.4} /> Swap ingredient</span>
            <h3 style={{ margin: 0, fontFamily: "var(--serif)", fontSize: 19 }}>Replace “{orig}”</h3>
            <p style={{ margin: "4px 0 0", color: "var(--gray-600)", fontSize: 13 }}>Pick an approved Library ingredient, its nutrition is applied and the plan is re-checked against your Health Rules.</p>
          </div>
          <button className="icon-btn" onClick={onClose}><Icon name="x" size={18} /></button>
        </div>
        <div className="mp-drawer-search">
          <Icon name="search" size={15} />
          <input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search published ingredients…" />
        </div>
        <div className="mp-drawer-body">
          {list.length === 0 && <div className="mp-drawer-empty">No approved ingredients match “{q}”.</div>}
          <div className="mp-swap-pick">
            {list.map((i) => {
              const dK = Math.round((i.nutr.calories || 0) - (before.calories || 0));
              const pf = previewFlags(i);
              return (
                <button key={i.id} className="mp-swap-opt mp-ingswap-opt" onClick={() => onPick(i)}>
                  <div className="mp-ingswap-cat">{(i.category || "").slice(0, 2).toUpperCase()}</div>
                  <div className="mp-swap-opt-main">
                    <div className="mp-swap-opt-nm">{i.name}</div>
                    <div className="mp-cell-meta">
                      <span>{i.nutr.calories} kcal</span><span>{i.nutr.p}g P</span><span>{i.nutr.c}g C</span><span>{i.nutr.f}g F</span>
                    </div>
                  </div>
                  <div className="mp-ingswap-impact">
                    <span className={`mp-ingswap-kdelta ${dK > 0 ? "up" : dK < 0 ? "down" : ""}`}>{dK > 0 ? "+" : ""}{dK} kcal</span>
                    {pf.delta > 0
                      ? <span className="mp-ingswap-flag warn"><Icon name="alert-circle" size={11} /> +{pf.delta} flag{pf.delta > 1 ? "s" : ""}</span>
                      : pf.delta < 0
                      ? <span className="mp-ingswap-flag ok"><Icon name="shield-check" size={11} /> clears {-pf.delta}</span>
                      : pf.after > 0
                      ? <span className="mp-ingswap-flag warn"><Icon name="alert-circle" size={11} /> {pf.after} flag{pf.after > 1 ? "s" : ""}</span>
                      : <span className="mp-ingswap-flag ok"><Icon name="shield-check" size={11} /> clean</span>}
                  </div>
                </button>
              );
            })}
          </div>
        </div>
      </div>
    </div>, document.body);
}

/* ─────────── Swap-meal drawer (right slide-in approved-recipe picker) ─────────── */
function MpSwapMealDrawer({ cell, onClose, onPick }) {
  const [q, setQ] = dUseState("");
  dUseEffect(() => { const k = (e) => e.key === "Escape" && onClose(); document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, [onClose]);
  const pool = dUseMemo(() => (window.RECIPES || []).filter((r) => (r.status === "approved" || r.status === "published") && r.id !== cell.recipeId), [cell.recipeId]);
  const adding = !cell.recipeId;
  const ql = q.trim().toLowerCase();
  const list = ql ? pool.filter((r) => r.name.toLowerCase().includes(ql)) : pool;
  return ReactDOM.createPortal(
    <div className="mp-drawer-bg" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="mp-drawer mp-swapmeal" role="dialog" aria-label={adding ? "Add meal" : "Swap meal"}>
        <div className="mp-drawer-head">
          <div style={{ minWidth: 0 }}>
            <span className="pill" style={{ background: "var(--green-50)", color: "var(--green-700)", border: "1px solid var(--green-200)", marginBottom: 8 }}><Icon name={adding ? "plus" : "replace"} size={11} stroke={2.4} /> {adding ? "Add meal" : "Swap meal"}</span>
            <h3 style={{ margin: 0, fontFamily: "var(--serif)", fontSize: 20 }}>{cell.meal} · Day {cell.day}</h3>
            <p style={{ margin: "4px 0 0", color: "var(--gray-600)", fontSize: 13 }}>{adding ? "Pick an approved, org-clean recipe to fill this slot." : "Pick an approved, org-clean recipe to replace this meal."}</p>
          </div>
          <button className="icon-btn" onClick={onClose}><Icon name="x" size={18} /></button>
        </div>
        <div className="mp-drawer-search">
          <Icon name="search" size={15} />
          <input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search approved recipes…" />
        </div>
        <div className="mp-drawer-body">
          {list.length === 0 && <div className="mp-drawer-empty">No approved recipes match “{q}”.</div>}
          <div className="mp-swap-pick">
            {list.map((r) => {
              const n = mpRecipeNutr(r.id);
              const clean = mpRecipeComplianceFlags(r.id).length === 0;
              return (
                <button key={r.id} className="mp-swap-opt" onClick={() => onPick(r)}>
                  <div className="mp-swap-opt-thumb" style={{ backgroundImage: `url("${r.cover}")` }} />
                  <div className="mp-swap-opt-main">
                    <div className="mp-swap-opt-nm">{r.name}</div>
                    <div className="mp-cell-meta"><span>{n.calories} kcal</span><span>{n.sodium}mg Na</span><span>{n.fiber}g fib</span></div>
                  </div>
                  {clean
                    ? <span className="mp-swap-opt-tag ok"><Icon name="shield-check" size={11} /> Clean</span>
                    : <span className="mp-swap-opt-tag warn"><Icon name="alert-circle" size={11} /> Flags</span>}
                  <Icon name="arrow-right" size={15} className="mp-swap-opt-go" />
                </button>
              );
            })}
          </div>
        </div>
      </div>
    </div>, document.body);
}

function MealPlannerScreen() {
  useMpRerender();
  const { role, setPage, toast } = useApp();
  const enabled = mpEnabled();
  const list = enabled ? mpLoad() : [];
  const [progId, setProgId] = dUseState(() => window.__mpOpen || (list[0] && list[0].id) || null);
  const [fixFlag, setFixFlag] = dUseState(null);
  const [swapCell, setSwapCell] = dUseState(null);      // cell currently choosing a new recipe
  const [dragKey, setDragKey] = dUseState(null);        // `${day}|${meal}` being dragged
  const [flagsOpen, setFlagsOpen] = dUseState(false);   // Loraa flags drawer
  if (!enabled) return <MealProgramsGate />;
  if (!list.length) return <div className="card pad" style={{ color: "var(--gray-500)" }}>No meal programs yet. Create one to start planning.</div>;

  const prog = list.find((p) => p.id === progId) || list[0];
  const flags = mpValidate(prog);
  const type = MP_TYPES.find((t) => t.id === prog.type) || MP_TYPES[0];
  // The planner is the flag-resolution surface: edit-capable roles can swap
  // meals/ingredients and reorder even on published programs (the distribution
  // gate protects what actually ships). Editing a published plan is a revision.
  const canEdit = mpCan(role, "edit");
  const isPublishedEdit = canEdit && prog.status === "published";

  // Flags keyed to a specific cell, and program-level flags (no day/meal)
  const flagByCell = {};
  flags.forEach((f) => { if (f.day && f.meal) flagByCell[`${f.day}|${f.meal}`] = f; });
  const globalFlags = flags.filter((f) => !f.day || !f.meal);

  const dayNums = Array.from(new Set((prog.days || []).map((d) => d.day))).sort((a, b) => a - b);
  const slots = MP_MEAL_ORDER.filter((s) => (prog.days || []).some((d) => d.meal === s));
  const cellOf = (dn, slot) => (prog.days || []).find((d) => d.day === dn && d.meal === slot) || null;
  const dayKcal = (dn) => (prog.days || []).filter((d) => d.day === dn).reduce((s, m) => s + (mpRecipeNutr(m.recipeId).calories || 0), 0);
  const dayFlagCount = (dn) => Object.keys(flagByCell).filter((k) => k.startsWith(`${dn}|`)).length;
  const avgKcal = dayNums.length ? Math.round(dayNums.reduce((s, dn) => s + dayKcal(dn), 0) / dayNums.length) : 0;

  // Per-day calendar date, anchored to the program start (created date as Day 1)
  const startISO = prog.startDate || prog.created || prog.updated || "2026-06-01";
  const dayDate = (dn) => {
    const d = new Date(startISO + "T00:00:00");
    d.setDate(d.getDate() + (dn - 1));
    return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
  };
  window.__mpResolve = setFixFlag;

  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
  flags.forEach((f) => { counts[f.severity] = (counts[f.severity] || 0) + 1; });

  // Apply a Loraa swap (editable programs only)
  const onSwap = canEdit ? (flag, newRecipe, cell) => {
    if (!cell) return;
    const newDays = (prog.days || []).map((d) => (d.day === cell.day && d.meal === cell.meal ? { ...d, recipeId: newRecipe.id } : d));
    mpUpsert({ ...prog, days: newDays, updated: "2026-06-18" });
    toast(`Swapped in ${newRecipe.name}`);
  } : null;

  // Swap a whole meal for another approved recipe
  // Swap a whole meal, or ADD a recipe into an empty slot (synthetic cell has no recipeId)
  const onSwapMeal = canEdit ? (cell, newRecipe) => {
    const exists = (prog.days || []).some((d) => d.day === cell.day && d.meal === cell.meal);
    const newDays = exists
      ? (prog.days || []).map((d) => (d.day === cell.day && d.meal === cell.meal ? { ...d, recipeId: newRecipe.id, ingSwaps: undefined, ingPortions: undefined, servings: undefined } : d))
      : [...(prog.days || []), { day: cell.day, meal: cell.meal, recipeId: newRecipe.id }];
    mpUpsert({ ...prog, days: newDays, updated: "2026-06-18" });
    toast(exists ? `${cell.meal} · Day ${cell.day} → ${newRecipe.name}` : `Added ${newRecipe.name} · ${cell.meal} · Day ${cell.day}`);
  } : null;

  // Swap a single ingredient (cell-level override; original is struck through)
  const onSwapIng = canEdit ? (cell, orig, replacement) => {
    const newDays = (prog.days || []).map((d) => {
      if (d.day !== cell.day || d.meal !== cell.meal) return d;
      const ingSwaps = { ...(d.ingSwaps || {}) };
      if (replacement && (typeof replacement === "object" ? replacement.id : String(replacement).trim())) ingSwaps[orig] = replacement; else delete ingSwaps[orig];
      return { ...d, ingSwaps };
    });
    mpUpsert({ ...prog, days: newDays, updated: "2026-06-18" });
    toast(replacement ? "Ingredient swapped" : "Swap removed");
  } : null;

  // Adjust a single ingredient's portion (multiplier relative to the recipe's base quantity)
  const onPortion = canEdit ? (cell, orig, mult) => {
    const newDays = (prog.days || []).map((d) => {
      if (d.day !== cell.day || d.meal !== cell.meal) return d;
      const ingPortions = { ...(d.ingPortions || {}) };
      if (mult && Math.abs(mult - 1) > 0.001) ingPortions[orig] = mult; else delete ingPortions[orig];
      return { ...d, ingPortions };
    });
    mpUpsert({ ...prog, days: newDays, updated: "2026-06-18" });
  } : null;

  // Recipe-level servings multiplier for a meal cell
  const onServings = canEdit ? (cell, sv) => {
    const newDays = (prog.days || []).map((d) => (d.day === cell.day && d.meal === cell.meal ? { ...d, servings: sv } : d));
    mpUpsert({ ...prog, days: newDays, updated: "2026-06-18" });
  } : null;

  // Drag a meal card onto another slot → swap the two recipes (+ their ingredient overrides)
  const onDropCell = canEdit ? (srcKey, tgtKey) => {
    if (!srcKey || !tgtKey || srcKey === tgtKey) return;
    const parse = (k) => { const [d, m] = k.split("|"); return { day: +d, meal: m }; };
    const s = parse(srcKey), t = parse(tgtKey);
    const days = (prog.days || []).map((x) => ({ ...x }));
    const src = days.find((x) => x.day === s.day && x.meal === s.meal);
    const tgt = days.find((x) => x.day === t.day && x.meal === t.meal);
    if (!src || !tgt) return;
    const tmp = { recipeId: src.recipeId, ingSwaps: src.ingSwaps, ingPortions: src.ingPortions, servings: src.servings };
    src.recipeId = tgt.recipeId; src.ingSwaps = tgt.ingSwaps; src.ingPortions = tgt.ingPortions; src.servings = tgt.servings;
    tgt.recipeId = tmp.recipeId; tgt.ingSwaps = tmp.ingSwaps; tgt.ingPortions = tmp.ingPortions; tgt.servings = tmp.servings;
    mpUpsert({ ...prog, days, updated: "2026-06-18" });
    toast(`Moved ${s.meal} · Day ${s.day} ↔ ${t.meal} · Day ${t.day}`);
    setDragKey(null);
  } : null;

  return (
    <div>
      <div className="mp-planner-head">
        <div style={{ minWidth: 0, flex: 1 }}>
          <h1 className="page-title">Meal Planner</h1>
          <p className="mpb-sub">Plan the weekly schedule and resolve Loraa flags before assignments go out.</p>
        </div>
        <div className="mp-planner-actions">
          <button className="btn secondary" onClick={() => { window.__mpOpen = prog.id; setPage("meal-program-detail"); }}><Icon name="external-link" size={15} /> Open program</button>
          {canEdit && <button className="btn primary" onClick={() => { window.__mpOpen = prog.id; setPage("meal-program-builder"); }}><Icon name="pencil" size={15} /> Edit schedule</button>}
        </div>
      </div>

      {/* Control bar */}
      <div className="mp-planner-bar">
        <label className="mp-planner-pick">
          <span>Program</span>
          <div className="mp-planner-select">
            <Icon name="utensils-crossed" size={15} />
            <select value={prog.id} onChange={(e) => { setProgId(e.target.value); window.__mpOpen = e.target.value; }}>
              {list.map((p) => <option key={p.id} value={p.id}>{p.name || "Untitled program"}</option>)}
            </select>
            <Icon name="chevron-down" size={15} className="mp-planner-caret" />
          </div>
        </label>
        <div className="mp-planner-stat"><span className="k">Status</span><MpStatusPill status={prog.status} /></div>
        <div className="mp-planner-stat"><span className="k">Cadence</span><span className="v">{prog.frequency || "—"}</span></div>
        <div className="mp-planner-stat"><span className="k">Avg / day</span><span className="v">{avgKcal} kcal</span></div>
        <div className="mp-planner-flagsum">
          {counts.critical > 0 && <span className="mp-flagdot err sm"><Icon name="alert-octagon" size={11} /> {counts.critical} blocking</span>}
          {counts.high > 0 && <span className="mp-flagdot warn sm"><Icon name="alert-triangle" size={11} /> {counts.high} high</span>}
          {counts.medium > 0 && <span className="mp-flagdot info sm"><Icon name="info" size={11} /> {counts.medium} medium</span>}
          {flags.length === 0 && <span className="mp-flagdot ok sm"><Icon name="shield-check" size={11} /> All clear</span>}
        </div>
      </div>

      <div className="mp-planner-full">
        {/* Selected recipes, grouped by day (full width) */}
        <div className="mp-planner-boardwrap">
          <div className="mp-plangrid-head">
            <div className="mp-plangrid-h"><Icon name="utensils-crossed" size={18} className="ic" /> Selected recipes</div>
            {canEdit && <span className="mp-plangrid-hint"><Icon name="move" size={13} /> Drag to reorder · swap meals · adjust portions</span>}
            {isPublishedEdit && <span className="mp-plangrid-revnote"><Icon name="git-branch" size={12} /> Editing a published plan creates a revision</span>}
            <span className="mp-plangrid-count">{(prog.days || []).length}</span>
          </div>
          <div className="mp-dayroll-wrap">
            {dayNums.map((dn) => {
              const dFlags = dayFlagCount(dn);
              return (
                <section key={dn} className="mp-dayroll">
                  <header className="mp-dayroll-head">
                    <div className="mp-dayroll-no">Day {dn}</div>
                    <div className="mp-dayroll-date"><Icon name="calendar" size={13} /> {dayDate(dn)}</div>
                    <div className="mp-dayroll-stats">
                      <span className="mp-dayroll-kcal">{dayKcal(dn)} kcal</span>
                      {dFlags > 0
                        ? <span className="mp-dayroll-flag warn"><Icon name="alert-circle" size={12} /> {dFlags} {dFlags === 1 ? "flag" : "flags"}</span>
                        : <span className="mp-dayroll-flag ok"><Icon name="shield-check" size={12} /> Clean</span>}
                    </div>
                  </header>
                  <div className="mp-dayroll-meals">
                    {slots.map((slot) => {
                      const cell = cellOf(dn, slot);
                      const key = `${dn}|${slot}`;
                      if (!cell) return canEdit
                        ? <button key={key} className="mp-dayroll-empty add" onClick={() => setSwapCell({ day: dn, meal: slot, recipeId: null })} title={`Add a ${slot.toLowerCase()} recipe`}><span className="mp-dayroll-empty-slot">{slot}</span><span className="mp-dayroll-empty-add"><Icon name="plus" size={20} /></span></button>
                        : <div key={key} className="mp-dayroll-empty">{slot}<span>—</span></div>;
                      return (
                        <MpPlannerCard
                          key={key} cellKey={key} cell={cell} flag={flagByCell[key]} date={dayDate(dn)}
                          canEdit={canEdit} dragKey={dragKey}
                          onSwapMeal={onSwapMeal ? () => setSwapCell(cell) : null}
                          onSwapIng={onSwapIng}
                          onPortion={onPortion}
                          onServings={onServings}
                          onDragStart={canEdit ? () => setDragKey(key) : null}
                          onDragEndCell={() => setDragKey(null)}
                          onDropCell={onDropCell}
                        />
                      );
                    })}
                  </div>
                </section>
              );
            })}
          </div>
        </div>
      </div>

      {/* Loraa flags, floating launcher + on-demand right drawer */}
      <button className={`mp-flag-fab ${flags.length === 0 ? "clear" : "has"}`} onClick={() => setFlagsOpen(true)} title="Loraa flags">
        <MpLoraaMark size={20} />
        <span className="mp-flag-fab-tx">Loraa</span>
        {flags.length > 0
          ? <span className="mp-flag-fab-count">{flags.length}</span>
          : <Icon name="shield-check" size={15} className="mp-flag-fab-ok" />}
      </button>
      {flagsOpen && ReactDOM.createPortal(
        <div className="mp-drawer-bg" onMouseDown={(e) => { if (e.target === e.currentTarget) setFlagsOpen(false); }}>
          <div className="mp-drawer mp-flagdrawer" role="dialog" aria-label="Loraa flags">
            <div className="mp-drawer-head">
              <div style={{ minWidth: 0, display: "flex", alignItems: "center", gap: 10 }}>
                <MpLoraaMark size={24} />
                <div>
                  <h3 style={{ margin: 0, fontFamily: "var(--serif)", fontSize: 19 }}>Loraa flags</h3>
                  <p style={{ margin: "2px 0 0", color: "var(--gray-600)", fontSize: 13 }}>{flags.length === 0 ? "Nothing to resolve" : `${flags.length} to review on this plan`}</p>
                </div>
              </div>
              <button className="icon-btn" onClick={() => setFlagsOpen(false)}><Icon name="x" size={18} /></button>
            </div>
            <div className="mp-drawer-body">
              {flags.length === 0 ? (
                <div className="mp-planner-clear"><Icon name="shield-check" size={26} /><p>This plan meets every target, approval, and rotation rule.</p></div>
              ) : (
                <div className="mp-planner-flaglist">
                  {flags.map((f, i) => (
                    <button key={i} className={`mp-planner-flag ${f.severity}`} onClick={() => { setFixFlag(f); setFlagsOpen(false); }}>
                      <span className={`mp-planner-flag-dot ${f.severity}`} />
                      <div className="mp-planner-flag-main">
                        <div className="mp-planner-flag-issue">{f.issue}</div>
                        <div className="mp-planner-flag-meta">
                          <span className={`mp-planner-flag-sev ${f.severity}`}>{f.severity}</span>
                          {f.day && <span className="mp-planner-flag-loc"><Icon name="map-pin" size={11} /> Day {f.day}{f.meal ? ` · ${f.meal}` : ""}</span>}
                        </div>
                      </div>
                      <Icon name="chevron-right" size={15} className="mp-planner-flag-go" />
                    </button>
                  ))}
                </div>
              )}
            </div>
          </div>
        </div>, document.body)}

      {fixFlag && <MpFixPopup prog={prog} flag={fixFlag} onClose={() => setFixFlag(null)} onApplyFix={onSwap ? (r, cell) => { onSwap(fixFlag, r, cell); } : null} />}
      {swapCell && <MpSwapMealDrawer cell={swapCell} onClose={() => setSwapCell(null)} onPick={(r) => { onSwapMeal && onSwapMeal(swapCell, r); setSwapCell(null); }} />}
    </div>
  );
}

/* ─────────── Dynamic workflow timeline ─────────── */
function MpWorkflowTimeline({ prog }) {
  const idx = mpStepIdx(prog);
  return (
    <div className="mp-side-card">
      <h4>Review Workflow</h4>
      <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
        {MP_WORKFLOW.map((s, i) => {
          const state = i < idx ? "done" : i === idx ? "current" : "future";
          return (
            <div key={s.k} style={{ display: "flex", alignItems: "center", gap: 11, padding: "7px 0" }}>
              <span style={{ width: 26, height: 26, borderRadius: "50%", display: "grid", placeItems: "center", flexShrink: 0,
                background: state === "done" ? "var(--green-600)" : state === "current" ? "#fff" : "var(--gray-100)",
                border: state === "current" ? "2px solid var(--green-500)" : "1px solid var(--gray-200)",
                color: state === "done" ? "#fff" : state === "current" ? "var(--green-700)" : "var(--gray-400)",
                boxShadow: state === "current" ? "0 0 0 4px var(--green-100)" : "none" }}>
                {state === "done" ? <Icon name="check" size={13} stroke={3} /> : <Icon name={s.ic} size={13} />}
              </span>
              <span style={{ fontSize: 13, fontWeight: state === "current" ? 800 : state === "done" ? 600 : 500, color: state === "future" ? "var(--gray-400)" : "var(--gray-800)" }}>{s.t}</span>
              {state === "current" && <span className="pill info" style={{ marginLeft: "auto", fontSize: 10.5, padding: "2px 8px" }}>You are here</span>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ─────────── Assignment board ─────────── */
function MpAssignmentBoard({ prog }) {
  const { role, openQuickAssign, openRecipe, toast } = useApp();
  const flags = dUseMemo(() => mpValidate(prog), [JSON.stringify(prog)]);
  const assigns = dUseMemo(() => mpAssignmentsFromValidation(prog, flags), [JSON.stringify(prog)]);
  const canAssign = mpCan(role, "assign");
  if (!assigns.length) return <div className="card pad" style={{ color: "var(--gray-500)", fontSize: 13 }}>No assignments needed, no critical or high-severity issues.</div>;
  const recipeFor = (a) => {
    const cell = (prog.days || []).find((d) => d.day === a.day && d.meal === a.meal);
    return cell ? mpRecipeById(cell.recipeId) : null;
  };
  const open = (a) => {
    const rec = recipeFor(a);
    if (canAssign) {
      openQuickAssign({ kind: "recipe", item: rec || undefined, field: a.field, note: `${prog.name} · Day ${a.day} ${a.meal}, ${a.recommendation}`, priority: a.priority });
    } else if (rec && openRecipe) {
      openRecipe(rec);
    } else {
      toast("Loraa generated this fix, a manager will route it to an owner.");
    }
  };
  return (
    <div className="mp-assign-track">
      {assigns.map((a) => (
        <button key={a.id} className="mp-assign" onClick={() => open(a)}>
          <div className={`mp-assign-ic ${a.severity}`}><Icon name={a.severity === "critical" ? "alert-triangle" : "wand-2"} size={17} /></div>
          <div className="mp-assign-main">
            <div className="mp-assign-field">{a.field}{a.day ? ` · Day ${a.day} ${a.meal}` : ""}</div>
            <div className="mp-assign-detail">{a.recommendation}</div>
          </div>
          <span className={`pill ${MP_ASSIGN_STATUS[a.status].tone}`} style={{ flexShrink: 0 }}>{MP_ASSIGN_STATUS[a.status].label}</span>
          <span className="mp-assign-cta">{canAssign ? <><Icon name="user-plus" size={13} /> Assign</> : <><Icon name="arrow-right" size={13} /> Open recipe</>}</span>
        </button>
      ))}
    </div>
  );
}

/* ─────────── Distribute drawer (right slide-in) ─────────── */
const MP_DIST_DEFAULTS = {
  portal:    { visibility: "team" },
  pdf:       { compliance: true, nutrition: true },
  smartmeal: { segment: "all", push: true, start: "" },
  email:     { recipients: "", subject: "", message: "" },
  csv:       { delim: "comma", cols: MP_CSV_COLS.map((c) => c.id) },
  api:       { key: MP_API_KEYS[0].id, format: "json" },
};
const MP_DIST_CTA = { portal: "Publish to workspace", pdf: "Generate booklet", smartmeal: "Push to SmartMeal", email: "Send email", csv: "Download CSV", api: "Stream now" };
const MP_DIST_CTA_IC = { portal: "monitor", pdf: "file-text", smartmeal: "smartphone", email: "send", csv: "download", api: "plug" };

function MpDistField({ label, hint, children }) {
  return (<label className="mp-distf"><span className="mp-distf-l">{label}</span>{children}{hint && <span className="mp-distf-h">{hint}</span>}</label>);
}

function MpDistConfig({ channel, prog, cfg, set }) {
  if (channel === "portal") {
    return (
      <div className="mp-distcfg">
        {prog.status !== "published" && <div className="mp-dist-note"><Icon name="info" size={13} /> This program is <strong>{(MP_STATUS[prog.status] || {}).label || prog.status}</strong>. Publishing here marks it <strong>Published</strong> and makes it live for your workspace.</div>}
        <div className="mp-distf-l">Who can see it</div>
        <div className="mp-dist-radios">
          {MP_PORTAL_VIS.map((v) => (
            <button key={v.id} type="button" className={`mp-dist-radio ${cfg.visibility === v.id ? "on" : ""}`} onClick={() => set({ visibility: v.id })}>
              <span className="mp-dist-radio-dot" />
              <span><span className="t">{v.label}</span><span className="s">{v.desc}</span></span>
            </button>
          ))}
        </div>
      </div>
    );
  }
  if (channel === "pdf") {
    return (
      <div className="mp-distcfg">
        <div className="mp-distf-l">Booklet contents</div>
        <label className="mp-dist-check disabled"><input type="checkbox" checked readOnly /> <span>Cover page &amp; nutrient targets</span></label>
        <label className="mp-dist-check"><input type="checkbox" checked={cfg.nutrition !== false} onChange={(e) => set({ nutrition: e.target.checked })} /> <span>Per-day nutrition tables</span></label>
        <label className="mp-dist-check"><input type="checkbox" checked={cfg.compliance !== false} onChange={(e) => set({ compliance: e.target.checked })} /> <span>Loraa compliance summary</span></label>
        <div className="mp-dist-note"><Icon name="printer" size={13} /> Opens a print-ready booklet in a new tab, use your browser's <strong>Save as PDF</strong>.</div>
      </div>
    );
  }
  if (channel === "smartmeal") {
    const seg = MP_SMARTMEAL_SEGMENTS.find((s) => s.id === cfg.segment) || MP_SMARTMEAL_SEGMENTS[0];
    return (
      <div className="mp-distcfg">
        <MpDistField label="Audience segment" hint={`${seg.count.toLocaleString()} users will receive this plan`}>
          <div className="mp-dist-select"><select value={cfg.segment} onChange={(e) => set({ segment: e.target.value })}>{MP_SMARTMEAL_SEGMENTS.map((s) => <option key={s.id} value={s.id}>{s.label} · {s.count.toLocaleString()}</option>)}</select><Icon name="chevron-down" size={14} /></div>
        </MpDistField>
        <MpDistField label="Available from"><input type="date" value={cfg.start || ""} onChange={(e) => set({ start: e.target.value })} /></MpDistField>
        <label className="mp-dist-check"><input type="checkbox" checked={!!cfg.push} onChange={(e) => set({ push: e.target.checked })} /> <span>Send a push notification on release</span></label>
      </div>
    );
  }
  if (channel === "email") {
    const { valid, invalid } = mpParseRecipients(cfg.recipients);
    return (
      <div className="mp-distcfg">
        <MpDistField label="Recipients" hint={cfg.recipients ? `${valid.length} valid${invalid.length ? ` · ${invalid.length} invalid` : ""}` : "Separate addresses with commas, spaces, or new lines"}>
          <textarea rows={3} placeholder="jane@clinic.com, team@org.com" value={cfg.recipients} onChange={(e) => set({ recipients: e.target.value })} className={invalid.length ? "has-err" : ""} />
        </MpDistField>
        <MpDistField label="Subject"><input type="text" placeholder={`Your plan: ${prog.name}`} value={cfg.subject} onChange={(e) => set({ subject: e.target.value })} /></MpDistField>
        <MpDistField label="Message (optional)"><textarea rows={2} placeholder="A short note to recipients…" value={cfg.message} onChange={(e) => set({ message: e.target.value })} /></MpDistField>
      </div>
    );
  }
  if (channel === "csv") {
    const cols = cfg.cols || [];
    const toggleCol = (id) => set({ cols: cols.includes(id) ? cols.filter((c) => c !== id) : [...cols, id] });
    return (
      <div className="mp-distcfg">
        <MpDistField label="Delimiter">
          <div className="mp-dist-seg">
            {[["comma", "Comma"], ["semicolon", "Semicolon"], ["tab", "Tab"]].map(([id, l]) => <button key={id} type="button" className={cfg.delim === id ? "on" : ""} onClick={() => set({ delim: id })}>{l}</button>)}
          </div>
        </MpDistField>
        <div className="mp-distf-l">Columns</div>
        <div className="mp-dist-cols">
          {MP_CSV_COLS.map((c) => <label key={c.id} className="mp-dist-check sm"><input type="checkbox" checked={cols.includes(c.id)} onChange={() => toggleCol(c.id)} /> <span>{c.label}</span></label>)}
        </div>
        <div className="mp-dist-note"><Icon name="table-2" size={13} /> {mpScheduleRows(prog).length} rows · {cols.length} columns ready to export.</div>
      </div>
    );
  }
  if (channel === "api") {
    const key = MP_API_KEYS.find((k) => k.id === cfg.key) || MP_API_KEYS[0];
    const payload = mpBuildPayload(prog);
    const json = JSON.stringify(payload, null, 2);
    return (
      <div className="mp-distcfg">
        <MpDistField label="API key">
          <div className="mp-dist-select"><select value={cfg.key} onChange={(e) => set({ key: e.target.value })}>{MP_API_KEYS.map((k) => <option key={k.id} value={k.id}>{k.name} · {k.env}</option>)}</select><Icon name="chevron-down" size={14} /></div>
        </MpDistField>
        <MpDistField label="Endpoint"><input type="text" value={`POST ${key.endpoint}`} readOnly className="mono" /></MpDistField>
        <div className="mp-dist-payload">
          <div className="mp-dist-payload-h"><span><Icon name="braces" size={13} /> Request payload</span><button type="button" className="mp-dist-copy" onClick={() => { try { navigator.clipboard.writeText(json); } catch (e) {} }}><Icon name="copy" size={12} /> Copy</button></div>
          <pre>{json}</pre>
        </div>
      </div>
    );
  }
  return null;
}

function MpDistributeDrawer({ prog, onClose }) {
  const { toast, role } = useApp();
  const [picked, setPicked] = dUseState(prog.channel || "portal");
  const [cfgAll, setCfgAll] = dUseState(() => JSON.parse(JSON.stringify(MP_DIST_DEFAULTS)));
  const [processing, setProcessing] = dUseState(false);
  const [result, setResult] = dUseState(null);
  const [sig, setSig] = dUseState("");
  const [showOverride, setShowOverride] = dUseState(false);
  dUseEffect(() => { const k = (e) => e.key === "Escape" && onClose(); document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, [onClose]);
  const ent = mpPlan() === "enterprise";
  const ch = MP_CHANNELS.find((c) => c.id === picked);
  const cfg = cfgAll[picked] || {};
  const setCfg = (patch) => setCfgAll((prev) => ({ ...prev, [picked]: { ...prev[picked], ...patch } }));
  const history = mpDistLog(prog.id);

  // Critical-flag distribution gate
  const blk = dUseMemo(() => mpBlockingFlags(prog), [JSON.stringify(prog)]);
  const overrides = prog.overrides || [];
  const overridden = overrides.length > 0;
  const gated = blk.total > 0 && !overridden;
  const canOv = mpCanOverride(role);
  const signOverride = () => {
    if (!sig.trim()) { toast("Type your full name to sign this override."); return; }
    mpAddOverride(prog, { name: sig.trim(), role, flags: blk.total, channel: picked });
    toast("Override signed, distribution unlocked.");
    setShowOverride(false); setSig("");
  };
  const roleLabel = (r) => ((window.ROLES && window.ROLES[r] && window.ROLES[r].label) || r);

  const run = () => {
    if (processing) return;
    const res = mpDistRun(prog, picked, cfg);
    if (!res.ok) { toast(res.summary); return; }
    setProcessing(true);
    setTimeout(() => {
      const rec = mpAddDist(prog.id, { channel: picked, channelLabel: ch.label, summary: res.summary, detail: res.detail || "", by: role });
      setProcessing(false);
      setResult({ ...res, channelLabel: ch.label });
      toast(`“${prog.name}” → ${res.summary}`);
    }, 850);
  };

  const fmtAgo = (iso) => {
    const d = (Date.now() - new Date(iso).getTime()) / 1000;
    if (d < 60) return "just now";
    if (d < 3600) return `${Math.floor(d / 60)}m ago`;
    if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
    return `${Math.floor(d / 86400)}d ago`;
  };

  return ReactDOM.createPortal(
    <div className="qad-bg" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="qad" role="dialog" aria-label="Distribute program">
        <div className="qad-head">
          <div style={{ minWidth: 0 }}>
            <span className="pill" style={{ background: "var(--green-50)", color: "var(--green-700)", border: "1px solid var(--green-200)", marginBottom: 8 }}><Icon name="share-2" size={11} stroke={2.4} /> Distribute</span>
            <h3 style={{ margin: 0, fontFamily: "var(--serif)", fontSize: 22 }}>Distribute program</h3>
            <p style={{ margin: "4px 0 0", color: "var(--gray-600)", fontSize: 13 }}>{prog.name}</p>
          </div>
          <button className="icon-btn" onClick={onClose}><Icon name="x" size={18} /></button>
        </div>

        {result ? (
          <>
            <div className="qad-body">
              <div className="mp-dist-success">
                <div className="mp-dist-done-ic"><Icon name="check" size={26} stroke={3} /></div>
                <h3 style={{ fontFamily: "var(--serif)", margin: "4px 0 2px" }}>{result.summary}</h3>
                {result.detail && <p className="muted" style={{ margin: 0 }}>{result.detail}</p>}
                <p className="mp-dist-success-via">via {result.channelLabel}</p>
              </div>
              {history.length > 0 && (
                <div className="mp-dist-history">
                  <div className="mp-dist-history-h">Distribution history</div>
                  {history.slice(0, 6).map((h) => (
                    <div key={h.id} className="mp-dist-history-row">
                      <Icon name={MP_DIST_CTA_IC[h.channel] || "share-2"} size={14} />
                      <div className="m"><span className="t">{h.channelLabel}</span><span className="s">{h.summary}</span></div>
                      <span className="when">{fmtAgo(h.at)}</span>
                    </div>
                  ))}
                </div>
              )}
            </div>
            <div className="qad-foot">
              <button className="btn secondary" onClick={() => setResult(null)}><Icon name="arrow-left" size={14} /> Distribute again</button>
              <button className="btn primary" onClick={onClose}>Done</button>
            </div>
          </>
        ) : (
          <>
            <div className="qad-body">
              {gated && (
                <div className="mp-gate">
                  <div className="mp-gate-head">
                    <div className="mp-gate-ic"><Icon name="shield-alert" size={20} /></div>
                    <div style={{ minWidth: 0 }}>
                      <div className="mp-gate-t">Distribution blocked, {blk.total} critical {blk.total === 1 ? "flag" : "flags"}</div>
                      <div className="mp-gate-s">This program can't be distributed until every critical flag is resolved{canOv ? ", or an authorized override is signed." : "."}</div>
                    </div>
                  </div>
                  <ul className="mp-gate-list">
                    {blk.program.map((f, i) => <li key={`p${i}`}><span className="mp-sev-dot err" /> {f.issue}{f.day ? `, Day ${f.day} ${f.meal || ""}` : ""}</li>)}
                    {blk.recipe.slice(0, 6).map((f, i) => <li key={`r${i}`}><span className="mp-sev-dot err" /> {f.message} <span className="mp-gate-loc">· {f.recipe} (Day {f.day} {f.meal})</span></li>)}
                  </ul>
                  {canOv ? (
                    !showOverride ? (
                      <button className="btn mp-gate-ovbtn" onClick={() => setShowOverride(true)}><Icon name="pen-line" size={14} /> Override &amp; sign as {roleLabel(role)}</button>
                    ) : (
                      <div className="mp-gate-sign">
                        <div className="mp-gate-sign-l"><Icon name="pen-line" size={13} /> Authorized override, type your full name to sign</div>
                        <p className="mp-gate-sign-note">Your name, role, and the timestamp are recorded on this program for audit. You are accepting responsibility for distributing with {blk.total} unresolved critical {blk.total === 1 ? "flag" : "flags"}.</p>
                        <div className="mp-gate-sign-row">
                          <input value={sig} onChange={(e) => setSig(e.target.value)} placeholder="Full name (signature)" onKeyDown={(e) => e.key === "Enter" && signOverride()} />
                          <button className="btn primary" onClick={signOverride}><Icon name="check" size={14} /> Sign override</button>
                        </div>
                        <button className="mp-gate-sign-cancel" onClick={() => { setShowOverride(false); setSig(""); }}>Cancel</button>
                      </div>
                    )
                  ) : (
                    <div className="mp-gate-noperm"><Icon name="lock" size={13} /> Only a Manager, Compliance Officer, or Admin can override. Resolve the flags or escalate.</div>
                  )}
                </div>
              )}
              {overridden && (
                <div className="mp-gate-ovon">
                  <div className="mp-gate-ovon-h"><Icon name="shield-check" size={15} /> Override on file, distribution unlocked</div>
                  {overrides.map((o) => (
                    <div key={o.id} className="mp-gate-ovrow">
                      <Icon name="pen-line" size={13} />
                      <div className="m"><span className="t">{o.name} <span className="r">· {roleLabel(o.role)}</span></span><span className="s">Signed off on {o.flags} critical {o.flags === 1 ? "flag" : "flags"}</span></div>
                      <span className="when">{new Date(o.at).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</span>
                    </div>
                  ))}
                  {canOv && <button className="mp-gate-ovclear" onClick={() => { mpClearOverrides(prog); toast("Override cleared, gate re-armed."); }}><Icon name="rotate-ccw" size={12} /> Clear override</button>}
                </div>
              )}
              <div className="mp-channels" style={{ gridTemplateColumns: "1fr" }}>
                {MP_CHANNELS.map((c) => {
                  const locked = c.enterprise && !ent;
                  const on = picked === c.id;
                  return (
                    <div key={c.id}>
                      <button className={`mp-channel ${on ? "on" : ""} ${locked ? "locked" : ""}`} style={on ? { borderColor: "var(--green-500)", boxShadow: "0 0 0 3px var(--green-100)" } : undefined} onClick={() => !locked && setPicked(c.id)}>
                        <div style={{ display: "flex", alignItems: "center", gap: 11 }}>
                          <div className="mp-channel-ic" style={{ margin: 0 }}><Icon name={c.icon} size={18} /></div>
                          <div style={{ minWidth: 0 }}>
                            <div className="mp-channel-t">{c.label} {locked && <span className="pill neutral" style={{ fontSize: 10 }}><Icon name="lock" size={10} /> Enterprise</span>}</div>
                            <div className="mp-channel-d">{c.desc}</div>
                          </div>
                          {on && <Icon name="check-circle-2" size={18} style={{ color: "var(--green-600)", marginLeft: "auto" }} />}
                        </div>
                      </button>
                      {on && !locked && <MpDistConfig channel={c.id} prog={prog} cfg={cfg} set={setCfg} />}
                    </div>
                  );
                })}
              </div>
              {history.length > 0 && (
                <div className="mp-dist-history">
                  <div className="mp-dist-history-h">Recent distributions</div>
                  {history.slice(0, 4).map((h) => (
                    <div key={h.id} className="mp-dist-history-row">
                      <Icon name={MP_DIST_CTA_IC[h.channel] || "share-2"} size={14} />
                      <div className="m"><span className="t">{h.channelLabel}</span><span className="s">{h.summary}</span></div>
                      <span className="when">{fmtAgo(h.at)}</span>
                    </div>
                  ))}
                </div>
              )}
            </div>
            <div className="qad-foot">
              <button className="btn secondary" onClick={onClose}>Cancel</button>
              <button className="btn primary" onClick={run} disabled={processing || gated} title={gated ? "Resolve critical flags or sign an override first" : undefined}>
                {processing ? <><span className="mp-dist-spin" /> Working…</> : gated ? <><Icon name="lock" size={14} /> Blocked by critical flags</> : <><Icon name={MP_DIST_CTA_IC[picked] || "send"} size={14} /> {MP_DIST_CTA[picked] || `Distribute via ${ch.label}`}</>}
              </button>
            </div>
          </>
        )}
      </div>
    </div>, document.body);
}

/* ═══════════════════════ Program detail ═══════════════════════ */
function MealProgramDetail() {
  useMpRerender();
  const { role, setPage, toast } = useApp();
  const [distOpen, setDistOpen] = dUseState(false);
  const [confirmDel, setConfirmDel] = dUseState(false);
  const enabled = mpEnabled();
  const prog = enabled ? (mpGet(window.__mpOpen) || mpLoad()[0]) : null;
  const flags = dUseMemo(() => (prog ? mpValidate(prog) : []), [JSON.stringify(prog)]);
  if (!enabled) return <MealProgramsGate />;
  if (!prog) return <div className="card pad">Program not found.</div>;
  const type = MP_TYPES.find((t) => t.id === prog.type) || MP_TYPES[0];
  const crit = flags.filter((f) => f.severity === "critical").length;
  const assigns = mpAssignmentsFromValidation(prog, flags);

  const canApprove = mpCan(role, "approve");
  const canPublish = mpCan(role, "publish");
  const canDistribute = mpCan(role, "distribute");
  const canDelete = role === "admin" || role === "super-admin";
  const canEdit = mpCan(role, "edit") && prog.status !== "published";
  const idx = mpStepIdx(prog);

  const advance = (toStatus, msg) => { mpUpsert({ ...prog, status: toStatus, _wfStepIdx: undefined, updated: "2026-06-18" }); toast(msg); };

  // Loraa fix: swap the flagged recipe for an approved suggestion (editable programs only)
  const swapInDetail = (flag, newRecipe, cell) => {
    if (!cell) return;
    const newDays = (prog.days || []).map((d) => (d.day === cell.day && d.meal === cell.meal ? { ...d, recipeId: newRecipe.id } : d));
    mpUpsert({ ...prog, days: newDays, updated: "2026-06-18" });
    toast(`Swapped in ${newRecipe.name}`);
  };

  // Primary CTA mirrors current workflow step
  let primary = null;
  if (prog.status === "published") primary = { label: "Distribute", ic: "share-2", on: () => setDistOpen(true), show: canDistribute };
  else if (idx >= 5) primary = { label: canPublish ? "Approve & Publish" : "Approve", ic: "shield-check", on: () => advance("published", canPublish ? "Program published" : "Program approved"), show: canApprove || canPublish };
  else if (idx >= 4) primary = { label: "Complete Review", ic: "clipboard-check", on: () => advance("approved", "Review complete → Manager Approval"), show: canApprove || mpCan(role, "validate") };
  else if (idx >= 1) primary = { label: "Generate Assignments", ic: "list-checks", on: () => advance("review", `${assigns.length} assignment${assigns.length === 1 ? "" : "s"} generated → Reviewer Verification`), show: mpCan(role, "assign") };
  else primary = { label: "Run Loraa Validation", ic: "lightbulb", on: () => advance("validating", "Loraa validation complete"), show: mpCan(role, "validate") };

  return (
    <div>
      <Crumbs path={[{ label: "Meal Programs", onClick: () => setPage("meal-programs") }, { label: prog.name }]} />
      <div className="mp-detail-head">
        <div style={{ minWidth: 0, flex: 1 }}>
          <span className={`mp-card-type ${prog.type}`} style={{ marginBottom: 6 }}><Icon name={type.icon} size={13} /> {type.label}</span>
          <h1 className="page-title" style={{ marginTop: 4 }}>{prog.name}</h1>
          <div style={{ display: "flex", gap: 9, alignItems: "center", flexWrap: "wrap", marginTop: 8 }}>
            <MpStatusPill status={prog.status} />
            <span className="mp-meta-chip"><Icon name="git-branch" size={12} /> {prog.version}</span>
            {crit > 0 && <span className="mp-flagdot err"><Icon name="alert-triangle" size={12} /> {crit} blocking issue{crit === 1 ? "" : "s"}</span>}
          </div>
        </div>
        <div className="mp-detail-actions">
          {canEdit && <button className="btn secondary" onClick={() => { window.__mpOpen = prog.id; setPage("meal-program-builder"); }}><Icon name="pencil" size={15} /> Edit</button>}
          {canDelete && <button className="btn danger mp-del-btn" onClick={() => setConfirmDel(true)} title="Move to Recycle Bin"><Icon name="trash-2" size={15} /> Delete</button>}
          {prog.status === "published" && !canEdit && <span className="pill neutral" style={{ alignSelf: "center" }}><Icon name="lock" size={12} /> Published, locked</span>}
          {primary.show && <button className={`btn ${crit && idx >= 1 && idx < 5 ? "secondary" : "primary"}`} onClick={primary.on} disabled={prog.status !== "published" && crit > 0 && idx >= 5}><Icon name={primary.ic} size={15} /> {primary.label}</button>}
        </div>
      </div>

      <div className="mp-detail-grid">
        <div style={{ minWidth: 0 }}>
          <MpValidationPanel prog={prog} onFix={canEdit ? (() => { window.__mpOpen = prog.id; setPage("meal-program-builder"); }) : null} onSwap={canEdit ? swapInDetail : null} />

          <MpSection icon="calendar-days" title="Schedule" count={`Week 1 · ${prog.frequency}`} defaultOpen={true}>
            <MpScheduleGrid prog={prog} />
          </MpSection>

          <MpSection icon="list-checks" title="Assignments" count={`${assigns.length} generated`} defaultOpen={false}>
            <MpAssignmentBoard prog={prog} />
          </MpSection>

          <div className="mp-section-h"><Icon name="share-2" size={18} className="ic" /><h3>Distribution</h3></div>
          <div className="mp-channels">
            {MP_CHANNELS.map((c) => {
              const locked = (c.enterprise && mpPlan() !== "enterprise") || prog.status !== "published" || !canDistribute;
              return (
                <button key={c.id} className={`mp-channel ${locked ? "locked" : ""}`} onClick={() => !locked && setDistOpen(true)}>
                  <div className="mp-channel-ic"><Icon name={c.icon} size={18} /></div>
                  <div className="mp-channel-t">{c.label} {c.enterprise && mpPlan() !== "enterprise" && <span className="pill neutral" style={{ fontSize: 10 }}><Icon name="lock" size={10} /> Ent.</span>}</div>
                  <div className="mp-channel-d">{c.desc}</div>
                </button>
              );
            })}
          </div>
          {prog.status !== "published" && <p className="muted" style={{ fontSize: 12.5, marginTop: 10 }}><Icon name="info" size={12} /> Distribution unlocks once the program is published.</p>}
        </div>

        <aside>
          <MpWorkflowTimeline prog={prog} />
          <div className="mp-side-card">
            <h4>Program Details</h4>
            <div className="mp-fact"><span className="mp-fact-k">Duration</span><span className="mp-fact-v">{prog.durationWeeks} weeks</span></div>
            <div className="mp-fact"><span className="mp-fact-k">Population</span><span className="mp-fact-v">{prog.population}</span></div>
            <div className="mp-fact"><span className="mp-fact-k">Frequency</span><span className="mp-fact-v">{prog.frequency}</span></div>
            <div className="mp-fact"><span className="mp-fact-k">Owner</span><span className="mp-fact-v">{prog.owner.name}</span></div>
            <div className="mp-fact"><span className="mp-fact-k">Reviewer</span><span className="mp-fact-v">{prog.reviewer ? prog.reviewer.name : "—"}</span></div>
          </div>
          <div className="mp-side-card">
            <h4>Restrictions</h4>
            {(prog.restrictions || []).map((r, i) => (
              <div key={i} className="mp-fact"><span className="mp-fact-k">{r.nutrient}</span><span className="mp-fact-v">{r.op} {r.value} {r.unit}</span></div>
            ))}
            {prog.conditions && prog.conditions.length > 0 && <div className="mp-fact"><span className="mp-fact-k">Conditions</span><span className="mp-fact-v">{prog.conditions.join(", ")}</span></div>}
          </div>
        </aside>
      </div>

      {distOpen && <MpDistributeDrawer prog={prog} onClose={() => setDistOpen(false)} />}
      {confirmDel && <ConfirmDialog
        title={`Move “${prog.name}” to Recycle Bin?`}
        body={`Deleted programs are kept in the Recycle Bin for ${window.BIN_RETENTION_DAYS || 90} days, then permanently removed. An Admin can restore or permanently delete them from Settings → Recycle Bin.`}
        confirmLabel="Move to Recycle Bin" tone="danger" icon="trash-2"
        onCancel={() => setConfirmDel(false)}
        onConfirm={() => {
          try {
            const who = (window.currentUser && window.currentUser(role)) ? window.currentUser(role).name : "Admin";
            if (window.binAdd) window.binAdd("meal-program", prog, who);
            if (typeof mpDelete === "function") mpDelete(prog.id);
            else if (typeof mpSave === "function") mpSave(mpLoad().filter((p) => p.id !== prog.id));
          } catch (e) {}
          window.__mpOpen = null;
          setConfirmDel(false);
          toast(`Moved “${prog.name}” to Recycle Bin`);
          setPage("meal-programs");
        }} />}
    </div>
  );
}

if (typeof window !== "undefined") Object.assign(window, { MpValidationPanel, MpScheduleGrid, MpWorkflowTimeline, MpAssignmentBoard, MpDistributeDrawer, MealProgramDetail, MealPlannerScreen });
