/* NutriDMS, Label Studio (Canadian Nutrition Facts Table generation)
   ═════════════════════════════════════════════════════════════════
   4-panel layout per PRD §16.2:
     Left  , offering, serving, package, language, template, status
     Center, LIVE NFt preview (EN/FR/Bilingual, raw/rounded, zoom)
     Right , compliance checklist, NFt decision tree, Loraa, export
     Bottom, template carousel (fits / does not fit within 15% ADS)
   Decision-support only, final approval stays with the organization. */

const { useState: lsState, useEffect: lsEffect, useMemo: lsMemo, useRef: lsRef } = React;

/* ───────── The authentic Health Canada Nutrition Facts table ───────── */
function NutritionFactsLabel({ recipe, profile, soldProfile, preparedProfile, dual, language, rounded, zoom, format }) {
  const t = NFT_TERMS;
  const bi = language === "bilingual";
  const L = language === "fr" ? t.fr : t.en;
  const group = (format && format.group) || "Standard";

  // §10.5, dual declaration (As Sold / As Prepared) renders two value columns.
  if (dual && soldProfile && preparedProfile) {
    return <NutritionFactsDual t={t} bi={bi} L={L} sold={soldProfile} prep={preparedProfile} language={language} rounded={rounded} zoom={zoom} />;
  }

  const data = lblNftRows(profile, rounded);

  // §16, alternate presentation formats change the whole layout, not just the size.
  if (group === "Linear")     return <NftLinear t={t} bi={bi} L={L} data={data} profile={profile} language={language} zoom={zoom} />;
  if (group === "Horizontal") return <NftHorizontal t={t} bi={bi} L={L} data={data} profile={profile} language={language} zoom={zoom} />;

  const simplified = group === "Simplified";
  const densityClass = group === "Narrow" ? "nft-narrow" : group === "Compact" ? "nft-compact" : "";
  // Simplified format (§16) drops negligible nutrients and lists them in a "not a significant source" line.
  const SIMPLE_DROP = ["saturated", "cholesterol", "fibre", "potassium", "calcium", "iron"];
  const rows = simplified ? data.rows.filter((r) => !SIMPLE_DROP.includes(r.k)) : data.rows;

  const fmt = (v, unit) => v == null ? "—" : `${v}${unit || ""}`;
  const dvCell = (v) => v == null ? "" : `${v}%`;
  const term = (key) => bi ? `${t.en[key]} / ${t.fr[key]}` : L[key];
  const servText = bi
    ? `${t.en.per} ${profile.servingG} grams (${profile.servingG} g) / ${profile.servingG} grammes (${profile.servingG} g)`
    : `${L.per} ${profile.servingG} g`;

  const Row = (r) => {
    const label = r.k === "saturated"
      ? (bi ? `${t.en.saturated} / ${t.fr.saturated}` : L.saturated)
      : term(r.k);
    // Official CFIA bilingual table: Saturated and +Trans render on two lines sharing one %DV.
    if (r.k === "saturated" && r.plusTrans != null) {
      return (
        <div key={r.k} className="nft-row nft-row-sat ind-1">
          <span className="nft-name">
            <span className="nft-sat-lines">
              <span>{label} {fmt(r.amount, r.unit)}</span>
              <span>+ {bi ? `${t.en.trans} / ${t.fr.trans}` : L.trans} {fmt(r.plusTrans, "g")}</span>
            </span>
          </span>
          <span className="nft-dv">{dvCell(r.dv)}</span>
        </div>
      );
    }
    return (
      <div key={r.k} className={`nft-row ind-${r.indent || 0} ${r.bold ? "b" : ""} ${r.sep ? "nft-thick-top" : ""}`}>
        <span className="nft-name">
          {r.bold ? <strong>{label}</strong> : label} {fmt(r.amount, r.unit)}
        </span>
        <span className="nft-dv">{dvCell(r.dv)}</span>
      </div>
    );
  };

  const notSig = bi
    ? "saturated fat, trans fat, cholesterol, fibre, potassium, calcium and iron / lipides saturés, trans, cholestérol, fibres, potassium, calcium et fer"
    : language === "fr"
    ? "lipides saturés, trans, cholestérol, fibres, potassium, calcium et fer"
    : "saturated fat, trans fat, cholesterol, fibre, potassium, calcium and iron";

  return (
    <div className={`nft ${densityClass}`} style={{ transform: `scale(${zoom})` }}>
      <div className="nft-title">{bi ? <span>{t.en.title}<br />{t.fr.title}</span> : L.title}</div>
      <div className="nft-serv">{servText}</div>
      <div className="nft-thick" />
      <div className="nft-cal">
        <span className="nft-cal-l">{bi ? `${t.en.calories} / ${t.fr.calories}` : L.calories}</span>
        <span className="nft-cal-v">{data.calories == null ? "—" : data.calories}</span>
      </div>
      <div className="nft-medium" />
      <div className="nft-dvhead">{bi ? <span>{t.en.dv}<br /><span className="fr">{t.fr.dv}</span></span> : <span>{L.dv}<span className="star">*</span></span>}</div>
      <div className="nft-rows">
        {rows.map(Row)}
      </div>
      <div className="nft-thick" />
      {simplified && (
        <div className="nft-note nft-notsig">
          {language === "fr" && !bi ? <span>Source négligeable de {notSig}.</span> : <span>Not a significant source of {notSig}.</span>}
        </div>
      )}
      <div className="nft-note">{bi ? <span>* {t.en.dvNote}<br />* {t.fr.dvNote}</span> : <span>* {L.dvNote}</span>}</div>
    </div>
  );
}

/* ───────── §16 Linear format, running-text NFt for very small packages ───────── */
function NftLinear({ t, bi, L, data, profile, language, zoom }) {
  const term = (key) => bi ? `${t.en[key]} / ${t.fr[key]}` : L[key];
  const fmt = (v, unit) => v == null ? "—" : `${v}${unit || ""}`;
  const title = bi ? `${t.en.title} / ${t.fr.title}` : L.title;
  const serv = bi ? `${t.en.per} ${profile.servingG} g / ${t.fr.per} ${profile.servingG} g` : `${L.per} ${profile.servingG} g`;
  const parts = [`${bi ? `${t.en.calories} / ${t.fr.calories}` : L.calories} ${data.calories == null ? "—" : data.calories}`];
  data.rows.forEach((r) => {
    const nm = r.k === "saturated" ? (bi ? `${t.en.saturated} / ${t.fr.saturated}` : L.saturated) : term(r.k);
    const trans = r.plusTrans != null ? (bi ? ` + ${t.en.trans} / ${t.fr.trans} ${fmt(r.plusTrans, "g")}` : ` + ${L.trans} ${fmt(r.plusTrans, "g")}`) : "";
    const dv = r.dv == null ? "" : ` (${r.dv}%)`;
    parts.push(`${nm} ${fmt(r.amount, r.unit)}${trans}${dv}`);
  });
  return (
    <div className="nft nft-linear" style={{ transform: `scale(${zoom})` }}>
      <span className="nft-linear-title">{title}</span>{" "}
      <span className="nft-linear-serv">{serv}:</span>{" "}
      <span className="nft-linear-body">{parts.join("; ")}. </span>
      <span className="nft-linear-note">{bi ? `* ${t.en.dvNote} / ${t.fr.dvNote}` : `* ${L.dvNote}`}</span>
    </div>
  );
}

/* ───────── §16 Horizontal format, landscape, nutrients in two columns ───────── */
function NftHorizontal({ t, bi, L, data, profile, language, zoom }) {
  const fmt = (v, unit) => v == null ? "—" : `${v}${unit || ""}`;
  const dvCell = (v) => v == null ? "" : `${v}%`;
  const term = (key) => bi ? `${t.en[key]} / ${t.fr[key]}` : L[key];
  const servText = bi ? `${t.en.per} ${profile.servingG} g / ${t.fr.per} ${profile.servingG} g` : `${L.per} ${profile.servingG} g`;
  const Row = (r) => {
    const label = r.k === "saturated" ? (bi ? `${t.en.saturated} / ${t.fr.saturated}` : L.saturated) : term(r.k);
    const trans = r.plusTrans != null ? (bi ? ` + ${t.en.trans} / ${t.fr.trans} ${fmt(r.plusTrans, "g")}` : ` + ${L.trans} ${fmt(r.plusTrans, "g")}`) : "";
    return (
      <div key={r.k} className={`nft-row ind-${r.indent || 0} ${r.bold ? "b" : ""}`}>
        <span className="nft-name">{r.bold ? <strong>{label}</strong> : label} {fmt(r.amount, r.unit)}{trans}</span>
        <span className="nft-dv">{dvCell(r.dv)}</span>
      </div>
    );
  };
  const half = Math.ceil(data.rows.length / 2);
  const colA = data.rows.slice(0, half);
  const colB = data.rows.slice(half);
  return (
    <div className="nft nft-horizontal" style={{ transform: `scale(${zoom})` }}>
      <div className="nft-h-head">
        <div className="nft-title">{bi ? <span>{t.en.title}<br />{t.fr.title}</span> : L.title}</div>
        <div className="nft-serv">{servText}</div>
        <div className="nft-h-cal"><span>{bi ? `${t.en.calories} / ${t.fr.calories}` : L.calories}</span><b>{data.calories == null ? "—" : data.calories}</b></div>
      </div>
      <div className="nft-h-cols">
        <div className="nft-h-col">
          <div className="nft-dvhead">{bi ? t.en.dv : `${L.dv}*`}</div>
          {colA.map(Row)}
        </div>
        <div className="nft-h-col">
          <div className="nft-dvhead">{bi ? t.fr.dv : "\u00a0"}</div>
          {colB.map(Row)}
          <div className="nft-note">{bi ? <span>* {t.en.dvNote}</span> : <span>* {L.dvNote}</span>}</div>
        </div>
      </div>
    </div>
  );
}

/* ───────── §10.5 Dual declaration, As Sold / As Prepared (two columns) ───────── */
function NutritionFactsDual({ t, bi, L, sold, prep, language, rounded, zoom }) {
  const dSold = lblNftRows(sold, rounded);
  const dPrep = lblNftRows(prep, rounded);
  const term = (key) => bi ? `${t.en[key]} / ${t.fr[key]}` : L[key];
  const fmt = (v, unit) => v == null ? "—" : `${v}${unit || ""}`;
  const byKey = {}; dPrep.rows.forEach((r) => { byKey[r.k] = r; });

  const colHead = (label, g) => (
    <div className="nft-dual-colh">
      <span className="nft-dual-coltitle">{label}</span>
      <span className="nft-dual-colserv">{bi ? `${t.en.per} ${g} g` : `${L.per} ${g} g`}</span>
    </div>
  );
  const cell = (r) => r == null ? <span className="nft-dual-cell">—</span> : (
    <span className="nft-dual-cell"><span className="nft-dual-amt">{fmt(r.amount, r.unit)}</span><span className="nft-dual-dv">{r.dv == null ? "" : `${r.dv}%`}</span></span>
  );

  return (
    <div className="nft nft-dual" style={{ transform: `scale(${zoom})` }}>
      <div className="nft-title">{bi ? <span>{t.en.title}<br />{t.fr.title}</span> : L.title}</div>
      <div className="nft-thick" />
      <div className="nft-dual-heads">
        <span className="nft-dual-corner" />
        {colHead(bi ? "As sold / Tel que vendu" : language === "fr" ? "Tel que vendu" : "As sold", sold.servingG)}
        {colHead(bi ? "As prepared / Préparé" : language === "fr" ? "Préparé" : "As prepared", prep.servingG)}
      </div>
      <div className="nft-medium" />
      <div className="nft-dual-row b">
        <span className="nft-dual-name"><strong>{bi ? `${t.en.calories} / ${t.fr.calories}` : L.calories}</strong></span>
        <span className="nft-dual-cell"><span className="nft-dual-amt">{dSold.calories == null ? "—" : dSold.calories}</span></span>
        <span className="nft-dual-cell"><span className="nft-dual-amt">{dPrep.calories == null ? "—" : dPrep.calories}</span></span>
      </div>
      <div className="nft-medium" />
      <div className="nft-dual-dvhead"><span /><span>{bi ? `${t.en.dv}` : `${L.dv}*`}</span><span>{bi ? `${t.en.dv}` : `${L.dv}*`}</span></div>
      <div className="nft-rows">
        {dSold.rows.map((r) => {
          const label = r.k === "saturated" ? (bi ? `${t.en.saturated} / ${t.fr.saturated}` : L.saturated) : term(r.k);
          const trans = r.plusTrans != null ? (bi ? ` + ${t.en.trans} / ${t.fr.trans}` : ` + ${L.trans}`) : "";
          return (
            <div key={r.k} className={`nft-dual-row ind-${r.indent || 0} ${r.bold ? "b" : ""} ${r.sep ? "nft-thick-top" : ""}`}>
              <span className="nft-dual-name">{r.bold ? <strong>{label}</strong> : label}{trans}</span>
              {cell(r)}
              {cell(byKey[r.k])}
            </div>
          );
        })}
      </div>
      <div className="nft-thick" />
      <div className="nft-note">{bi ? <span>* {t.en.dvNote}<br />* {t.fr.dvNote}</span> : <span>* {L.dvNote}</span>}</div>
    </div>
  );
}

/* ───────── Compliance status pill ───────── */
function LsStatusPill({ status }) {
  const map = {
    pass: { cls: "success", ic: "check-circle-2", label: "Pass, ready for approval" },
    warning: { cls: "warning", ic: "alert-triangle", label: "Warning, review needed" },
    fail: { cls: "error", ic: "x-circle", label: "Fail, cannot approve" },
  };
  const s = map[status] || map.warning;
  return <span className={`pill ${s.cls} ls-statuspill`}><Icon name={s.ic} size={13} stroke={2.4} /> {s.label}</span>;
}

/* ───────── Searchable offering (recipe) picker ───────── */
function LsOfferingPicker({ recipes, value, onChange }) {
  const [open, setOpen] = lsState(false);
  const [q, setQ] = lsState("");
  const ref = lsRef(null);
  lsEffect(() => {
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [open]);
  const cur = recipes.find((r) => r.id === value);
  const filtered = recipes.filter((r) => !q || r.name.toLowerCase().includes(q.toLowerCase()) || r.cuisine.toLowerCase().includes(q.toLowerCase()));
  return (
    <div className="ls-picker" ref={ref}>
      <button className="ls-picker-btn" onClick={() => setOpen((o) => !o)}>
        {cur ? (
          <span className="ls-picker-cur">
            <span className="ls-picker-thumb" style={{ backgroundImage: `url("${cur.cover}")` }} />
            <span><span className="ls-picker-nm">{cur.name}</span><span className="ls-picker-sub">{cur.cuisine} · {cur.servings} servings</span></span>
          </span>
        ) : <span className="ls-picker-ph">Select an offering…</span>}
        <Icon name="chevrons-up-down" size={15} />
      </button>
      {open && (
        <div className="ls-picker-pop">
          <div className="ls-picker-search"><Icon name="search" size={14} /><input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search approved recipes…" /></div>
          <div className="ls-picker-list">
            {filtered.length === 0 && <div className="ls-picker-empty">No approved recipes match.</div>}
            {filtered.map((r) => (
              <button key={r.id} className={`ls-picker-item ${r.id === value ? "on" : ""}`} onClick={() => { onChange(r.id); setOpen(false); setQ(""); }}>
                <span className="ls-picker-thumb" style={{ backgroundImage: `url("${r.cover}")` }} />
                <span><span className="ls-picker-nm">{r.name}</span><span className="ls-picker-sub">{r.cuisine} · {r.calories} kcal · {r.servings} servings</span></span>
                {r.id === value && <Icon name="check" size={15} className="ls-picker-ck" />}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

/* ───────── Loraa explanation (rolling-pill, expandable) ───────── */
/* ───────── Loraa label intelligence, dynamic answers from live label state ───────── */
/* Logo with retry, the shared asset can cold-404 on first request; retry warms it. */
function LsLoraaLogo({ alt }) {
  const onErr = (e) => {
    const img = e.currentTarget;
    const n = +(img.dataset.retry || 0);
    if (n < 4) { img.dataset.retry = n + 1; setTimeout(() => { img.src = `assets/loraa-logo.png?r=${n + 1}`; }, 250 * (n + 1)); }
  };
  return <span className="cu-loraa-logo sm"><img src="assets/loraa-logo.png" alt={alt == null ? "Loraa" : alt} onError={onErr} /></span>;
}
function lsLoraaAnswer(id, ctx) {
  const { decision, fop, checklist, format, servingStatus, claimRes, ads } = ctx;
  const cap = ((ads || 0) * 0.15).toFixed(1);
  switch (id) {
    case "format": {
      if (!format) return `No standard NFt format fits within 15% of the package's available display surface (${cap} cm²). You'll need an alternative presentation, a fold-out tag, peel-back panel, or package insert, to carry the full table legally.`;
      return `“${format.label}” is a Level ${format.level} layout occupying about ${format.area} cm². That fits inside the 15% display-surface cap (${cap} cm²) for your ${(ads || 0).toFixed(0)} cm² package, so Loraa picked the smallest compliant format. A bigger package could use the roomier Standard layout; a much smaller one would drop to the Linear (running-text) format.`;
    }
    case "fop": {
      if (!fop) return "Front-of-package screening hasn't run yet.";
      const catLabel = { GENERAL: "general prepackaged food (15% DV)", SMALL_REFERENCE_AMOUNT: "small reference amount (10% DV)", MAIN_DISH: "prepackaged main dish / meal (30% DV)" }[fop.category] || "general (15% DV)";
      if (fop.status !== "REQUIRED") return `No Canadian front-of-package symbol is required. For a ${catLabel}, none of saturated+trans fat (${Math.round(fop.rows[0].pct || 0)}% DV), sugars (${Math.round(fop.rows[1].pct || 0)}% DV) or sodium (${Math.round(fop.rows[2].pct || 0)}% DV) reach the ${fop.threshold}% DV threshold.`;
      const hi = fop.rows.filter((r) => r.required).map((r) => `${r.label.toLowerCase()} is ${Math.round(r.pct)}% DV`);
      const below = fop.rows.filter((r) => !r.required).map((r) => `${r.label.toLowerCase()} is ${Math.round(r.pct)}% DV and does not trigger`);
      return `This label requires a Canadian front-of-package nutrition symbol because ${hi.join(" and ")}, at or above the ${fop.threshold}% DV threshold for a ${catLabel}.${below.length ? " " + below.join("; ") + "." : ""}\n\nRequired action: apply the regulated “High in ${fop.symbolNutrients.join(" / ")}” magnifying-glass symbol to the principal display panel, validate placement, and submit for compliance review before production export. The symbol is regulated artwork, its text, border and typography can't be edited.`;
    }
    case "approve": {
      const fails = checklist.checks.filter((c) => c.state === "fail");
      const warns = checklist.checks.filter((c) => c.state === "warning");
      if (!fails.length && !warns.length) return "Everything checks out, all mandatory items pass. A Compliance Reviewer can approve the label, then lock the version to unlock production export.";
      const parts = [];
      if (fails.length) parts.push(`${fails.length} blocking issue(s), ${fails.map((c) => c.label.toLowerCase()).join(", ")}`);
      if (warns.length) parts.push(`${warns.length} item(s) to confirm, ${warns.map((c) => c.label.toLowerCase()).join(", ")}`);
      return `Before approval you still have ${parts.join("; and ")}. Click any flagged row in the Compliance checklist and I'll show exactly why it's flagged and how to clear it.`;
    }
    case "serving": {
      if (!servingStatus || servingStatus.status === "pass") return "Your serving size matches the Canadian reference amount for this food category, so %DV, FOP screening and claim eligibility are all calculated on a compliant basis.";
      return `${servingStatus.message} The serving you declare drives every value on the label, %DV, the FOP thresholds and whether nutrient-content claims qualify, so align it with the reference amount unless you have a documented reason to differ.`;
    }
    case "claims": {
      if (!claimRes || !claimRes.results || !claimRes.results.length) return "You haven't selected any claims yet. Open the Claims validator and I'll test each one live against this label's numbers, a claim only prints if the nutrient values support it and no FOP “high in” symbol blocks it.";
      const c = claimRes.counts || {};
      const bits = [];
      if (c.eligible) bits.push(`${c.eligible} eligible`);
      if (c.warning) bits.push(`${c.warning} need review`);
      if (c.failed) bits.push(`${c.failed} don't qualify on the numbers`);
      if (c.restricted) bits.push(`${c.restricted} restricted by an FOP cross-check`);
      return `Of the claims you selected, ${bits.join(", ")}. Restricted and failed claims can't be printed, remove or correct them in the Claims validator before generating.`;
    }
    case "allergens": {
      const a = checklist.checks.find((c) => c.id === "allergens");
      if (a && a.state === "pass") return "Allergen review passed, every priority allergen detected in the ingredients is declared in the “Contains” statement.";
      return `${(a && a.note) || "Review the ingredient-derived allergens."} Open the Ingredients & allergens card → Review status, then reconcile detected vs. declared. The “Contains” line must list every priority allergen present, and “May contain” covers cross-contact risks.`;
    }
    default:
      return "I can explain the chosen NFt format, front-of-package symbols, serving size, claims, allergens, and what's blocking approval. Tap a question or type your own.";
  }
}
function lsLoraaMatch(q) {
  const s = q.toLowerCase();
  if (/format|template|layout|fit|surface|size of (the )?label/.test(s)) return "format";
  if (/symbol|front|fop|high in|magnif/.test(s)) return "fop";
  if (/serv|portion|reference|gram|how much/.test(s)) return "serving";
  if (/claim|low |source of|free|reduced|light|high protein/.test(s)) return "claims";
  if (/allergen|milk|nut|gluten|contain|sulphite|soy|egg|fish/.test(s)) return "allergens";
  if (/approv|missing|ready|block|lock|publish|export/.test(s)) return "approve";
  return "default";
}

/* ───────── Loraa explanation (rolling-pill, expandable, interactive) ───────── */
function LsLoraa({ recipe, decision, fop, checklist, format, profile, claimRes, servingStatus, ads, language }) {
  const [open, setOpen] = lsState(false);
  const [asked, setAsked] = lsState(null); // { q, a }
  const [typed, setTyped] = lsState("");
  const ctx = { decision, fop, checklist, format, profile, claimRes, servingStatus, ads: ads || 0, language };

  const takeaways = [];
  if (decision && decision.template) takeaways.push(`NFt format “${decision.template.label}” was auto-selected because it fits the package's available display surface.`);
  if (fop && fop.status === "REQUIRED") {
    takeaways.push(`FOP symbol required, “High in ${fop.symbolNutrients.join(" / ")}” (threshold ${fop.threshold}% DV). Apply it to the principal display panel.`);
  } else if (fop) {
    takeaways.push(`No front-of-package symbol triggered, saturated fat, sugars and sodium are below the ${fop.threshold}% DV threshold.`);
  }
  if (checklist) {
    if (checklist.status === "fail") takeaways.push(`This label can't be approved yet: ${checklist.failed.map((c) => c.label.toLowerCase()).join(", ")}.`);
    else if (checklist.status === "warning") takeaways.push(`Almost ready, ${checklist.warnings.length} item(s) need a reviewer's confirmation.`);
    else takeaways.push("All mandatory checks pass, ready for compliance approval.");
  }

  const QS = [
    { id: "format", q: "Why was this NFt format selected?" },
    { id: "fop", q: "Why might a front-of-package symbol be required?" },
    { id: "approve", q: "What's missing before I can approve?" },
    { id: "serving", q: "Is my serving size compliant?" },
    { id: "claims", q: "Which claims can I make?" },
    { id: "allergens", q: "What allergens must I declare?" },
  ];
  const askQ = (id, q) => setAsked({ q, a: lsLoraaAnswer(id, ctx) });
  const submit = () => { if (!typed.trim()) return; setAsked({ q: typed.trim(), a: lsLoraaAnswer(lsLoraaMatch(typed), ctx) }); setTyped(""); };

  return (
    <div className={`ls-loraa ${open ? "open" : ""}`}>
      <button className="ls-loraa-head" onClick={() => setOpen((o) => !o)}>
        <LsLoraaLogo />
        <span className="ls-loraa-t">Ask Loraa about this label</span>
        <Icon name={open ? "chevron-up" : "chevron-down"} size={15} />
      </button>
      {open && (
        <div className="ls-loraa-body">
          <div className="ls-loraa-conf"><Icon name="shield-check" size={12} stroke={2.4} /> High confidence · NutriDMS internal data</div>
          <p className="ls-loraa-lead">Loraa reviewed this offering using NutriDMS nutrition records, the {DV_VERSION} Daily Value table, Canadian rounding rules, and your organization-approved compliance rules.</p>
          <div className="ls-loraa-takeaways">
            {takeaways.map((t, i) => <div key={i} className="ls-loraa-take"><Icon name="lightbulb" size={12} stroke={2.4} /> {t}</div>)}
          </div>

          {asked && (
            <div className="ls-loraa-answer">
              <div className="ls-loraa-answer-q"><Icon name="help-circle" size={13} stroke={2.4} /> {asked.q}</div>
              <div className="ls-loraa-answer-a">
                <LsLoraaLogo alt="" />
                <p>{asked.a}</p>
              </div>
            </div>
          )}

          <div className="ls-loraa-qs">
            {QS.map((it) => <button key={it.id} className={`ls-loraa-q ${asked && asked.q === it.q ? "on" : ""}`} onClick={() => askQ(it.id, it.q)}>{it.q}</button>)}
          </div>

          <div className="ls-loraa-askbar">
            <input value={typed} onChange={(e) => setTyped(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") submit(); }} placeholder="Ask Loraa about this label…" />
            <button className="ls-loraa-send" onClick={submit} disabled={!typed.trim()} aria-label="Ask"><Icon name="arrow-up" size={15} stroke={2.4} /></button>
          </div>

          <div className="ls-loraa-disc"><Icon name="info" size={12} stroke={2.4} /> This explanation is generated from NutriDMS internal data and organization-approved rules. It is for nutrition and compliance support only and does not replace review by an authorized regulatory professional.</div>
        </div>
      )}
    </div>
  );
}

/* ───────── Main screen ───────── */
/* ───────── Ingredient statement block (rendered on the label, center) ───────── */
function IngredientStatementBlock({ recipe, language, zoom, mayContain }) {
  if (!recipe || typeof isIngredientStatement !== "function") return null;
  const statement = isIngredientStatement(recipe, language);
  const allergenLines = isAllergenStatement(recipe, language, mayContain || []);
  if (!statement) return null;
  const pick = (l) => language === "fr" ? l.fr : language === "bilingual" ? `${l.en}  /  ${l.fr}` : l.en;
  return (
    <div className="ls-ingstmt" style={{ transform: `scale(${zoom})`, transformOrigin: "top left" }}>
      <div className="ls-ingstmt-body">{statement}</div>
      {allergenLines.map((l, i) => (
        <div key={i} className={`ls-ingstmt-allergen ${l.kind}`}>{pick(l)}</div>
      ))}
    </div>
  );
}

/* ───────── Ingredient & allergen panel (§19), tabbed ───────── */
function LsIngredients({ recipe, language, rows, review, mayContain, setMayContain }) {
  const [open, setOpen] = lsState(true);
  const [tab, setTab] = lsState("statement");
  if (!recipe) return null;
  const found = (typeof isDetectAllergens === "function") ? isDetectAllergens(recipe) : {};
  const gluten = (typeof isDetectGluten === "function") ? isDetectGluten(recipe) : {};
  const sulph = (typeof isDetectSulphites === "function") ? isDetectSulphites(recipe) : [];
  const declared = (typeof isDeclaredAllergens === "function") ? [...isDeclaredAllergens(recipe)] : [];
  const meta = (typeof CLAIM_STATUS_META !== "undefined") ? CLAIM_STATUS_META : {};
  const rm = meta[review.status] || { tone: "neutral", icon: "circle", label: review.status };
  const MAY_OPTS = ["Milk", "Egg", "Wheat", "Soy", "Fish", "Crustaceans & molluscs", "Tree nuts", "Peanuts", "Sesame", "Mustard", "Sulphites"];
  const toggleMay = (a) => setMayContain((cur) => cur.includes(a) ? cur.filter((x) => x !== a) : [...cur, a]);
  const tabs = [
    ["statement", "Ingredient statement"],
    ["allergens", "Allergens"],
    ["gluten", "Gluten sources"],
    ["sulphites", "Sulphites"],
    ["may", "May contain"],
    ["review", "Review status"],
  ];
  return (
    <section className="ls-card" data-ls-sec="allergens">
      <div className="ls-card-h" style={{ cursor: "pointer" }} onClick={() => setOpen((o) => !o)}>
        <Icon name="list-ordered" size={14} /> Ingredients &amp; allergens <LsStatusPill status={review.status} />
        <span className="grow" />
        <Icon name={open ? "chevron-up" : "chevron-down"} size={14} />
      </div>
      {open && (
        <div className="ls-ing">
          <div className="ls-ing-tabs">
            {tabs.map(([id, label]) => (
              <button key={id} type="button" className={`ls-ing-tab ${tab === id ? "on" : ""}`} onClick={() => setTab(id)}>{label}</button>
            ))}
          </div>

          {tab === "statement" && (
            <div className="ls-ing-pane">
              <div className="ls-hint" style={{ marginBottom: 6 }}>Sorted by descending weight contribution (CFIA §B.01.008). Weights marked ~ are estimated from measures.</div>
              <ol className="ls-ing-list">
                {rows.map((r, i) => (
                  <li key={i}>
                    <span className="ls-ing-nm">{r.name}</span>
                    <span className="ls-ing-amt">{r.estimated ? "~" : ""}{r.amount_g} g · {r.pct}%</span>
                  </li>
                ))}
              </ol>
            </div>
          )}

          {tab === "allergens" && (
            <div className="ls-ing-pane">
              <div className="ls-hint" style={{ marginBottom: 6 }}>Detected from ingredient names. "Contains" prints on the label.</div>
              {Object.keys(found).length === 0 && <div className="ls-ing-empty">No priority allergens detected.</div>}
              {Object.keys(found).map((a) => {
                const isDecl = declared.some((d) => a.toLowerCase().includes(d) || d.includes(a.toLowerCase().split(/[\s&/]/)[0]));
                return (
                  <div key={a} className={`ls-ing-allergen ${isDecl ? "ok" : "warn"}`}>
                    <Icon name={isDecl ? "check-circle-2" : "alert-triangle"} size={13} stroke={2.4} />
                    <div>
                      <span className="ls-ing-allergen-nm">{a}</span>
                      <span className="ls-ing-allergen-src">from {found[a].join(", ")}</span>
                    </div>
                    <span className={`pill ${isDecl ? "success" : "warning"}`} style={{ fontSize: 9.5 }}>{isDecl ? "Declared" : "Not declared"}</span>
                  </div>
                );
              })}
            </div>
          )}

          {tab === "gluten" && (
            <div className="ls-ing-pane">
              {Object.keys(gluten).length === 0 && <div className="ls-ing-empty">No gluten sources detected.</div>}
              {Object.keys(gluten).map((g) => (
                <div key={g} className="ls-ing-allergen warn">
                  <Icon name="wheat" size={13} stroke={2.4} />
                  <div><span className="ls-ing-allergen-nm">{g}</span><span className="ls-ing-allergen-src">from {gluten[g].join(", ")}</span></div>
                </div>
              ))}
            </div>
          )}

          {tab === "sulphites" && (
            <div className="ls-ing-pane">
              {sulph.length === 0
                ? <div className="ls-ing-empty">No sulphite sources detected. Declaration required at ≥10 ppm.</div>
                : sulph.map((s, i) => <div key={i} className="ls-ing-allergen warn"><Icon name="flask-conical" size={13} stroke={2.4} /><div><span className="ls-ing-allergen-nm">{s}</span><span className="ls-ing-allergen-src">possible sulphite source, confirm ppm</span></div></div>)}
            </div>
          )}

          {tab === "may" && (
            <div className="ls-ing-pane">
              <div className="ls-hint" style={{ marginBottom: 6 }}>Add precautionary "May contain" allergens for shared-line cross-contact.</div>
              <div className="ls-claim-chips">
                {MAY_OPTS.map((a) => (
                  <button key={a} type="button" className={`ls-claim-chip ${mayContain.includes(a) ? "on" : ""}`} onClick={() => toggleMay(a)}>
                    <Icon name={mayContain.includes(a) ? "check" : "plus"} size={11} stroke={2.6} /> {a}
                  </button>
                ))}
              </div>
            </div>
          )}

          {tab === "review" && (
            <div className="ls-ing-pane">
              <div className={`ls-ing-review ${review.status}`}>
                <div className="ls-ing-review-top"><Icon name={rm.icon} size={14} stroke={2.4} /> <span>{rm.label}</span></div>
                <div className="ls-ing-review-msg">{review.message}</div>
                {review.detail && <div className="ls-ing-review-detail">{review.detail}</div>}
              </div>
            </div>
          )}
        </div>
      )}
    </section>
  );
}

/* ───────── Claims Validator (PRD §20), pick claims, live eligibility ───────── */
function LsClaims({ defs, selected, onToggle, result }) {
  const [open, setOpen] = lsState(true);
  const groups = lsMemo(() => {
    const g = {};
    defs.forEach((d) => { (g[d.group] = g[d.group] || []).push(d); });
    return g;
  }, [defs]);
  const meta = (typeof CLAIM_STATUS_META !== "undefined") ? CLAIM_STATUS_META : {};
  const c = result.counts || {};
  const roll = result.status === "fail" ? "fail" : result.status === "warning" ? "warning" : selected.length ? "pass" : "neutral";
  return (
    <section className="ls-card" data-ls-sec="claims">
      <div className="ls-card-h" style={{ cursor: "pointer" }} onClick={() => setOpen((o) => !o)}>
        <Icon name="badge-check" size={14} /> Claims validator
        {selected.length > 0 && <LsStatusPill status={roll} />}
        <span className="grow" />
        <Icon name={open ? "chevron-up" : "chevron-down"} size={14} />
      </div>
      {open && (
        <div className="ls-claims">
          <div className="ls-hint" style={{ marginBottom: 6 }}>Select the claims you intend to print. Each is validated live against the values above.</div>
          {Object.keys(groups).map((grp) => (
            <div key={grp} className="ls-claim-grp">
              <div className="ls-claim-grp-h">{grp}</div>
              <div className="ls-claim-chips">
                {groups[grp].map((d) => {
                  const on = selected.includes(d.id);
                  return (
                    <button key={d.id} type="button" className={`ls-claim-chip ${on ? "on" : ""}`} onClick={() => onToggle(d.id)}>
                      <Icon name={on ? "check" : "plus"} size={11} stroke={2.6} /> {d.label}
                    </button>
                  );
                })}
              </div>
            </div>
          ))}

          {result.results.length > 0 && (
            <div className="ls-claim-results">
              <div className="ls-claim-summary">
                {c.eligible ? <span className="pill success" style={{ fontSize: 10 }}>{c.eligible} eligible</span> : null}
                {c.warning ? <span className="pill warning" style={{ fontSize: 10 }}>{c.warning} review</span> : null}
                {c.failed ? <span className="pill error" style={{ fontSize: 10 }}>{c.failed} failed</span> : null}
                {c.restricted ? <span className="pill error" style={{ fontSize: 10 }}>{c.restricted} restricted</span> : null}
              </div>
              {result.results.map((r) => {
                const m = meta[r.status] || { label: r.status, tone: "neutral", icon: "circle" };
                return (
                  <div key={r.id} className={`ls-claim-res ${r.status}`}>
                    <div className="ls-claim-res-top">
                      <Icon name={m.icon} size={13} stroke={2.4} />
                      <span className="ls-claim-res-nm">{r.label}</span>
                      <span className={`pill ${m.tone}`} style={{ fontSize: 9.5 }}>{m.label}</span>
                    </div>
                    {r.basis && <div className="ls-claim-res-basis">{r.basis}</div>}
                    <div className="ls-claim-res-msg">{r.message}</div>
                  </div>
                );
              })}
              <div className="ls-claim-disc"><Icon name="info" size={11} stroke={2.4} /> {(typeof CLAIM_DISCLAIMER !== "undefined") ? CLAIM_DISCLAIMER : ""}</div>
            </div>
          )}
        </div>
      )}
    </section>
  );
}

/* ── Checklist fix guidance (§22), why each non-pass item is flagged + how to resolve it.
   `sec` matches a [data-ls-sec] card so the UI can jump straight to the control. */
const LS_FIX = {
  offering:   { why: "No offering/recipe is selected, so there is nothing to label.", how: "Pick an approved recipe in the Offering card.", sec: "offering" },
  ingredients:{ why: "The source recipe isn't approved yet, so its values can still change before the label is final.", how: "Choose an approved or published recipe in the Offering card, or get this recipe approved first.", sec: "offering" },
  nutrients:  { why: "One or more mandatory Nutrition Facts nutrients have no value.", how: "Complete the missing nutrient values on the recipe (Recipe Builder → Nutrition), then reopen Label Studio.", sec: "offering" },
  serving:    { why: "The serving size differs from the Canadian reference amount for this food category, which affects %DV, FOP and claim eligibility.", how: "Adjust the serving weight in Serving setup to match the reference amount, or confirm the difference is intentional before approval.", sec: "serving" },
  format:     { why: "No NFt template fits the package's available display surface.", how: "Increase the package dimensions in Package setup, or pick an alternative (linear / aggregate) format from the template carousel.", sec: "format" },
  bilingual:  { why: "The label is set to a single language. Canadian prepackaged foods generally require bilingual (EN/FR) labelling.", how: "Switch the Language toggle to Bilingual, or confirm a bilingual exemption applies.", sec: "bilingual" },
  fop:        { why: "Front-of-package screening needs review, a 'high in' symbol may be required.", how: "Open Front-of-package screening: if saturated fat, sugars or sodium reach 15% DV, a symbol is required on the front panel.", sec: "fop" },
  allergens:  { why: "The allergen declaration needs review against the ingredient-derived allergens.", how: "Open Ingredients & allergens → Review status and reconcile detected vs. declared allergens.", sec: "allergens" },
  claims:     { why: "One or more selected claims aren't supported by the numbers or are restricted by an FOP cross-check.", how: "Open the Claims validator and remove or correct the flagged claims.", sec: "claims" },
  reviewer:   { why: "A compliance reviewer hasn't approved this label yet.", how: "A Compliance Reviewer or Admin must click Approve in the Approve · Lock · Export card.", sec: "reviewer" },
  lock:       { why: "The version isn't locked. A production export requires a locked, immutable version.", how: "After approval, click Lock version in the Approve · Lock · Export card.", sec: "reviewer" },
};

/* Export tiers (§25), each is gated by role and by where the label is in the Approve → Lock flow. */
const LS_EXPORT_TIERS = [
  { kind: "draft",      label: "Draft export",      icon: "file-text",      cap: "Watermarked, available anytime",            arg: "Draft (watermarked)" },
  { kind: "review",     label: "Review export",     icon: "file-check-2",   cap: "Share with a reviewer for sign-off",          arg: "Review PDF" },
  { kind: "approved",   label: "Approved export",   icon: "badge-check",    cap: "Clean copy, after compliance approval",      arg: "Approved label" },
  { kind: "production", label: "Production export", icon: "printer",        cap: "Print-ready, needs a locked version",        arg: "Production label" },
  { kind: "audit",      label: "Audit bundle",      icon: "folder-archive", cap: "Full record: NFt, claims, allergens, log",    arg: "Audit bundle" },
];
const LS_ROLE_LABEL = { "media-contributor": "Contributor", dietitian: "Dietitian", manager: "Manager", compliance: "Compliance Reviewer", admin: "Admin", "super-admin": "Super Admin" };

/* Collect same-origin CSS rules that style the label so an exported file renders faithfully offline. */
function lsCollectLabelCss() {
  let css = "";
  try {
    for (const sheet of document.styleSheets) {
      let rules; try { rules = sheet.cssRules; } catch (e) { continue; }
      if (!rules) continue;
      for (const r of rules) {
        const sel = r.selectorText || "";
        if (/nft|ls-stage|ls-lockstamp|pill|gs-qr|gs-bc/.test(sel) || r.type === 1 && /:root/.test(sel)) css += r.cssText + "\n";
        else if (r.type === 1 && /\.nft/.test(r.cssText)) css += r.cssText + "\n";
      }
    }
  } catch (e) {}
  return css;
}

/* Build + download a standalone, printable HTML of the current label preview. */
function lsDownloadLabel({ kind, recipe, ref, language, format, watermark }) {
  const stage = document.querySelector(".ls-stage-inner");
  const labelHtml = stage ? stage.innerHTML : "<p>Label preview unavailable.</p>";
  const css = lsCollectLabelCss();
  const title = `${recipe ? recipe.name : "Label"}, ${kind} export`;
  const stamp = new Date().toLocaleString();
  const wm = watermark ? `<div class="exp-wm">DRAFT, NOT FOR PRODUCTION</div>` : "";
  const doc = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title>
<style>
  :root{color-scheme:light}
  body{margin:0;background:#f1f0ec;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#1a2218}
  .exp-wrap{max-width:720px;margin:0 auto;padding:32px 24px}
  .exp-head{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:2.5px solid #1a2218;padding-bottom:14px;margin-bottom:22px}
  .exp-brand{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:#2f6b18}
  .exp-title{font-size:24px;font-weight:800;margin:3px 0 2px}
  .exp-sub{font-size:12px;color:#5a6657}
  .exp-id{text-align:right;font-size:11px;color:#5a6657}
  .exp-id b{display:block;font-family:ui-monospace,monospace;font-size:15px;color:#15201a}
  .exp-tier{display:inline-block;margin-top:6px;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;background:#e8f2dd;color:#2f6b18;border-radius:999px;padding:3px 10px}
  .exp-stage{position:relative;display:flex;justify-content:center;background:#fff;border-radius:8px;padding:34px;box-shadow:0 2px 12px rgba(14,22,18,.12)}
  .exp-wm{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:34px;font-weight:900;color:rgba(217,72,77,.16);transform:rotate(-24deg);pointer-events:none;letter-spacing:.05em;text-align:center}
  .exp-foot{margin-top:22px;font-size:10.5px;color:#9aa595;border-top:1px solid #d8e0d2;padding-top:12px}
  .exp-print{position:fixed;top:16px;right:16px;background:#2f6b18;color:#fff;border:0;border-radius:999px;padding:10px 18px;font-size:13px;font-weight:700;cursor:pointer;box-shadow:0 8px 18px -8px rgba(47,107,24,.6)}
  @media print{.exp-print{display:none}body{background:#fff}.exp-stage{box-shadow:none;padding:0}}
  ${css}
</style></head>
<body>
  <button class="exp-print" onclick="window.print()">Print / Save PDF</button>
  <div class="exp-wrap">
    <div class="exp-head">
      <div>
        <div class="exp-brand">NutriDMS · Nutrition Facts Label</div>
        <div class="exp-title">${recipe ? recipe.name : "Label"}</div>
        <div class="exp-sub">${format ? format.label : ""} ${language ? "· " + language : ""}</div>
        <span class="exp-tier">${kind} export</span>
      </div>
      <div class="exp-id"><span>Reference</span><b>${(ref || "").toUpperCase()}</b><div>${stamp}</div></div>
    </div>
    <div class="exp-stage">${wm}${labelHtml}</div>
    <div class="exp-foot">Generated by NutriDMS Label Studio · ${stamp}${watermark ? " · DRAFT watermark applied, not for production use." : ""} · Decision-support output; final compliance approval rests with an authorized professional.</div>
  </div>
</body></html>`;
  const blob = new Blob([doc], { type: "text/html" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url; a.download = `${(recipe ? recipe.name : "label").replace(/[^\w]+/g, "-").toLowerCase()}-${kind}.html`;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 4000);
}

/* Build + download the full audit bundle as JSON. */
function lsDownloadAudit({ recipe, ref, profile, fop, claimRes, checklist, format, language }) {
  const bundle = {
    document: "NutriDMS Label Audit Bundle",
    generatedAt: new Date().toISOString(),
    product: { name: recipe ? recipe.name : null, reference: ref, language, nftFormat: format ? format.label : null },
    nutritionFacts: profile || null,
    frontOfPackage: fop ? { threshold: fop.threshold, anyRequired: fop.anyRequired, rows: fop.rows } : null,
    claims: claimRes ? { counts: claimRes.counts, results: claimRes.results } : null,
    complianceChecklist: checklist ? { status: checklist.status, checks: checklist.checks } : null,
    disclaimer: "Decision-support output generated from NutriDMS internal data and organization-approved rules. Final compliance approval rests with an authorized regulatory professional.",
  };
  const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url; a.download = `${(recipe ? recipe.name : "label").replace(/[^\w]+/g, "-").toLowerCase()}-audit-bundle.json`;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 4000);
}

/* Scroll a [data-ls-sec] card into view (no scrollIntoView) + flash-highlight it. */
function lsScrollToSec(sec) {
  const el = document.querySelector(`[data-ls-sec="${sec}"]`);
  if (!el) return;
  let p = el.parentElement;
  while (p) {
    const oy = getComputedStyle(p).overflowY;
    if ((oy === "auto" || oy === "scroll") && p.scrollHeight > p.clientHeight + 4) break;
    p = p.parentElement;
  }
  const scroller = p || document.scrollingElement || document.documentElement;
  const er = el.getBoundingClientRect();
  const sr = (scroller.getBoundingClientRect ? scroller.getBoundingClientRect() : { top: 0 });
  const top = (scroller.scrollTop || 0) + (er.top - (sr.top || 0)) - 90;
  try { scroller.scrollTo({ top, behavior: "smooth" }); } catch (e) { scroller.scrollTop = top; }
  el.classList.add("ls-flash");
  setTimeout(() => el.classList.remove("ls-flash"), 1700);
}

/* Trigger a real file download from the prototype. */
function lsDownload(filename, content, mime) {
  try {
    const blob = new Blob([content], { type: mime || "text/plain" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = filename; document.body.appendChild(a); a.click();
    setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
  } catch (e) {}
}
function lsEsc(s) { return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }

/* Reusable rendered NFt table (plain HTML, print-safe). */
function lsNftTableHtml(profile, rounded) {
  const rows = lblNftRows(profile, rounded);
  const lines = rows.rows.map((r) =>
    `<tr><td class="${r.bold ? "b" : ""} ${r.indent ? "ind" : ""}">${lsEsc(r.k)}${r.amount == null ? "" : " " + lsEsc(r.amount) + lsEsc(r.unit || "")}</td><td class="dv">${r.dv == null ? "" : r.dv + " %"}</td></tr>`
  ).join("");
  return `<div class="nft"><div class="nft-t">Nutrition Facts<br><span class="nft-fr">Valeur nutritive</span></div>
    <div class="nft-serv">Per ${lsEsc(profile.servingG)} g / pour ${lsEsc(profile.servingG)} g</div>
    <div class="nft-bar"></div>
    <div class="nft-cal"><span>Calories</span><span>${rows.calories}</span></div>
    <div class="nft-dvh">% Daily Value* / % valeur quotidienne*</div>
    <table>${lines}</table>
    <div class="nft-foot">* 5% or less is a little, 15% or more is a lot / 5% ou moins c'est peu, 15% ou plus c'est beaucoup</div></div>`;
}

/* Tier-aware label document. Distinct banner/watermark per tier. */
function lsBuildLabelDoc(opts) {
  const { tier, recipe, profile, rounded, format, language, ref, status, locked, who, version } = opts;
  const TIER = {
    draft:      { title: "DRAFT LABEL",       tone: "#B45309", bg: "#FEF3C7", watermark: "DRAFT, NOT FOR PRODUCTION", note: "Working copy. Values may change before approval." },
    review:     { title: "REVIEW COPY",       tone: "#2563EB", bg: "#DBEAFE", watermark: "FOR REVIEW",                 note: "Shared for compliance sign-off. Not for printing." },
    approved:   { title: "APPROVED LABEL",    tone: "#15803D", bg: "#DCFCE7", watermark: "",                          note: "Compliance-approved clean copy." },
    production: { title: "PRODUCTION LABEL",  tone: "#0F172A", bg: "#E2E8F0", watermark: "",                          note: "Print-ready. Locked production version." },
  }[tier] || { title: "LABEL", tone: "#0F172A", bg: "#E2E8F0", watermark: "", note: "" };
  const ingState = typeof isIngredientStatement === "function" ? isIngredientStatement(recipe, language) : "";
  const allerg = typeof isAllergenStatement === "function" ? isAllergenStatement(recipe, language, []) : [];
  const allergHtml = allerg && allerg.length
    ? allerg.map((a) => `<div class="state-line"><b>${lsEsc(language === "fr" ? a.fr.split(":")[0] : a.en.split(":")[0])}:</b> ${lsEsc((language === "fr" ? a.fr : a.en).split(":").slice(1).join(":").trim())}</div>`).join("")
    : `<div class="state-line muted">No declared allergens detected.</div>`;
  const wm = TIER.watermark ? `<div class="wm">${lsEsc(TIER.watermark)}</div>` : "";
  const signoff = tier === "review" ? `<div class="signoff"><div class="so-t">Compliance sign-off</div>
    <div class="so-row"><span>Reviewer</span><span class="so-line"></span></div>
    <div class="so-row"><span>Signature</span><span class="so-line"></span></div>
    <div class="so-row"><span>Date</span><span class="so-line"></span></div>
    <div class="so-row"><span>Decision</span><span>☐ Approve&nbsp;&nbsp;&nbsp;☐ Changes requested</span></div></div>` : "";
  return `<!doctype html><html lang="${language === "fr" ? "fr" : "en"}"><head><meta charset="utf-8">
<title>${lsEsc(recipe.name)}, ${TIER.title}, ${lsEsc(ref)}</title>
<style>
  @page { size: letter; margin: 16mm; }
  * { box-sizing: border-box; }
  body { font-family: -apple-system, Helvetica, Arial, sans-serif; color: #0F172A; margin: 0; padding: 32px; position: relative; }
  .banner { display: flex; align-items: center; justify-content: space-between; gap: 12px; background: ${TIER.bg}; color: ${TIER.tone}; border: 1.5px solid ${TIER.tone}33; border-radius: 10px; padding: 12px 16px; margin-bottom: 18px; }
  .banner b { font-size: 15px; letter-spacing: .04em; }
  .banner .meta { font-size: 12px; opacity: .85; text-align: right; }
  .wrap { display: flex; gap: 28px; align-items: flex-start; flex-wrap: wrap; }
  .nft { border: 2.5px solid #000; width: 300px; padding: 8px 12px; background: #fff; flex: 0 0 auto; }
  .nft-t { font-size: 26px; font-weight: 800; line-height: 1; border-bottom: 6px solid #000; padding-bottom: 3px; }
  .nft-fr { font-size: 16px; font-weight: 700; }
  .nft-serv { font-size: 12px; padding: 3px 0; }
  .nft-bar { border-top: 8px solid #000; margin: 2px 0; }
  .nft-cal { display: flex; justify-content: space-between; font-size: 22px; font-weight: 800; border-bottom: 4px solid #000; padding-bottom: 2px; }
  .nft-dvh { text-align: right; font-size: 10px; font-weight: 700; border-bottom: 1px solid #000; padding: 2px 0; }
  .nft table { width: 100%; border-collapse: collapse; font-size: 12px; }
  .nft td { border-bottom: 1px solid #000; padding: 2px 0; }
  .nft td.b { font-weight: 700; } .nft td.ind { padding-left: 14px; } .nft td.dv { text-align: right; font-weight: 700; }
  .nft-foot { font-size: 8.5px; margin-top: 4px; }
  .side { flex: 1 1 280px; min-width: 260px; }
  .side h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: #475569; margin: 0 0 6px; }
  .state { font-size: 12.5px; line-height: 1.55; margin-bottom: 16px; }
  .state-line { margin-bottom: 3px; } .muted { color: #94A3B8; }
  .signoff { border: 1px dashed #94A3B8; border-radius: 8px; padding: 12px 14px; margin-top: 8px; }
  .so-t { font-weight: 700; font-size: 12px; margin-bottom: 8px; }
  .so-row { display: flex; align-items: center; gap: 10px; font-size: 12px; margin-bottom: 10px; }
  .so-row > span:first-child { width: 78px; color: #475569; }
  .so-line { flex: 1; border-bottom: 1px solid #0F172A; height: 14px; }
  .foot { margin-top: 22px; font-size: 11px; color: #64748B; border-top: 1px solid #E2E8F0; padding-top: 10px; }
  .wm { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; z-index: 0; }
  .wm::after { content: "${lsEsc(TIER.watermark)}"; transform: rotate(-28deg); font-size: 64px; font-weight: 900; color: ${TIER.tone}; opacity: .08; white-space: nowrap; }
  .content { position: relative; z-index: 1; }
  @media print { .noprint { display: none; } body { padding: 0; } }
</style></head><body>${wm}
<div class="content">
  <div class="banner"><b>${TIER.title}</b><div class="meta">${lsEsc(recipe.name)}<br>${lsEsc(ref)}${version ? " · " + lsEsc(version) : ""}</div></div>
  <div class="wrap">
    ${lsNftTableHtml(profile, rounded)}
    <div class="side">
      <h3>Ingredients</h3>
      <div class="state">${ingState ? lsEsc(ingState) : '<span class="muted">No ingredient list available.</span>'}</div>
      <h3>Allergens</h3>
      <div class="state">${allergHtml}</div>
      ${signoff}
    </div>
  </div>
  <div class="foot">${lsEsc(ref)} · ${format ? lsEsc(format.label) : ""} · ${language === "bilingual" ? "Bilingual (EN/FR)" : String(language).toUpperCase()} · Compliance: ${lsEsc(status)}${locked ? " · LOCKED" : ""} · ${TIER.note} · Generated ${new Date().toLocaleString()} by ${lsEsc(who)}<br>Decision-support output, final regulatory responsibility remains with the organization.</div>
</div>
<div class="noprint" style="margin-top:20px;text-align:center"><button onclick="window.print()" style="font:600 14px/1 inherit;padding:10px 20px;border-radius:8px;border:0;background:${TIER.tone};color:#fff;cursor:pointer">Print / Save as PDF</button></div>
</body></html>`;
}

/* Full audit bundle (§25), NFt, ingredients, allergens, claims, FOP, compliance checklist + record. */
function lsBuildAuditDoc(opts) {
  const { recipe, profile, rounded, format, language, ref, status, locked, who, checklist, claimRes, isReview, fop } = opts;
  const ingState = typeof isIngredientStatement === "function" ? isIngredientStatement(recipe, language) : "";
  const checkRows = (checklist.checks || []).map((c) =>
    `<tr class="st-${c.state}"><td>${lsEsc(c.label)}</td><td class="cap">${c.state}</td></tr>`).join("");
  const claimRows = (claimRes && claimRes.results || []).map((r) =>
    `<tr><td>${lsEsc(r.label)}</td><td class="cap st-${r.status === "eligible" ? "pass" : r.status === "restricted" || r.status === "failed" ? "fail" : "warning"}">${lsEsc(r.status)}</td></tr>`).join("") || `<tr><td colspan="2" class="muted">No claims evaluated.</td></tr>`;
  const fopRows = (fop && fop.rows || []).map((r) =>
    `<tr><td>${lsEsc(r.label)}</td><td class="cap ${r.required ? "st-fail" : "st-pass"}">${r.required ? "Symbol required" : "OK"}</td></tr>`).join("") || `<tr><td colspan="2" class="muted">Not screened.</td></tr>`;
  const allerg = isReview || { undeclared: [], extraDeclared: [], message: "" };
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${lsEsc(recipe.name)}, Audit Bundle, ${lsEsc(ref)}</title>
<style>
  @page { size: letter; margin: 16mm; }
  body { font-family: -apple-system, Helvetica, Arial, sans-serif; color: #0F172A; max-width: 820px; margin: 0 auto; padding: 36px 28px; }
  h1 { font-size: 24px; margin: 0 0 2px; } .sub { color: #64748B; font-size: 13px; margin-bottom: 22px; }
  h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: #15803D; border-bottom: 2px solid #DCFCE7; padding-bottom: 5px; margin: 26px 0 10px; }
  table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-top: 4px; }
  td { border-bottom: 1px solid #E2E8F0; padding: 5px 4px; vertical-align: top; }
  .cap { text-transform: capitalize; text-align: right; font-weight: 700; width: 140px; }
  .st-pass td.cap, td.cap.st-pass { color: #15803D; } .st-warning td.cap, td.cap.st-warning { color: #B45309; } .st-fail td.cap, td.cap.st-fail { color: #DC2626; }
  .meta { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 24px; font-size: 12.5px; }
  .meta div { display: flex; justify-content: space-between; border-bottom: 1px solid #F1F5F9; padding: 3px 0; }
  .meta span:first-child { color: #64748B; } .meta b { }
  .state { font-size: 12.5px; line-height: 1.6; } .muted { color: #94A3B8; }
  .nft { border: 2.5px solid #000; width: 280px; padding: 8px 12px; }
  .nft-t { font-size: 24px; font-weight: 800; border-bottom: 6px solid #000; } .nft-fr { font-size: 15px; }
  .nft-serv { font-size: 11px; padding: 2px 0; } .nft-bar { border-top: 8px solid #000; }
  .nft-cal { display: flex; justify-content: space-between; font-size: 20px; font-weight: 800; border-bottom: 4px solid #000; }
  .nft-dvh { text-align: right; font-size: 9.5px; font-weight: 700; border-bottom: 1px solid #000; padding: 2px 0; }
  .nft table { font-size: 11.5px; } .nft td { border-bottom: 1px solid #000; padding: 2px 0; } .nft td.b { font-weight: 700; } .nft td.ind { padding-left: 12px; } .nft td.dv { text-align: right; font-weight: 700; } .nft-foot { font-size: 8px; }
  .badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }
  @media print { .noprint { display: none; } }
</style></head><body>
  <h1>Compliance Audit Bundle</h1>
  <div class="sub">${lsEsc(recipe.name)} · ${lsEsc(ref)} · generated ${new Date().toLocaleString()} by ${lsEsc(who)}</div>

  <h2>Record</h2>
  <div class="meta">
    <div><span>Reference ID</span><b>${lsEsc(ref)}</b></div>
    <div><span>Format</span><b>${format ? lsEsc(format.label) : "—"}</b></div>
    <div><span>Language</span><b>${language === "bilingual" ? "Bilingual (EN/FR)" : String(language).toUpperCase()}</b></div>
    <div><span>Serving</span><b>${lsEsc(profile.servingG)} g</b></div>
    <div><span>Compliance status</span><b class="badge ${status === "pass" ? "st-pass" : "st-warning"}">${lsEsc(status)}</b></div>
    <div><span>Version state</span><b>${locked ? "Locked" : "Unlocked"}</b></div>
  </div>

  <h2>Nutrition Facts</h2>
  ${lsNftTableHtml(profile, rounded)}

  <h2>Ingredient statement</h2>
  <div class="state">${ingState ? lsEsc(ingState) : '<span class="muted">No ingredient list available.</span>'}</div>

  <h2>Allergen review</h2>
  <div class="state">${lsEsc(allerg.message || "Reviewed.")}${allerg.undeclared && allerg.undeclared.length ? `<br><b style="color:#DC2626">Undeclared:</b> ${lsEsc(allerg.undeclared.join(", "))}` : ""}${allerg.extraDeclared && allerg.extraDeclared.length ? `<br><b style="color:#B45309">Extra declared:</b> ${lsEsc(allerg.extraDeclared.join(", "))}` : ""}</div>

  <h2>Claims validation</h2>
  <table>${claimRows}</table>

  <h2>Front-of-package screening</h2>
  <table>${fopRows}</table>

  <h2>Compliance checklist</h2>
  <table>${checkRows}</table>

  <div class="noprint" style="margin-top:26px;text-align:center"><button onclick="window.print()" style="font:600 14px/1 inherit;padding:10px 20px;border-radius:8px;border:0;background:#15803D;color:#fff;cursor:pointer">Print / Save as PDF</button></div>
</body></html>`;
}

/* ── Header status pill + issues popover ── */
function LsHeaderStatus({ checklist, onGoto }) {
  const [open, setOpen] = lsState(false);
  const ref = lsRef(null);
  lsEffect(() => {
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [open]);
  const issues = checklist.checks.filter((c) => c.state !== "pass");
  const map = { pass: { cls: "success", ic: "check-circle-2", label: "Pass, ready for approval" }, warning: { cls: "warning", ic: "alert-triangle", label: "Warning, review needed" }, fail: { cls: "error", ic: "x-circle", label: "Fail, cannot approve" } };
  const s = map[checklist.status] || map.warning;
  return (
    <div className="ls-headstatus" ref={ref}>
      <button className={`pill ${s.cls} ls-statuspill ${issues.length ? "clickable" : ""}`} onClick={() => issues.length && setOpen((o) => !o)}>
        <Icon name={s.ic} size={13} stroke={2.4} /> {s.label}
        {issues.length > 0 && <Icon name={open ? "chevron-up" : "chevron-down"} size={13} />}
      </button>
      {open && (
        <div className="ls-issues-pop">
          <div className="ls-issues-h"><Icon name="list-checks" size={14} /> {issues.length} item(s) to resolve</div>
          <div className="ls-issues-list">
            {issues.map((c) => {
              const fix = LS_FIX[c.id];
              return (
                <button key={c.id} className={`ls-issue ${c.state}`} onClick={() => { setOpen(false); onGoto(c.id); }}>
                  <Icon name={c.state === "fail" ? "x-circle" : "alert-triangle"} size={15} stroke={2.4} />
                  <div className="ls-issue-body">
                    <span className="ls-issue-l">{c.label}</span>
                    {fix && <span className="ls-issue-how">{fix.how}</span>}
                  </div>
                  <Icon name="arrow-right" size={14} className="ls-issue-go" />
                </button>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

/* ── Generate-label modal (end-to-end output) ── */
function LsGenerateModal({ onClose, recipe, labelRef, profile, soldProfile, preparedProfile, dual, language, rounded, format, fop, checklist, claimRes, isReview, mayContain, onGoto, onAudit, toast }) {
  const blockers = checklist.checks.filter((c) => c.state === "fail");
  const warnings = checklist.checks.filter((c) => c.state === "warning");
  const blocked = blockers.length > 0;

  const labelSpec = () => ({
    reference: labelRef, recipe: recipe.name, generatedAt: new Date().toISOString(),
    format: format ? format.label : null, language, servingG: profile.servingG,
    calories: (lblNftRows(profile, rounded).calories),
    fopRequired: fop ? fop.rows.filter((r) => r.required).map((r) => r.label) : [],
    allergens: isReview ? isReview : null,
    claims: claimRes ? claimRes.results.map((r) => ({ claim: r.label, status: r.status })) : [],
    complianceStatus: checklist.status,
  });
  const labelHtml = () => {
    const rows = lblNftRows(profile, rounded);
    const lines = rows.rows.map((r) => `<tr><td>${r.bold ? "<b>" + r.k + "</b>" : r.k} ${r.amount == null ? "" : r.amount + (r.unit || "")}</td><td style="text-align:right">${r.dv == null ? "" : r.dv + "%"}</td></tr>`).join("");
    return `<!doctype html><html><head><meta charset="utf-8"><title>${recipe.name}, Nutrition Facts</title><style>body{font-family:Helvetica,Arial,sans-serif;padding:24px}.nft{border:2.5px solid #000;width:300px;padding:8px 12px}h1{font-size:20px;margin:0}hr{border:none;border-top:8px solid #000;margin:4px 0}table{width:100%;border-collapse:collapse;font-size:13px}td{border-bottom:1px solid #000;padding:2px 0}</style></head><body><div class="nft"><h1>Nutrition Facts / Valeur nutritive</h1><div>Per ${profile.servingG} g</div><hr><div style="font-size:22px;font-weight:800">Calories ${rows.calories}</div><div style="text-align:right;font-weight:700;border-bottom:1px solid #000">% Daily Value</div><table>${lines}</table></div><p style="font-size:11px;color:#555">${labelRef} · ${format ? format.label : ""} · ${language} · generated ${new Date().toLocaleString()}</p></body></html>`;
  };

  const download = (kind) => {
    if (kind === "json") lsDownload(`${labelRef}-label-spec.json`, JSON.stringify(labelSpec(), null, 2), "application/json");
    else lsDownload(`${labelRef}-nutrition-facts.html`, labelHtml(), "text/html");
    onAudit("label.exported", `${kind.toUpperCase()} label generated for ${recipe.name} (${labelRef})`);
    toast(`${kind === "json" ? "Label spec (JSON)" : "Print-ready label (HTML)"} downloaded`);
  };

  return (
    <div className="ls-gen-scrim" onClick={onClose}>
      <div className="ls-gen" onClick={(e) => e.stopPropagation()}>
        <div className="ls-gen-h">
          <div><div className="ls-gen-t"><Icon name="lightbulb" size={16} /> {blocked ? "Resolve issues before generating" : "Generate label"}</div><div className="ls-gen-sub">{recipe.name} · {labelRef}</div></div>
          <button className="icon-btn" onClick={onClose}><Icon name="x" size={18} /></button>
        </div>

        {blocked ? (
          <div className="ls-gen-body">
            <div className="ls-gen-blockbanner"><Icon name="x-circle" size={16} /> This label can't be generated yet, {blockers.length} blocking issue(s) must be fixed.</div>
            <div className="ls-gen-issues">
              {blockers.map((c) => {
                const fix = LS_FIX[c.id];
                return (
                  <button key={c.id} className="ls-gen-issue fail" onClick={() => { onClose(); onGoto(c.id); }}>
                    <Icon name="x-circle" size={15} stroke={2.4} />
                    <div><span className="ls-gen-issue-l">{c.label}</span>{fix && <span className="ls-gen-issue-how"><b>Fix:</b> {fix.how}</span>}</div>
                    <Icon name="arrow-right" size={14} />
                  </button>
                );
              })}
            </div>
          </div>
        ) : (
          <div className="ls-gen-body">
            {warnings.length > 0 && (
              <div className="ls-gen-warnbanner"><Icon name="alert-triangle" size={15} /> {warnings.length} warning(s), you can generate, but review these before final approval. <button className="ls-gen-warnlink" onClick={() => { onClose(); onGoto(warnings[0].id); }}>Review →</button></div>
            )}
            <div className="ls-gen-preview">
              <NutritionFactsLabel recipe={recipe} profile={profile} soldProfile={soldProfile} preparedProfile={preparedProfile} dual={dual} language={language} rounded={rounded} zoom={0.92} format={format} />
              <IngredientStatementBlock recipe={recipe} language={language} zoom={0.92} mayContain={mayContain} />
            </div>
            <div className="ls-gen-meta">
              <div className="ls-gen-metarow"><span>Format</span><b>{format ? format.label : "—"}</b></div>
              <div className="ls-gen-metarow"><span>Language</span><b>{language === "bilingual" ? "Bilingual (EN/FR)" : language.toUpperCase()}</b></div>
              <div className="ls-gen-metarow"><span>FOP symbol</span><b>{fop && fop.rows.some((r) => r.required) ? "Required" : "Not required"}</b></div>
              <div className="ls-gen-metarow"><span>Compliance</span><b className={checklist.status === "pass" ? "ok" : "warn"}>{checklist.status === "pass" ? "Pass" : "Warning"}</b></div>
            </div>
            <div className="ls-gen-actions">
              <button className="btn primary" onClick={() => download("html")}><Icon name="download" size={15} /> Download label (HTML)</button>
              <button className="btn secondary" onClick={() => download("json")}><Icon name="file-json" size={15} /> Export spec (JSON)</button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function LabelStudio() {
  const { role, setPage, toast } = useApp();
  const recipes = lsMemo(() => (typeof RECIPES !== "undefined" ? RECIPES : []).filter((r) => r.status === "approved" || r.status === "published"), []);
  const [recipeId, setRecipeId] = lsState(() => (recipes[0] && recipes[0].id) || null);
  const recipe = recipes.find((r) => r.id === recipeId) || null;

  const baseProfile = lsMemo(() => recipe ? lblNutrientProfile(recipe) : {}, [recipeId]);
  const defServ = recipe ? lblDefaultServingG(recipe) : 250;
  const [servingG, setServingG] = lsState(defServ);
  lsEffect(() => { setServingG(recipe ? lblDefaultServingG(recipe) : 250); }, [recipeId]);
  const profile = lsMemo(() => recipe ? lblScaleProfile(baseProfile, baseProfile.servingG, servingG) : {}, [baseProfile, servingG]);

  // Serving Size Engine (PRD §11), full serving setup + reference-amount check.
  const refAmt = lsMemo(() => recipe && typeof lblReferenceAmount === "function" ? lblReferenceAmount(recipe) : { refG: 100, unit: "g", house: "1 portion" }, [recipeId]);
  const [serving, setServing] = lsState({ amount: 1, unit: "g", household: "", perContainer: 1, basis: "as-sold", single: "single" });
  lsEffect(() => { setServing({ amount: 1, unit: refAmt.unit, household: refAmt.house, perContainer: (recipe && recipe.servings) || 1, basis: "as-sold", single: ((recipe && recipe.servings) || 1) > 1 ? "multi" : "single" }); }, [recipeId]);
  const setSv = (patch) => setServing((s) => ({ ...s, ...patch }));
  const servingStatus = lsMemo(() => typeof lblServingStatus === "function" ? lblServingStatus({ servingG, refG: refAmt.refG, unit: refAmt.unit }) : { status: "pass", message: "" }, [servingG, refAmt]);

  // Yield & retention (PRD §6/§7), pulled from the recipe if set in the builder,
  // and adjustable here so the live NFt reflects the food "as prepared".
  const [cooking, setCooking] = lsState(() => (recipe && recipe.yr) ? { ...recipe.yr } : { methodId: "raw" });
  lsEffect(() => { setCooking((recipe && recipe.yr) ? { ...recipe.yr } : { methodId: "raw" }); }, [recipeId]);
  const cookedRes = lsMemo(() => {
    if (typeof yrApply !== "function" || !recipe) return { profile, meta: null };
    return yrApply(profile, {
      methodId: cooking.methodId,
      rawWeightG: parseFloat(cooking.rawWeightG) || 0,
      cookedWeightG: parseFloat(cooking.cookedWeightG) || 0,
      servings: parseFloat(cooking.servings) || 0,
      oilAbsorbedG: parseFloat(cooking.oilAbsorbedG) || 0,
      overrides: cooking.overrides || {},
    });
  }, [profile, cooking]);
  const dispProfile = cookedRes.profile;
  const cookMethod = (typeof yrMethod === "function") ? yrMethod(cooking.methodId) : { label: "Raw" };
  const cookActive = (typeof yrConfigured === "function") ? yrConfigured(cooking) : false;

  const [language, setLanguage] = lsState("bilingual");
  const [rounded, setRounded] = lsState(true);
  const [zoom, setZoom] = lsState(1);
  const [specMode, setSpecMode] = lsState(false);
  // §10.5, dual declaration (As Sold / As Prepared). Meaningful only when a
  // cooking/yield adjustment makes the two profiles differ.
  const [dualDecl, setDualDecl] = lsState(false);
  lsEffect(() => { setDualDecl(false); }, [recipeId]);
  const dualOn = dualDecl && cookActive;

  // Package setup → ADS → decision tree
  const [pkg, setPkg] = lsState({ type: "box", l: 14, w: 8, h: 20 });
  const ads = lsMemo(() => lblBoxSurface(pkg.l, pkg.w, pkg.h), [pkg]);
  const decision = lsMemo(() => lblSelectNftFormat(ads, language), [ads, language]);
  const [manualFormat, setManualFormat] = lsState(null);
  const format = manualFormat ? NFT_FORMATS.find((f) => f.id === manualFormat) : decision.template;

  const [fopCategory, setFopCategory] = lsState("GENERAL");
  const fop = lsMemo(() => recipe ? lblFopScreen(dispProfile, { category: fopCategory, servingWeightG: servingG, referenceAmountG: refAmt.refG || servingG }) : null, [dispProfile, fopCategory, servingG, recipeId, refAmt.refG]);
  const [fopLangMode, setFopLangMode] = lsState("BILINGUAL_EN_FIRST");
  const [fopOrient, setFopOrient] = lsState("VERTICAL");
  const [fopApplied, setFopApplied] = lsState(false);
  const [fopDrawer, setFopDrawer] = lsState(false);
  const [sumOpen, setSumOpen] = lsState(null);
  const [fopRowOpen, setFopRowOpen] = lsState(null);
  const [previewFull, setPreviewFull] = lsState(false);
  const [pdpFull, setPdpFull] = lsState(false);
  const [pdpFont, setPdpFont] = lsState(1.6);
  lsEffect(() => { setFopApplied(false); }, [recipeId]);

  // Claims validation (PRD §20), selected claims re-checked live against the
  // current profile, with FOP cross-checks (a positive claim that contradicts a
  // required FOP symbol becomes "restricted").
  const [selClaims, setSelClaims] = lsState([]);
  const toggleClaim = (id) => setSelClaims((cur) => cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id]);
  const claimRes = lsMemo(() => (typeof claimValidateSet === "function" && recipe)
    ? claimValidateSet(selClaims, dispProfile, { fop })
    : { results: [], counts: { restricted: 0, failed: 0, warning: 0, eligible: 0 }, status: "pass" },
    [selClaims, dispProfile, fop, recipeId]);
  lsEffect(() => { setSelClaims([]); }, [recipeId]);

  // Ingredient statement & allergen declaration (PRD §19)
  const [mayContain, setMayContain] = lsState([]);
  lsEffect(() => { setMayContain([]); }, [recipeId]);
  const isRows = lsMemo(() => recipe && typeof isIngredientRows === "function" ? isIngredientRows(recipe) : [], [recipeId]);
  const isReview = lsMemo(() => recipe && typeof isReviewStatus === "function" ? isReviewStatus(recipe) : { status: "pass", message: "", undeclared: [], extraDeclared: [] }, [recipeId]);

  const [reviewerApproved, setReviewerApproved] = lsState(false);
  const [locked, setLocked] = lsState(false);
  lsEffect(() => { setReviewerApproved(false); setLocked(typeof lblIsLockedRecipe === "function" ? lblIsLockedRecipe(recipeId) : false); }, [recipeId]);
  const [genOpen, setGenOpen] = lsState(false);
  const [openFix, setOpenFix] = lsState(null);
  lsEffect(() => { setGenOpen(false); setOpenFix(null); }, [recipeId]);

  const checklist = lsMemo(() => lblComplianceChecklist({
    recipe, profile: dispProfile, servingG, language, format, fopScreened: !!fop, reviewerApproved, locked,
    claims: claimRes, allergenReview: isReview, servingStatus,
  }), [recipe, dispProfile, servingG, language, format, fop, reviewerApproved, locked, claimRes, isReview, servingStatus]);

  const who = (window.currentUser ? (window.currentUser(role) || {}).name : null) || "You";
  const ref = recipe && window.auditRef ? window.auditRef("recipe", recipe) : "";

  const doApprove = () => {
    if (!lblCanLock(role)) { toast("Only a Compliance Reviewer or Admin can approve labels"); return; }
    if (checklist.status === "fail") { toast("Resolve failed checks before approving"); return; }
    if (fop && fop.status === "REQUIRED" && !fopApplied) { toast("Apply the required FOP symbol to the PDP before approving"); return; }
    if (claimRes && claimRes.counts && claimRes.counts.restricted > 0) { toast("Resolve the claim conflict with the FOP result before approving"); return; }
    setReviewerApproved(true);
    toast("Label approved by compliance");
  };
  const doLock = () => {
    if (!lblCanLock(role)) { toast("Only a Compliance Reviewer or Admin can lock a version"); return; }
    if (!reviewerApproved) { toast("Approve the label before locking the version"); return; }
    setLocked(true);
    try { if (typeof lblSetLocked === "function") lblSetLocked(recipeId, true); } catch (e) {}
    toast("Production version locked");
  };
  const doExport = (kind, label) => {
    if (!lblCanExport(role, kind)) { toast(`Your role can't run a ${label} export`); return; }
    // FOP / claim-conflict hard-block on production export (draft stays available, watermarked)
    if (kind === "production") {
      if (fop && fop.status === "REQUIRED" && !fopApplied) { toast("Export blocked, apply the required FOP symbol to the PDP first"); return; }
      if (claimRes && claimRes.counts && claimRes.counts.restricted > 0) { toast("Export blocked, a claim conflicts with the FOP result"); return; }
    }
    if (kind === "approved" && !reviewerApproved) { toast("Approve the label before exporting the approved version"); return; }
    if (kind === "production" && !locked) { toast("Production export is only available for locked labels"); return; }
    if (kind === "audit") {
      lsDownloadAudit({ recipe, ref, profile: dispProfile, fop, claimRes, checklist, format, language });
      toast("Audit bundle downloaded (JSON)");
    } else {
      lsDownloadLabel({ kind, recipe, ref, language, format, watermark: kind === "draft" });
      toast(`${label} downloaded, open it to print or save as PDF`);
    }
  };
  const doGenerate = () => { setGenOpen(true); };
  const gotoCheck = (id) => {
    const fix = LS_FIX[id];
    setOpenFix(id);
    if (fix) lsScrollToSec(fix.sec);
  };
  const genAudit = () => {};

  /* Why is this export tier locked? Returns null when available. */
  const exportState = (kind) => {
    if (!lblCanExport(role, kind)) return { disabled: true, reason: `Your ${LS_ROLE_LABEL[role] || role} role can't run this export, it needs a higher permission level.` };
    if (kind === "production" && fop && fop.status === "REQUIRED" && !fopApplied) return { disabled: true, reason: "Blocked, apply the required FOP symbol to the PDP first." };
    if (kind === "production" && claimRes && claimRes.counts && claimRes.counts.restricted > 0) return { disabled: true, reason: "Blocked, a claim conflicts with the FOP result." };
    if (kind === "approved" && !reviewerApproved) return { disabled: true, reason: "Locked, a Compliance Reviewer must approve the label first (Step 1)." };
    if (kind === "production" && !locked) return { disabled: true, reason: "Locked, lock the version first (Step 2)." };
    return { disabled: false, reason: kind === "draft" ? "Available now, carries a draft watermark." : "Available now." };
  };

  if (!recipe) {
    return (
      <div>
        <Crumbs path={[{ label: "Compliance" }, { label: "CFIA Label Studio" }]} />
        <div className="empty" style={{ marginTop: 40 }}>
          <div className="icon"><Icon name="tag" size={24} /></div>
          <h3>No approved offerings yet</h3>
          <p>Approve a recipe to generate its Canadian Nutrition Facts table.</p>
        </div>
      </div>
    );
  }

  return (
    <div className="ls">
      <Crumbs path={[{ label: "Compliance" }, { label: "CFIA Label Studio" }]} />
      <div className="page-head ls-head">
        <div>
          <h1 className="page-title">CFIA Label Studio</h1>
          <p className="page-sub">Generate a Health Canada–aligned Nutrition Facts table with a live preview, compliance checks and Loraa guidance.</p>
        </div>
        <div className="ls-head-actions">
          <LsHeaderStatus checklist={checklist} onGoto={gotoCheck} />
          <button className="btn primary" onClick={doGenerate}><Icon name="lightbulb" size={15} /> Generate label</button>
        </div>
      </div>

      <button className={`fop-banner ${fop.status === "REQUIRED" ? (fopApplied ? "applied" : "req") : "ok"}`} onClick={() => setFopDrawer(true)}>
        <span className="fop-banner-ic"><Icon name={fop.status === "REQUIRED" ? "shield-alert" : "shield-check"} size={22} /></span>
        <span className="fop-banner-tx">
          <b>Front-of-package compliance</b>
          {fop.status === "REQUIRED"
            ? <span>{fopApplied ? "Symbol applied · " : "Symbol required · "}High in {fop.symbolNutrients.join(", ")}</span>
            : <span>No FOP symbol required for this product</span>}
        </span>
        {fop.status === "REQUIRED" && <span className={`fop-banner-badge ${fopApplied ? "ok" : "req"}`}>{fopApplied ? "Applied to PDP" : "Action required"}</span>}
        <span className="fop-banner-cta">Open <Icon name="chevron-right" size={16} /></span>
      </button>

      <div className="ls-grid">
        {/* LEFT */}
        <aside className="ls-left">
          <section className="ls-card" data-ls-sec="offering">
            <div className="ls-card-h"><Icon name="package" size={14} /> Offering</div>
            <LsOfferingPicker recipes={recipes} value={recipeId} onChange={setRecipeId} />
            <div className="ls-ref"><Icon name="hash" size={11} /> {ref} · <span className={`pill ${recipe.status === "published" ? "success" : "neutral"}`} style={{ fontSize: 10 }}>{recipe.status}</span></div>
          </section>

          <section className="ls-card" data-ls-sec="serving">
            <div className="ls-card-h"><Icon name="utensils" size={14} /> Serving setup <LsStatusPill status={servingStatus.status} /></div>
            <div className="ls-serv-grid">
              <label className="ls-field"><span>Serving amount</span>
                <input type="number" min="0" step="0.5" value={serving.amount} onChange={(e) => setSv({ amount: +e.target.value || 0 })} />
              </label>
              <label className="ls-field"><span>Unit</span>
                <select value={serving.unit} onChange={(e) => setSv({ unit: e.target.value })}>
                  {["g", "mL", "piece", "cup", "tbsp", "package"].map((u) => <option key={u} value={u}>{u}</option>)}
                </select>
              </label>
            </div>
            <label className="ls-field"><span>Household measure</span>
              <input value={serving.household} onChange={(e) => setSv({ household: e.target.value })} placeholder="e.g. 1 cup (250 mL)" />
            </label>
            <label className="ls-field"><span>Serving weight (g/mL)</span>
              <div className="ls-stepper">
                <button onClick={() => setServingG((g) => Math.max(5, g - 5))}>−</button>
                <input type="number" value={servingG} onChange={(e) => setServingG(Math.max(1, +e.target.value || 1))} />
                <button onClick={() => setServingG((g) => g + 5)}>+</button>
              </div>
            </label>
            <div className="ls-serv-grid">
              <div className="ls-serv-ref"><span className="ls-serv-ref-k">Reference amount</span><span className="ls-serv-ref-v">{refAmt.refG} {refAmt.unit}</span></div>
              <label className="ls-field"><span>Servings / container</span>
                <input type="number" min="1" value={serving.perContainer} onChange={(e) => setSv({ perContainer: Math.max(1, +e.target.value || 1) })} />
              </label>
            </div>
            <div className="ls-serv-segs">
              <div className="ls-seg sm">
                {[["as-sold", "As sold"], ["as-prepared", "As prepared"]].map(([v, l]) => (
                  <button key={v} className={serving.basis === v ? "on" : ""} onClick={() => setSv({ basis: v })}>{l}</button>
                ))}
              </div>
              <div className="ls-seg sm">
                {[["single", "Single-serving"], ["multi", "Multi-serving"]].map(([v, l]) => (
                  <button key={v} className={serving.single === v ? "on" : ""} onClick={() => setSv({ single: v })}>{l}</button>
                ))}
              </div>
            </div>
            <div className={`ls-serv-status ${servingStatus.status}`}>
              <Icon name={servingStatus.status === "pass" ? "check-circle-2" : servingStatus.status === "fail" ? "x-circle" : "alert-triangle"} size={13} stroke={2.4} />
              <div><span>{servingStatus.message}</span>{servingStatus.detail && <span className="ls-serv-status-d">{servingStatus.detail}</span>}</div>
            </div>
            <div className="ls-hint">Serving weight drives NFt values, %DV, FOP thresholds and claim eligibility (×{(servingG / 100).toFixed(2)} per 100 g).</div>
          </section>

          <section className="ls-card">
            <div className="ls-card-h"><Icon name="chef-hat" size={14} /> Cooking &amp; yield
              {cookActive && <span className="ls-cook-tag">As prepared</span>}
            </div>
            <label className="ls-field"><span>Preparation method</span>
              <select className="ls-select" value={cooking.methodId || "raw"} onChange={(e) => setCooking((c) => ({ ...c, methodId: e.target.value }))}>
                {(typeof YR_METHODS !== "undefined" ? YR_METHODS : []).map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
              </select>
            </label>
            {cookMethod && cookMethod.absorbsFat && (
              <label className="ls-field"><span>Oil absorbed / serving (g)</span>
                <input type="number" min="0" step="0.5" value={cooking.oilAbsorbedG || ""} onChange={(e) => setCooking((c) => ({ ...c, oilAbsorbedG: e.target.value }))} />
              </label>
            )}
            <div className="ls-cook-grid">
              <label className="ls-field sm"><span>Raw wt (g)</span>
                <input type="number" min="0" value={cooking.rawWeightG || ""} onChange={(e) => setCooking((c) => ({ ...c, rawWeightG: e.target.value }))} placeholder="—" />
              </label>
              <label className="ls-field sm"><span>Cooked wt (g)</span>
                <input type="number" min="0" value={cooking.cookedWeightG || ""} onChange={(e) => setCooking((c) => ({ ...c, cookedWeightG: e.target.value }))} placeholder="—" />
              </label>
            </div>
            {cookActive
              ? <div className="ls-hint">Retention factors for <strong>{cookMethod.label}</strong> are applied to the values above, water-soluble vitamins and leachable minerals are reduced to reflect cooking loss.</div>
              : <div className="ls-hint">Showing raw values. Pick a cooking method to apply Health-Canada-aligned retention &amp; yield.</div>}
            <button type="button" className={`ls-dual-toggle ${dualOn ? "on" : ""}`} disabled={!cookActive} onClick={() => setDualDecl((d) => !d)}>
              <span className="ls-dual-check">{dualOn && <Icon name="check" size={12} stroke={3} />}</span>
              <span><span className="ls-dual-t">Dual declaration (As Sold / As Prepared)</span><span className="ls-dual-sub">{cookActive ? "Show both columns on the label (§10.5)" : "Available once a cooking method is set"}</span></span>
            </button>
          </section>

          <section className="ls-card" data-ls-sec="format">
            <div className="ls-card-h"><Icon name="box" size={14} /> Package setup</div>
            <div className="ls-dims">
              {["l", "w", "h"].map((d) => (
                <label key={d} className="ls-dim"><span>{d === "l" ? "Length" : d === "w" ? "Width" : "Height"} (cm)</span>
                  <input type="number" value={pkg[d]} onChange={(e) => setPkg((p) => ({ ...p, [d]: Math.max(0.1, +e.target.value || 0.1) }))} />
                </label>
              ))}
            </div>
            <div className="ls-ads">
              <div><span className="k">Available display surface</span><strong>{ads.toFixed(0)} cm²</strong></div>
              <div><span className="k">15% NFt cap</span><strong>{lblMaxNftArea(ads).toFixed(1)} cm²</strong></div>
            </div>
          </section>

          <section className="ls-card" data-ls-sec="bilingual">
            <div className="ls-card-h"><Icon name="languages" size={14} /> Language</div>
            <div className="ls-seg">
              {[["en", "English"], ["fr", "Français"], ["bilingual", "Bilingual"]].map(([v, l]) => (
                <button key={v} className={language === v ? "on" : ""} onClick={() => { setLanguage(v); setManualFormat(null); }}>{l}</button>
              ))}
            </div>
          </section>

          <section className="ls-card">
            <div className="ls-card-h"><Icon name="layers" size={14} /> Template level</div>
            {decision.status === "pass"
              ? <div className="ls-decision pass"><Icon name="check-circle-2" size={14} /> Level {decision.template.level} · {decision.template.label}</div>
              : <div className="ls-decision warn"><Icon name="alert-triangle" size={14} /> No standard format fits, alternative method required</div>}
            {manualFormat && <button className="ls-link" onClick={() => setManualFormat(null)}>↺ Use auto-selected format</button>}
          </section>
        </aside>

        {/* CENTER, live preview */}
        <section className="ls-center">
          <div className="ls-toolbar">
            <div className="ls-seg sm">
              {[["en", "EN"], ["fr", "FR"], ["bilingual", "Bilingual"]].map(([v, l]) => (
                <button key={v} className={language === v ? "on" : ""} onClick={() => { setLanguage(v); setManualFormat(null); }}>{l}</button>
              ))}
            </div>
            <div className="ls-seg sm">
              <button className={!rounded ? "on" : ""} onClick={() => setRounded(false)}>Raw</button>
              <button className={rounded ? "on" : ""} onClick={() => setRounded(true)}>Rounded</button>
            </div>
            <button className={`ls-tbtn ${specMode ? "on" : ""}`} onClick={() => setSpecMode((s) => !s)} title="Grid / spec mode"><Icon name="ruler" size={14} /></button>
            <div className="ls-zoom">
              <button onClick={() => setZoom((z) => Math.max(0.6, +(z - 0.1).toFixed(1)))}><Icon name="minus" size={14} /></button>
              <span>{Math.round(zoom * 100)}%</span>
              <button onClick={() => setZoom((z) => Math.min(1.8, +(z + 0.1).toFixed(1)))}><Icon name="plus" size={14} /></button>
            </div>
            <button className="ls-tbtn" onClick={() => setPreviewFull(true)} title="Enlarge to full screen"><Icon name="maximize-2" size={14} /></button>
          </div>
          <div className={`ls-stage ${specMode ? "spec" : ""}`}>
            <div className="ls-stage-inner">
              <NutritionFactsLabel recipe={recipe} profile={dispProfile} soldProfile={profile} preparedProfile={dispProfile} dual={dualOn} language={language} rounded={rounded} zoom={zoom} format={format} />
              <IngredientStatementBlock recipe={recipe} language={language} zoom={zoom} mayContain={mayContain} />
            </div>
            {locked && <div className="ls-lockstamp"><Icon name="lock" size={13} /> LOCKED PRODUCTION VERSION</div>}
            {!reviewerApproved && !locked && <div className="ls-draftmark">DRAFT, NOT APPROVED</div>}
          </div>
          <div className="ls-legal"><Icon name="info" size={12} /> {language === "fr" ? LABEL_LEGAL.fr : LABEL_LEGAL.en}</div>
        </section>

        {/* RIGHT */}
        <aside className="ls-right">
          <section className="ls-card">
            <div className="ls-card-h"><Icon name="clipboard-check" size={14} /> Compliance checklist <LsStatusPill status={checklist.status} /></div>
            <div className="ls-checks">
              {checklist.checks.map((c) => {
                const fix = LS_FIX[c.id];
                const clickable = c.state !== "pass" && !!fix;
                const expanded = openFix === c.id;
                return (
                  <div key={c.id} className={`ls-check ${c.state} ${clickable ? "clickable" : ""} ${expanded ? "open" : ""}`}
                    onClick={clickable ? () => { setOpenFix(expanded ? null : c.id); if (!expanded) lsScrollToSec(fix.sec); } : undefined}>
                    <Icon name={c.state === "pass" ? "check-circle-2" : c.state === "fail" ? "x-circle" : "alert-triangle"} size={14} />
                    <div className="ls-check-body">
                      <span className="ls-check-l">{c.label}{clickable && <Icon name={expanded ? "chevron-up" : "chevron-down"} size={12} className="ls-check-caret" />}</span>
                      {c.note && <span className="ls-check-n">{c.note}</span>}
                      {expanded && fix && (
                        <div className="ls-check-fix">
                          <div className="ls-check-fix-row"><span className="ls-check-fix-k">Why</span><span>{fix.why}</span></div>
                          <div className="ls-check-fix-row"><span className="ls-check-fix-k">How to fix</span><span>{fix.how}</span></div>
                          <div className="ls-check-fix-actions">
                            {c.id === "reviewer" && lblCanLock(role) && checklist.status !== "fail" && (
                              <button className="ls-check-apply" onClick={(e) => { e.stopPropagation(); doApprove(); setOpenFix(null); }}><Icon name="stamp" size={12} /> Approve now</button>
                            )}
                            {c.id === "lock" && reviewerApproved && lblCanLock(role) && (
                              <button className="ls-check-apply" onClick={(e) => { e.stopPropagation(); doLock(); setOpenFix(null); }}><Icon name="lock" size={12} /> Lock version now</button>
                            )}
                            <button className="ls-check-goto" onClick={(e) => { e.stopPropagation(); lsScrollToSec(fix.sec); }}>Go to setting <Icon name="arrow-right" size={12} /></button>
                          </div>
                        </div>
                      )}
                    </div>
                  </div>
                );
              })}
            </div>
          </section>

          <section className="ls-card">
            <div className="ls-card-h"><Icon name="git-branch" size={14} /> NFt decision tree</div>
            <p className="ls-decision-reason">{decision.reason}</p>
          </section>

          <button className={`fop-trigger`} style={{ display: "none" }} onClick={() => setFopDrawer(true)}>
            <span className={`fop-trigger-ic ${fop.status === "REQUIRED" ? "req" : "ok"}`}><Icon name={fop.status === "REQUIRED" ? "shield-alert" : "shield-check"} size={18} /></span>
            <span className="fop-trigger-tx">
              <b>Front-of-package compliance</b>
              {fop.status === "REQUIRED"
                ? <span className="req">{fopApplied ? "Symbol applied · " : "Symbol required · "}High in {fop.symbolNutrients.join(", ")}</span>
                : <span>No FOP symbol required</span>}
            </span>
            <Icon name="chevron-right" size={18} className="fop-trigger-arrow" />
          </button>

          <FopDrawer open={fopDrawer} onClose={() => setFopDrawer(false)} title="Front-of-package compliance" subtitle="Health Canada FOP nutrition symbol" icon="shield-alert">
          <section className="ls-card" data-ls-sec="fop">
            <div className="ls-card-h"><Icon name="shield-alert" size={14} /> Front-of-package screening</div>
            <div className="ls-fop-cat">
              <span className="ls-fop-cat-lbl">Product category</span>
              <div className="ls-fop-cat-seg">
                {[{ id: "GENERAL", t: "General", th: "15%" }, { id: "SMALL_REFERENCE_AMOUNT", t: "Small ref. amount", th: "10%" }, { id: "MAIN_DISH", t: "Main dish / meal", th: "30%" }].map((c) => (
                  <button key={c.id} className={`ls-fop-cat-btn ${fopCategory === c.id ? "on" : ""}`} onClick={() => setFopCategory(c.id)}>{c.t}<small>{c.th}</small></button>
                ))}
              </div>
            </div>
            <div className="ls-fop">
              {fop.rows.map((r) => {
                const fixMap = {
                  saturated: { why: `Saturated + trans fat is ${Math.round(r.pct)}% DV on the calculation base (greater of serving / reference amount), which is ${r.required ? "at or above" : "below"} the ${fop.threshold}% DV threshold for this product category.`, how: "Reduce saturated/trans fat, swap to leaner cuts, lower-fat dairy or oils high in unsaturated fat, or reduce the serving/reference amount if appropriate." },
                  sugars: { why: `Sugars is ${Math.round(r.pct)}% DV on the calculation base, ${r.required ? "at or above" : "below"} the ${fop.threshold}% DV threshold.`, how: "Lower added sugars, reduce sweeteners, use unsweetened ingredients, or reformulate. Naturally-occurring sugars still count toward the total." },
                  sodium: { why: `Sodium is ${Math.round(r.pct)}% DV on the calculation base, ${r.required ? "at or above" : "below"} the ${fop.threshold}% DV threshold.`, how: "Reduce sodium, cut added salt and high-sodium ingredients (soy sauce, miso, cured items), or use low-sodium alternatives." },
                };
                const fx = fixMap[r.key] || {};
                const open = fopRowOpen === r.key;
                return (
                  <div key={r.key} className="ls-fop-wrap">
                    <button className={`ls-fop-row ${r.required ? "req" : ""} ${open ? "open" : ""}`} onClick={() => setFopRowOpen(open ? null : r.key)}>
                      <Icon name={open ? "chevron-down" : "chevron-right"} size={13} className="ls-fop-caret" />
                      <span className="ls-fop-lbl">{r.label}</span>
                      <span className="ls-fop-pct">{r.pct == null ? "—" : Math.round(r.pct) + "% DV"}</span>
                      <span className={`pill ${r.required ? "warning" : "success"}`} style={{ fontSize: 10 }}>{r.required ? "Triggered" : "Below trigger"}</span>
                    </button>
                    {open && (
                      <div className="ls-fop-explain">
                        <div className="ls-fop-explain-row"><span className="ls-fop-explain-k">Why</span><p>{fx.why}</p></div>
                        <div className="ls-fop-explain-row"><span className="ls-fop-explain-k">How to fix</span><p>{fx.how}</p></div>
                        {r.required && <div className="ls-fop-explain-note"><Icon name="info" size={12} /> While triggered, Health Canada requires this nutrient to appear in the “High in” symbol. The symbol satisfies the requirement, it does not need to be removed.</div>}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
            <div className="ls-hint">Threshold: ≥{fop.threshold}% DV (saturated+trans fat, sugars or sodium) on the greater of serving / reference amount.</div>

            <div className={`ls-fop-result ${fop.status === "REQUIRED" ? "req" : "ok"}`}>
              <Icon name={fop.status === "REQUIRED" ? "shield-alert" : "shield-check"} size={15} />
              <div>
                <b>{fop.status === "REQUIRED" ? "FOP symbol required" : "No FOP symbol required"}</b>
                {fop.status === "REQUIRED" && <span>High in {fop.symbolNutrients.join(", ")}</span>}
              </div>
            </div>

            {fop.status === "REQUIRED" && (
              <div className="ls-fop-apply">
                <div className="ls-fop-symbol-preview">
                  <FopSymbol nutrients={fop.symbolNutrients} languageMode={fopLangMode} orientation={fopOrient} size="md" />
                </div>
                <FopVariantPicker languageMode={fopLangMode} orientation={fopOrient} onLanguage={setFopLangMode} onOrientation={setFopOrient} />
                <button className={`btn ${fopApplied ? "secondary" : "primary"} sm ls-fop-applybtn`} onClick={() => {
                  setFopApplied((v) => {
                    const next = !v;
                    return next;
                  });
                }}>
                  <Icon name={fopApplied ? "check-circle-2" : "stamp"} size={14} /> {fopApplied ? "Applied to PDP" : "Apply symbol to PDP"}
                </button>
                {!fopApplied && <div className="ls-fop-warn"><Icon name="alert-triangle" size={12} /> Production export is blocked until the required symbol is applied to the principal display panel.</div>}
              </div>
            )}
          </section>

          <section className="ls-card" data-ls-sec="pdp">
            <div className="ls-card-h"><Icon name="scan-line" size={14} /> PDP preview <span className="ls-pill-note">Principal display panel</span><button className="ls-pdp-enlarge" onClick={() => setPdpFull(true)} title="Enlarge for print"><Icon name="maximize-2" size={14} /></button></div>
            <div className="ls-pdp">
              <div className="ls-pdp-panel">
                <div className="ls-pdp-brand">{recipe ? recipe.name : "Product name"}</div>
                <div className="ls-pdp-sub">{recipe && recipe.cuisine ? recipe.cuisine : "Prepackaged food"}</div>
                <div className="ls-pdp-fop">
                  {fop.status === "REQUIRED"
                    ? (fopApplied
                        ? <FopSymbol nutrients={fop.symbolNutrients} languageMode={fopLangMode} orientation={fopOrient} size="sm" />
                        : <div className="ls-pdp-missing"><Icon name="alert-triangle" size={16} /> FOP symbol not placed</div>)
                    : <div className="ls-pdp-none">No FOP symbol required</div>}
                </div>
                <div className="ls-pdp-net">Net quantity: {servingG ? Math.round(servingG * (profile.servings || 1)) + " g" : "—"}</div>
              </div>
              <div className="ls-pdp-checks">
                {(() => {
                  const need = fop.status === "REQUIRED";
                  const rows = [
                    { l: "Symbol applied to PDP", ok: !need || fopApplied, na: !need },
                    { l: "Placement: top-right of PDP", ok: !need || fopApplied, na: !need },
                    { l: "Required clear space", ok: !need || fopApplied, na: !need },
                    { l: "Minimum symbol size", ok: !need || fopApplied, na: !need },
                    { l: "Contrast (black on white)", ok: true, na: false },
                  ];
                  return rows.map((r, i) => (
                    <div key={i} className={`ls-pdp-check ${r.na ? "na" : r.ok ? "ok" : "bad"}`}>
                      <Icon name={r.na ? "minus-circle" : r.ok ? "check-circle-2" : "x-circle"} size={13} /> {r.l}
                      {r.na && <span className="ls-pdp-na">N/A</span>}
                    </div>
                  ));
                })()}
              </div>
            </div>
            <div className="ls-hint">The FOP symbol is displayed on the principal display panel, never inside the Nutrition Facts table.</div>
          </section>
          </FopDrawer>

          <LsIngredients recipe={recipe} language={language} rows={isRows} review={isReview} mayContain={mayContain} setMayContain={setMayContain} />

          <LsClaims defs={CLAIM_DEFS} selected={selClaims} onToggle={toggleClaim} result={claimRes} />

          {(() => {
            const need = fop.status === "REQUIRED";
            const claimConflict = claimRes && claimRes.counts && claimRes.counts.restricted > 0;
            const nutFail = checklist.checks.find((c) => c.id === "nutrients") && checklist.checks.find((c) => c.id === "nutrients").state === "fail";
            const rows = [
              { l: "Nutrition Facts table", st: nutFail ? "FAIL" : "PASS", info: nutFail ? "One or more mandatory nutrients are missing a value." : "All mandatory Nutrition Facts nutrients are present and the panel renders correctly." },
              { l: "Ingredient statement", st: "PASS", info: "Ingredients are listed in descending order by weight in the required format." },
              { l: "Allergen declaration", st: (isReview && isReview.status === "pass") ? "PASS" : "REVIEW", info: (isReview && isReview.status === "pass") ? "Declared allergens reconcile with the ingredient-derived allergens." : "Confirm the allergen declaration against the detected allergens." },
              { l: "Claims validation", st: claimConflict ? "CONFLICT" : (claimRes && claimRes.counts && claimRes.counts.failed) ? "FAIL" : "PASS", info: claimConflict ? "A selected claim contradicts the required FOP symbol and must be removed or corrected." : "No selected claim conflicts with the nutrition numbers or the FOP result." },
              { l: "FOP screening", st: need ? "REQUIRED" : "NOT REQUIRED", tone: need ? "info" : "ok", info: need
                  ? `This is an informational result, not a failure. Because saturated+trans fat is ${Math.round(fop.rows[0].pct)}% DV${fop.highSodium ? ` and sodium is ${Math.round(fop.rows[2].pct)}% DV` : ""}${fop.highSugars ? ` and sugars is ${Math.round(fop.rows[1].pct)}% DV` : ""}, at or above the ${fop.threshold}% DV threshold, Health Canada requires a front-of-package symbol. The requirement is satisfied by the “FOP symbol applied” row below.`
                  : `None of saturated+trans fat, sugars or sodium reach the ${fop.threshold}% DV threshold, so no front-of-package symbol is required.` },
              { l: "FOP symbol applied", st: !need ? "N/A" : fopApplied ? "PASS" : "FAIL", info: !need ? "No symbol is required for this product, so nothing needs to be applied." : fopApplied ? `The regulated “High in ${fop.symbolNutrients.join(" / ")}” symbol has been applied to the principal display panel.` : "The required FOP symbol has not yet been applied to the PDP, export is blocked until it is." },
              { l: "PDP placement", st: !need ? "N/A" : fopApplied ? "PASS" : "NOT REVIEWED", info: !need ? "No symbol to place." : fopApplied ? "Symbol placed top-right of the principal display panel with the required clear space and contrast." : "Apply the symbol to validate placement." },
              { l: "Reviewer approval", st: reviewerApproved ? "APPROVED" : "PENDING", info: reviewerApproved ? "A compliance reviewer has approved this label." : "A Compliance Reviewer or Admin must approve before locking." },
              { l: "Version lock", st: locked ? "LOCKED" : "UNLOCKED", info: locked ? "This version is locked and immutable, production export is enabled." : "Lock the approved version to enable production export." },
            ];
            const blocked = (need && !fopApplied) || claimConflict || rows.some((r) => r.st === "FAIL") || !reviewerApproved;
            const stCls = (s, tone) => tone === "info" ? "info" : ({ PASS: "ok", APPROVED: "ok", LOCKED: "ok", "NOT REQUIRED": "ok", "N/A": "na", REVIEW: "warn", "NOT REVIEWED": "warn", PENDING: "warn", UNLOCKED: "warn", REQUIRED: "warn", FAIL: "bad", CONFLICT: "bad" })[s] || "na";
            const action = need && !fopApplied
              ? `Apply the “High in ${fop.symbolNutrients.join(" / ")}” symbol to the PDP preview.`
              : claimConflict ? "Resolve the claim that conflicts with the FOP result."
              : !reviewerApproved ? "Submit for compliance reviewer approval."
              : !locked ? "Lock the version to enable production export."
              : "All compliance checks satisfied.";
            return (
              <section className="ls-card" data-ls-sec="summary">
                <div className="ls-card-h"><Icon name="clipboard-list" size={14} /> Compliance summary <span className="ls-sum-hint-tag">tap a row for why</span></div>
                <div className="ls-sum">
                  {rows.map((r, i) => (
                    <div key={i} className="ls-sum-wrap">
                      <button className={`ls-sum-row ${sumOpen === i ? "open" : ""}`} onClick={() => setSumOpen(sumOpen === i ? null : i)}>
                        <Icon name={sumOpen === i ? "chevron-down" : "chevron-right"} size={13} className="ls-sum-caret" />
                        <span className="ls-sum-l">{r.l}</span>
                        <span className={`ls-sum-st ${stCls(r.st, r.tone)}`}>{r.st}</span>
                      </button>
                      {sumOpen === i && <div className="ls-sum-info">{r.info}</div>}
                    </div>
                  ))}
                </div>
                <div className={`ls-sum-overall ${blocked ? "bad" : "ok"}`}>
                  <div className="ls-sum-overall-row"><span>Overall status</span><b>{blocked ? "REVIEW REQUIRED" : "COMPLIANT"}</b></div>
                  <div className="ls-sum-overall-row"><span>Export status</span><b className={blocked ? "bad" : "ok"}>{blocked ? "BLOCKED" : "READY"}</b></div>
                </div>
                <div className="ls-sum-action"><Icon name="arrow-right-circle" size={13} /> {action}</div>
              </section>
            );
          })()}

          <LsLoraa recipe={recipe} decision={decision} fop={fop} checklist={checklist} format={format} profile={dispProfile} claimRes={claimRes} servingStatus={servingStatus} ads={ads} language={language} />

          <section className="ls-card" data-ls-sec="reviewer">
            <div className="ls-card-h"><Icon name="download" size={14} /> Approve · Lock · Export</div>

            {/* progress: Approve → Lock → Export */}
            <div className="ls-steps">
              <div className={`ls-step ${reviewerApproved ? "done" : "now"}`}>
                <span className="ls-step-dot">{reviewerApproved ? <Icon name="check" size={12} stroke={3} /> : "1"}</span>
                <div className="ls-step-tx"><b>Approve</b><small>{reviewerApproved ? "Signed off" : "Compliance sign-off"}</small></div>
              </div>
              <Icon name="chevron-right" size={14} className="ls-step-arrow" />
              <div className={`ls-step ${locked ? "done" : reviewerApproved ? "now" : ""}`}>
                <span className="ls-step-dot">{locked ? <Icon name="check" size={12} stroke={3} /> : "2"}</span>
                <div className="ls-step-tx"><b>Lock</b><small>{locked ? "Version frozen" : "Freeze version"}</small></div>
              </div>
              <Icon name="chevron-right" size={14} className="ls-step-arrow" />
              <div className={`ls-step ${locked ? "now" : ""}`}>
                <span className="ls-step-dot">3</span>
                <div className="ls-step-tx"><b>Export</b><small>Download labels</small></div>
              </div>
            </div>

            <div className="ls-actions">
              <button className="btn secondary sm" disabled={!lblCanLock(role) || reviewerApproved || checklist.status === "fail" || (fop && fop.status === "REQUIRED" && !fopApplied) || (claimRes && claimRes.counts && claimRes.counts.restricted > 0)} onClick={doApprove}
                title={!lblCanLock(role) ? "Only a Compliance Reviewer or Admin can approve" : checklist.status === "fail" ? "Resolve all failing checks before approving" : (fop && fop.status === "REQUIRED" && !fopApplied) ? "Apply the required FOP symbol to the PDP first" : (claimRes && claimRes.counts && claimRes.counts.restricted > 0) ? "Resolve the claim conflict with the FOP result" : ""}>
                <Icon name={reviewerApproved ? "check-circle-2" : "stamp"} size={14} /> {reviewerApproved ? "Approved" : "Approve (compliance)"}
              </button>
              <button className="btn secondary sm" disabled={!lblCanLock(role) || locked || !reviewerApproved} onClick={doLock}
                title={!lblCanLock(role) ? "Only a Compliance Reviewer or Admin can lock" : !reviewerApproved ? "Approve the label first (Step 1)" : ""}>
                <Icon name={locked ? "lock" : "lock-open"} size={14} /> {locked ? "Locked" : "Lock version"}
              </button>
            </div>

            <div className="ls-exports-lbl">Export tiers</div>
            <div className="ls-exports">
              {LS_EXPORT_TIERS.map((t) => {
                const st = exportState(t.kind);
                return (
                  <button key={t.kind} className={`ls-export ${t.kind === "production" ? "prod" : ""}`} title={st.reason} disabled={st.disabled} onClick={() => doExport(t.kind, t.arg)}>
                    <span className="ls-export-top"><Icon name={t.icon} size={13} /> {t.label}{st.disabled && <Icon name="lock" size={11} className="ls-export-lock" />}</span>
                    <span className="ls-export-cap">{t.cap}</span>
                  </button>
                );
              })}
            </div>
            <div className="ls-hint">
              {!reviewerApproved
                ? "Start at Step 1, a Compliance Reviewer approves the label. Disabled tiers show why on hover."
                : !locked
                ? "Approved ✓, lock the version (Step 2) to unlock the production export."
                : "Locked ✓, all export tiers your role allows are now available."}
            </div>
          </section>
        </aside>
      </div>

      {/* BOTTOM, template carousel */}
      <section className="ls-carousel">
        <div className="ls-carousel-h"><Icon name="layout-template" size={14} /> NFt template inventory <span className="ls-carousel-sub">fit tested against the 15% display-surface cap ({lblMaxNftArea(ads).toFixed(1)} cm²)</span></div>
        <div className="ls-cards">
          {NFT_FORMATS.filter((f) => f.lang === language || f.lang === "bilingual").map((f) => {
            const fits = f.area <= lblMaxNftArea(ads);
            const active = format && format.id === f.id;
            return (
              <button key={f.id} className={`ls-tcard ${active ? "active" : ""} ${fits ? "" : "nofit"}`} onClick={() => setManualFormat(f.id)}>
                <div className="ls-tcard-top"><span className="ls-tcard-nm">{f.label}</span><span className="ls-tcard-lvl">L{f.level}</span></div>
                <div className="ls-tcard-mini"><div className="ls-tcard-mini-t">Nutrition Facts</div><div className="ls-tcard-mini-l" /><div className="ls-tcard-mini-l short" /><div className="ls-tcard-mini-l" /></div>
                <div className="ls-tcard-meta">
                  <span className={`pill ${fits ? "success" : "error"}`} style={{ fontSize: 9.5 }}>{fits ? "Fits" : "Does not fit"}</span>
                  <span className="ls-tcard-area">{f.area} cm² / max {lblMaxNftArea(ads).toFixed(0)}</span>
                </div>
              </button>
            );
          })}
        </div>
      </section>

      {genOpen && (
        <LsGenerateModal onClose={() => setGenOpen(false)} recipe={recipe} labelRef={ref}
          profile={dispProfile} soldProfile={profile} preparedProfile={dispProfile} dual={dualOn}
          language={language} rounded={rounded} format={format} fop={fop} checklist={checklist}
          claimRes={claimRes} isReview={isReview} mayContain={mayContain}
          onGoto={gotoCheck} onAudit={genAudit} toast={toast} />
      )}

      {previewFull && (
        <div className="ls-full-scrim" onClick={() => setPreviewFull(false)}>
          <div className="ls-full" onClick={(e) => e.stopPropagation()}>
            <div className="ls-full-head">
              <div className="ls-full-title"><Icon name="maximize-2" size={16} /> <b>{recipe ? recipe.name : "Label"}</b> <span>{format ? format.label : ""} · {language === "bilingual" ? "Bilingual" : language.toUpperCase()}</span></div>
              <div className="ls-full-actions">
                <button className="btn secondary sm" disabled={!locked} title={locked ? "Print the locked production label" : "Lock the approved version to enable print"} onClick={() => { if (locked) doExport("production", "Production label"); }}>
                  <Icon name="printer" size={14} /> {locked ? "Print / Save PDF" : "Locked when approved"}
                </button>
                <button className="fop-drawer-close" onClick={() => setPreviewFull(false)} aria-label="Close"><Icon name="x" size={20} /></button>
              </div>
            </div>
            <div className="ls-full-body">
              <div className="ls-full-stage">
                <NutritionFactsLabel recipe={recipe} profile={dispProfile} soldProfile={profile} preparedProfile={dispProfile} dual={dualOn} language={language} rounded={rounded} zoom={1.6} format={format} />
                <IngredientStatementBlock recipe={recipe} language={language} zoom={1.6} mayContain={mayContain} />
                {fop.status === "REQUIRED" && fopApplied && (
                  <div className="ls-full-fop"><div className="ls-full-fop-lbl">Front-of-package symbol (PDP)</div><FopSymbol nutrients={fop.symbolNutrients} languageMode={fopLangMode} orientation={fopOrient} size="lg" /></div>
                )}
              </div>
            </div>
            {!locked && <div className="ls-full-note"><Icon name="info" size={13} /> Printing is enabled once the label is approved and the version is locked.</div>}
          </div>
        </div>
      )}

      {pdpFull && (
        <div className="ls-full-scrim" onClick={() => setPdpFull(false)}>
          <div className="ls-full" onClick={(e) => e.stopPropagation()}>
            <div className="ls-full-head">
              <div className="ls-full-title"><Icon name="scan-line" size={16} /> <b>PDP preview</b> <span>Principal display panel</span></div>
              <div className="ls-full-actions">
                <div className="ls-font-ctl">
                  <span>Font</span>
                  <button onClick={() => setPdpFont((f) => Math.max(1, +(f - 0.2).toFixed(1)))} aria-label="Smaller"><Icon name="minus" size={13} /></button>
                  <b>{Math.round(pdpFont * 100)}%</b>
                  <button onClick={() => setPdpFont((f) => Math.min(3, +(f + 0.2).toFixed(1)))} aria-label="Larger"><Icon name="plus" size={13} /></button>
                </div>
                <button className="btn secondary sm" disabled={!locked} title={locked ? "Print the locked production label" : "Lock the approved version to enable print"} onClick={() => { if (locked) doExport("production", "Production label"); }}>
                  <Icon name="printer" size={14} /> {locked ? "Print / Save PDF" : "Locked when approved"}
                </button>
                <button className="fop-drawer-close" onClick={() => setPdpFull(false)} aria-label="Close"><Icon name="x" size={20} /></button>
              </div>
            </div>
            <div className="ls-full-body">
              <div className="ls-pdp-fop-only" style={{ width: (pdpFont * 240) + "px" }}>
                {fop.status === "REQUIRED"
                  ? (fopApplied
                      ? <FopSymbol nutrients={fop.symbolNutrients} languageMode={fopLangMode} orientation={fopOrient} size="lg" />
                      : <div className="ls-pdp-missing"><Icon name="alert-triangle" size={16} /> FOP symbol not placed</div>)
                  : <div className="ls-pdp-none">No FOP symbol required</div>}
              </div>
            </div>
            {!locked && <div className="ls-full-note"><Icon name="info" size={13} /> Use Font − / + to size the panel for print. Printing is enabled once the label is approved and locked.</div>}
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { LabelStudio, NutritionFactsLabel });
